When working with collections like ArrayList, HashSet, and HashMap, you've probably already seen something like this:
ArrayList<String> names = new ArrayList<>();You may have wondered what the <String> part means.
This is related to Generics.
Generics allow us to specify what type of data a class, method, or collection is allowed to work with.
In simple words, generics help Java make our code type-safe and reusable.
Why Do We Need Generics?
Suppose we create an ArrayList without specifying a type:
ArrayList names = new ArrayList();
names.add("John");
names.add(25);
names.add(true);Now the same list can contain a String, an Integer, and a Boolean.
That can create problems because we don't know what type of data the list contains.
With generics, we can specify exactly what we want:
ArrayList<String> names = new ArrayList<>();Now Java knows that this list should contain only String values.
names.add("John");
names.add("Jason");But this will produce an error:
names.add(25); // ErrorThis is one of the biggest advantages of generics.
Generics with ArrayList
You've already been using generics with ArrayList.
For example:
ArrayList<String> names = new ArrayList<>();Here:
String → type of data the ArrayList storesWe can also use other types:
ArrayList<Integer> numbers = new ArrayList<>();
ArrayList<Double> prices = new ArrayList<>();
ArrayList<Boolean> results = new ArrayList<>();For primitive types, we use their wrapper classes such as Integer instead of int.
For example:
ArrayList<int> numbers; // ErrorInstead:
ArrayList<Integer> numbers = new ArrayList<>();Generic Classes
Generics are not limited to collections. We can create our own generic classes.
For example:
class Box<T> {
T value;
void setValue(T value) {
this.value = value;
}
T getValue() {
return value;
}
}Here, T is a type parameter.
It basically means:
"The type will be decided later."
We can now create a Box for different types.
Box<String> nameBox = new Box<>();
nameBox.setValue("John");
Box<Integer> numberBox = new Box<>();
numberBox.setValue(100);The same Box class works with both String and Integer.
How Does T Work?
In:
class Box<T>T is just a placeholder for a type.
When we write:
Box<String> box = new Box<>();Java treats T as String for that particular object.
When we write:
Box<Integer> box = new Box<>();Java treats T as Integer.
So we can think of it like:
Box<T>
↓
T can become different types
Box<String>
↓
T = String
Box<Integer>
↓
T = IntegerThe letter T is a common convention for "Type", but you can technically use other valid names.
Generic Methods
We can also create methods that work with different types.
For example:
class Utility {
static <T> void printValue(T value) {
System.out.println(value);
}
}Now the same method can work with different types:
Utility.printValue("John");
Utility.printValue(100);
Utility.printValue(25.5);Output:
John
100
25.5The <T> before the return type tells Java that this method uses a generic type parameter.
Generic Methods with Return Values
A generic method can also return a value.
class Utility {
static <T> T getValue(T value) {
return value;
}
}Now:
String name = Utility.getValue("John");
Integer number = Utility.getValue(100);Java understands the appropriate type based on what we pass to the method.
Multiple Type Parameters
We can use more than one type parameter.
For example:
class Pair<K, V> {
K key;
V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
}Here, K and V represent two different types.
We can create:
Pair<String, Integer> student = new Pair<>("John", 85);Here:
K → String
V → IntegerSo the object contains:
John → 85This idea is similar to how a HashMap works, where we have a key type and a value type.
Benefits of Generics
Generics provide several important benefits.
Type safety: Java can catch incorrect types during compilation.
Code reuse: One generic class or method can work with many different types.
Less casting: We don't need to constantly convert objects back to their original types.
Cleaner code: The intended type of data is clearly visible.
For example:
ArrayList<String> names = new ArrayList<>();Immediately tells us that names should contain Strings.
A Complete Example
Let's create a generic Box class:
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
class Main {
public static void main(String[] args) {
Box<String> nameBox = new Box<>();
nameBox.setValue("John");
Box<Integer> numberBox = new Box<>();
numberBox.setValue(100);
System.out.println(nameBox.getValue());
System.out.println(numberBox.getValue());
}
}Output:
John
100Notice that we didn't have to create separate classes like StringBox and IntegerBox. The same generic Box<T> works with both.
The Main Idea
Generics might look complicated at first, but the basic idea is simple:
Generics allow us to write reusable code that can work with different data types while still maintaining type safety.
When you see:
ArrayList<String>think:
"This
ArrayListis specifically for Strings."
And when you see:
class Box<T>think:
"The type will be decided when we use this class."
Once you understand this idea, the <T>, <K>, and <V> that you often see in Java code become much easier to understand.