Skip to main content

Introduction to Generics

Generics were introduced in Java 5 (JDK 1.5) to provide tighter type checks at compile time and to support generic programming.

In a nutshell, Generics allow you to specify the exact type of object that a class, interface, or method can operate on. This is most commonly seen when using the Collections Framework (e.g., specifying that a List should only hold String objects).


Why do we need Generics?

Before Generics were introduced, collections in Java could hold any type of object. While this seemed flexible, it caused massive issues in large codebases.

Generics solve two major problems:

1. Type Safety

Without generics, you can add any object (an Integer, a String, a custom object) to a single Collection. If you accidentally add a String to a list that was supposed to only hold Integers, the compiler won't complain, but your program will crash at runtime when you try to use it.

Generics enforce Type Safety, shifting the error from a runtime exception to a compile-time error. It is always better to catch bugs during compilation!

2. Elimination of Type Casting

Before generics, when you retrieved an object from a collection, you received a generic Object reference. You had to explicitly cast it back to its original type before using it. With generics, the compiler knows exactly what type the collection holds, completely eliminating the need for manual casting.


Example: Before vs After Generics

Let's look at how the introduction of Generics drastically improved the developer experience when working with an ArrayList.

Before Generics (Java 1.4 and earlier)

import java.util.ArrayList;
import java.util.List;

public class LabGenerics1 {

public static void main(String args[]) {
// A non-generic list can hold absolutely anything
List list = new ArrayList();

list.add("Hello");
list.add(10); // Accidentally adding an Integer! No compile error.

// We must manually cast the retrieved object to a String
String s1 = (String) list.get(0);

// RUNTIME ERROR: ClassCastException! Cannot cast Integer to String.
String s2 = (String) list.get(1);
}
}

After Generics (Java 5 and later)

By placing the type inside angle brackets < >, we tell the compiler exactly what this list is allowed to hold.

import java.util.ArrayList;
import java.util.List;

public class LabGenerics2 {

public static void main(String args[]) {
// A generic list explicitly restricted to Strings
List<String> list = new ArrayList<String>();

list.add("Hello");

// COMPILE-TIME ERROR! The compiler stops you from making a mistake.
// list.add(10);

// No casting required! The compiler knows it's a String.
String s1 = list.get(0);
}
}

[!NOTE] The Diamond Operator (<>): Introduced in Java 7, you no longer need to duplicate the type parameter on the right side of the assignment. The compiler infers it.

Old way: List<String> list = new ArrayList<String>(); Modern way: List<String> list = new ArrayList<>();