Java Generics – Type-Safe Programming
100%

Java Generics

Java Generics allow classes, interfaces, and methods to work with different data types while providing compile-time type safety. Generics reduce the need for explicit type casting and help prevent type-related errors.

1. Introduction to Generics

Without generics, a collection such as ArrayList can store different kinds of objects when used as a raw type. This can lead to errors when retrieving values.

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();

        list.add("Rahul");
        list.add(100);

        String name = (String) list.get(0);

        System.out.println(name);
    }
}

This code uses a raw ArrayList. It allows different types of objects and requires casting when retrieving values.

Using generics makes the expected type clear:

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();

        names.add("Rahul");
        names.add("Amit");

        System.out.println(names.get(0));
    }
}

Output:

Rahul

2. Generic Collections

You can specify the type of elements a collection should store by using angle brackets <>.

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        ArrayList<Integer> numbers = new ArrayList<>();
        ArrayList<Double> prices = new ArrayList<>();

        names.add("Priya");
        numbers.add(50);
        prices.add(99.50);

        System.out.println(names);
        System.out.println(numbers);
        System.out.println(prices);
    }
}

Output:

[Priya]
[50]
[99.5]

For example, ArrayList<String> accepts String values but does not accept an Integer value. This type restriction is checked by the compiler.

3. Creating a Generic Class

A generic class uses a type parameter, commonly written as T. The type is supplied when an object is created.

class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<String> box = new Box<>();
        box.setValue("Hello Java");

        System.out.println(box.getValue());

        Box<Integer> numberBox = new Box<>();
        numberBox.setValue(100);

        System.out.println(numberBox.getValue());
    }
}

Output:

Hello Java
100

The same Box class works with different reference types without needing separate classes for each type.

4. Understanding the Type Parameter T

The letter T is a conventional name for a generic type parameter. It is not a reserved Java keyword.

class Display<T> {
    T data;

    Display(T data) {
        this.data = data;
    }

    void show() {
        System.out.println(data);
    }
}

public class Main {
    public static void main(String[] args) {
        Display<String> d1 = new Display<>("Java");
        Display<Integer> d2 = new Display<>(25);

        d1.show();
        d2.show();
    }
}

Output:

Java
25

Common generic parameter naming conventions:

Letter Common meaning
TType
EElement
KKey
VValue
NNumber

5. Generic Class with Two Type Parameters

A generic class can accept more than one type parameter. For example, a class can store a key and a value.

class Pair<K, V> {
    private K key;
    private V value;

    Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    void display() {
        System.out.println("Key: " + key);
        System.out.println("Value: " + value);
    }
}

public class Main {
    public static void main(String[] args) {
        Pair<Integer, String> student =
            new Pair<>(101, "Rahul");

        student.display();
    }
}

Output:

Key: 101
Value: Rahul

Here, K represents the key type and V represents the value type.

6. Creating a Generic Method

A generic method declares its own type parameter before the return type. It can be used independently of whether its containing class is generic.

public class Main {

    public static <T> void display(T value) {
        System.out.println(value);
    }

    public static void main(String[] args) {
        display("Hello");
        display(100);
        display(25.5);
    }
}

Output:

Hello
100
25.5

Java can usually infer the type argument from the value passed to the method.

7. Generic Method Returning a Value

A generic method can return a value of its type parameter.

public class Main {

    public static <T> T getValue(T value) {
        return value;
    }

    public static void main(String[] args) {
        String name = getValue("Rahul");
        Integer number = getValue(50);

        System.out.println(name);
        System.out.println(number);
    }
}

Output:

Rahul
50

The return type T matches the inferred type of the argument in each call.

8. Bounded Type Parameters

A bounded type parameter restricts which types can be used. For example, T extends Number allows Number and its subclasses, such as Integer and Double.

class Calculator<T extends Number> {
    private T number;

    Calculator(T number) {
        this.number = number;
    }

    double getDoubleValue() {
        return number.doubleValue();
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator<Integer> c1 = new Calculator<>(100);
        Calculator<Double> c2 = new Calculator<>(25.5);

        System.out.println(c1.getDoubleValue());
        System.out.println(c2.getDoubleValue());
    }
}

Output:

100.0
25.5

A type such as String cannot be used with this Calculator class because String does not extend Number.

9. Multiple Bounds

A type parameter can have more than one bound. When using a class and interfaces, the class bound must appear first.

class Data<T extends Number & Comparable<T>> {
    private T value;

    Data(T value) {
        this.value = value;
    }

    T getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Data<Integer> data = new Data<>(50);

        System.out.println(data.getValue());
    }
}

Output:

50

Here, T must extend Number and implement Comparable for the same type. Integer satisfies both requirements.

10. Wildcards in Generics

The wildcard symbol ? represents an unknown type. It is useful when a method should accept collections of different element types.

import java.util.Arrays;
import java.util.List;

public class Main {

    public static void display(List<?> items) {
        for (Object item : items) {
            System.out.println(item);
        }
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Rahul", "Amit");
        List<Integer> numbers = Arrays.asList(10, 20, 30);

        display(names);
        display(numbers);
    }
}

Output:

Rahul
Amit
10
20
30

List<?> can accept a List with any element type. Reading an element from it gives a value that can safely be treated as an Object.

11. Upper and Lower Bounded Wildcards

Upper-bounded wildcard

<? extends Number> accepts a collection whose element type is Number or a subtype of Number.

import java.util.List;

public class Main {
    public static double total(List<? extends Number> numbers) {
        double sum = 0;

        for (Number number : numbers) {
            sum += number.doubleValue();
        }

        return sum;
    }
}
Lower-bounded wildcard

<? super Integer> accepts a collection whose element type is Integer or a supertype of Integer, such as Number or Object.

import java.util.List;

public class Main {
    public static void addNumbers(List<? super Integer> numbers) {
        numbers.add(10);
        numbers.add(20);
    }
}
Tip: Use extends when you mainly need to read values as a given upper-bound type. Use super when you need to add values of the specified lower-bound type.

12. Benefits and Limitations of Generics

Benefits
  • Provides compile-time type safety.
  • Reduces the need for explicit type casting.
  • Allows classes and methods to be reused for different types.
  • Makes code easier to understand and maintain.
Important limitations
  • Primitive types such as int cannot be used directly as type arguments; use Integer instead.
  • Java generally implements generics through type erasure.
  • You cannot create an object directly with new T().
  • You cannot create a generic array directly with new T[10].

For example, use this:

ArrayList<Integer> numbers = new ArrayList<>();

Instead of trying to use a primitive type argument:

// Invalid Java:
ArrayList<int> numbers = new ArrayList<>();

13. Practice Assignments

  1. Create an ArrayList of String values using generics.
  2. Create a generic Box class that stores and returns a value.
  3. Create a generic Display method that prints any value.
  4. Create a generic Pair class with two type parameters.
  5. Create a Calculator class restricted to Number types.
  6. Write a generic method that returns the same value it receives.
  7. Create a method that accepts a List of any type using a wildcard.
  8. Write a method that calculates the sum of a List of Number values.
  9. Write a method that adds integers to a List using a lower-bounded wildcard.
  10. Explain why ArrayList<Integer> is valid but ArrayList<int> is invalid.

🧠 Quick Quiz

1. What is the main purpose of Java Generics?




2. Which type parameter is commonly used to represent a generic type?




3. Which declaration is valid Java?