Generics in Java
Generics were introduced in Java 5 to provide compile-time type safety and eliminate the need for explicit type casting. They allow classes, interfaces, and methods to operate on objects of various types while providing compile-time type checks.
Why Generics?
- Type Safety: We can hold only a single type of objects in generics. It doesn't allow to store other objects.
- Type Casting is not required: There is no need to typecast the object.
- Compile-Time Checking: It is checked at compile time so problems will not occur at runtime.
int, but you can use Integer.
Type Parameters
Commonly used type parameter names are single, uppercase letters. These stand in for the type argument provided by the user.
| Letter | Meaning |
|---|---|
T | Type |
E | Element (used extensively by the Java Collections Framework) |
K | Key |
V | Value |
N | Number |
Generic Classes
A generic class is defined with a generic type parameter list, enclosed in angle brackets <>. Let's create a generic Box class.
public class GenericBox<T> { private T item; public void setItem(T item) { this.item = item; } public T getItem() { return item; } public static void main(String[] args) { // Box for Integer GenericBox<Integer> intBox = new GenericBox<>(); intBox.setItem(100); System.out.println("Integer Value: " + intBox.getItem()); // Box for String GenericBox<String> strBox = new GenericBox<>(); strBox.setItem("Generics are awesome"); System.out.println("String Value: " + strBox.getItem()); } }
Multiple Type Parameters
A generic class can have multiple type parameters. Here's a generic Pair class using K and V.
public class GenericPair<K, V> { private K key; private V value; public GenericPair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } public static void main(String[] args) { GenericPair<String, Integer> pair1 = new GenericPair<>("Age", 30); System.out.println(pair1.getKey() + ": " + pair1.getValue()); GenericPair<Integer, String> pair2 = new GenericPair<>(404, "Not Found"); System.out.println("Error " + pair2.getKey() + ": " + pair2.getValue()); } }
Generic Methods and Data Structures
Let's build a Generic Stack data structure to understand how generics help build reusable components.
import java.util.ArrayList; import java.util.EmptyStackException; public class GenericStack<T> { private ArrayList<T> elements = new ArrayList<>(); public void push(T item) { elements.add(item); } public T pop() { if (elements.isEmpty()) { throw new EmptyStackException(); } return elements.remove(elements.size() - 1); } public boolean isEmpty() { return elements.isEmpty(); } public static void main(String[] args) { GenericStack<Double> stack = new GenericStack<>(); stack.push(10.5); stack.push(20.2); System.out.println("Popped: " + stack.pop()); System.out.println("Popped: " + stack.pop()); } }
Bounded Type Parameters
Sometimes you want to restrict the types that can be used as type arguments. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for.
public class BoundedGenericDemo { // This method only accepts elements that extend Number public static <T extends Number> double sum(T num1, T num2) { return num1.doubleValue() + num2.doubleValue(); } public static void main(String[] args) { System.out.println("Sum of Integers: " + sum(10, 20)); System.out.println("Sum of Doubles: " + sum(15.5, 4.5)); // sum("Hello", "World"); // Compile-time error! String is not a Number } }
<T extends ClassA & InterfaceB & InterfaceC>. If one of the bounds is a class, it must be specified first.
Wildcards
In generic code, the question mark ?, called the wildcard, represents an unknown type. There are three types of wildcards:
- Unbounded Wildcards
<?>: Matches any type. Useful when methods don't depend on the type parameter. - Upper Bounded Wildcards
<? extends Type>: Matches the type or any of its subclasses. - Lower Bounded Wildcards
<? super Type>: Matches the type or any of its superclasses.
import java.util.Arrays; import java.util.List; public class WildcardDemo { // Upper Bounded Wildcard - Accepts List of Number or its subclasses (Integer, Double, etc.) public static void printNumbers(List<? extends Number> list) { for (Number n : list) { System.out.print(n + " "); } System.out.println(); } // Unbounded Wildcard - Accepts List of any type public static void printAnything(List<?> list) { for (Object obj : list) { System.out.print(obj + " "); } System.out.println(); } public static void main(String[] args) { List<Integer> ints = Arrays.asList(1, 2, 3); List<Double> doubles = Arrays.asList(1.1, 2.2); List<String> strings = Arrays.asList("A", "B"); System.out.print("Integers: "); printNumbers(ints); System.out.print("Doubles: "); printNumbers(doubles); // printNumbers(strings); // Compilation error System.out.print("Strings: "); printAnything(strings); // Unbounded wildcard accepts String } }
Type Erasure & Raw Types
Generics were introduced to Java to provide tighter type checks at compile time. To implement generics, the Java compiler applies type erasure to:
- Replace all type parameters with their bounds or
Objectif unbounded. - Insert type casts if necessary to preserve type safety.
- Generate bridge methods to preserve polymorphism in extended generic types.
List<String> and List<Integer> have the same class at runtime.
Raw Types: Using a generic type without type arguments (e.g., List list = new ArrayList();) is called a raw type. Raw types bypass generic type checks and can lead to ClassCastException at runtime. You should avoid them in modern Java.