Chapter 47 of 57

Generics in Java

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); // Error

This 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 stores

We 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; // Error

Instead:

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 = Integer

The 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.5

The <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 → Integer

So the object contains:

John → 85

This 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
100

Notice 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 ArrayList is 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.