Introduction to Generics in Java
Introduction
Before Java 5, collections and various utility classes operated on the Object type. This meant you could put any object into any collection, but retrieving it required explicit casting. This approach was error-prone, as type mismatches were only caught at runtime, resulting in the dreaded ClassCastException.
Generics were introduced in Java 5 to solve this problem. Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. Much like the more familiar formal parameters used in method declarations, type parameters provide a way for you to re-use the same code with different inputs. The difference is that the inputs to formal parameters are values, while the inputs to type parameters are types.
Core Concept: Why Use Generics?
Stronger Type Checks at Compile Time
The Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is much easier and safer than fixing runtime errors.
Elimination of Casts
Without generics, extracting an item from a collection requires a cast:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // Explicit cast required
With generics, the cast is implicit and handled by the compiler:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // No cast required
Writing Generic Classes
A generic class is defined with a type parameter section enclosed in angle brackets <>.
/**
* A generic Box class that can hold any type of object.
* @param <T> The type of the value being boxed.
*/
public class Box<T> {
private T object;
public void set(T object) {
this.object = object;
}
public T get() {
return object;
}
}
Type Parameter Naming Conventions
By convention, type parameter names are single, uppercase letters. This makes it easy to tell the difference between a type variable and an ordinary class or interface name.
- E - Element (used extensively by the Java Collections Framework)
- K - Key
- N - Number
- T - Type
- V - Value
- S, U, V etc. - 2nd, 3rd, 4th types
Generic Methods
You can also define generic methods. The type parameter is introduced before the return type.
public class Util {
// Generic method to compare two Box objects
public static <T> boolean compareBoxes(Box<T> b1, Box<T> b2) {
return b1.get().equals(b2.get());
}
}
Bounded Type Parameters
Sometimes you want to restrict the types that can be used as type arguments in a parameterized type. 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.
Use the extends keyword to set an upper bound.
public class MathBox<T extends Number> {
private T number;
public MathBox(T number) {
this.number = number;
}
public double doubleValue() {
return number.doubleValue(); // Safe because T extends Number
}
}
You can specify multiple bounds using the & symbol (e.g., <T extends ClassA & InterfaceB>).
Type Erasure
It is critically important to understand how Java compiles generics. Java uses a process called Type Erasure to implement generics.
During compilation, the compiler:
- Replaces all type parameters in generic types with their bounds or
Objectif the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods. - Inserts type casts if necessary to preserve type safety.
- Generates bridge methods to preserve polymorphism in extended generic types.
Because of type erasure, you cannot use generic type parameters in certain ways at runtime (e.g., you cannot do new T(), or instanceof T).
Summary
- Generics add stability to your code by making more of your bugs detectable at compile time.
- They allow you to write flexible, reusable code.
- Remember type erasure: generic types do not exist at runtime.