Deep Dive into Java Generics
The ArrayList
is a generic class. By specifying different types, we can store various data types in collections while enforcing type constraints (one of the key advantages of generics).
1. What Are Generics?
Generics, or “parameterized types,” address code redundancy in scenarios like creating container classes for different data types (e.g., String
, Integer
, or custom types). Instead of rewriting logic for each type, a placeholder T represents any type, allowing algorithms to work universally. The ArrayList
is a prime example, seamlessly handling any data type.
Non-Generic Implementation Example:
class MyList {
private Object[] elements = new Object[10];
private int size;
public void add(Object item) {
elements[size++] = item;
}
public Object get(int index) {
return elements[index];
}
}
This approach risks runtime errors due to unsafe casts:
MyList list = new MyList();
list.add("Hello");
Integer num = (Integer) list.get(0); // Throws ClassCastException
Generics eliminate such risks through compile-time type checks.
2. Java Generics in Depth
1. Generic Classes
Definition:
class ClassName<T1, T2, ..., Tn> { /* ... */ }
Example:
class DataHolder<T> {
private T item;
public void setData(T t) { this.item = t; }
public T getData() { return this.item; }
}
Usage:
DataHolder<String> holder = new DataHolder<>();
holder.setData("Generic");
String data = holder.getData(); // No cast needed
Multiple Type Parameters:
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) { /* ... */ }
// Getters & Setters
}
2. Generic Methods
Can exist in generic or non-generic classes.
Syntax:
<T1, T2, ...> ReturnType methodName(Parameters) { /* ... */ }
Example:
class Utils {
public static <T> T getMiddle(T[] array) {
return array[array.length / 2];
}
}
Invocation:
String[] names = {"Alice", "Bob", "Charlie"};
String middle = Utils.<String>getMiddle(names); // Explicit type (optional)
3. Generic Interfaces
Definition:
public interface Generator<T> {
T next();
}
Implementations:
- Concrete Type:
class StringGenerator implements Generator<String> {
@Override
public String next() { return "Next String"; }
}
- Generic Type:
class FruitGenerator<T> implements Generator<T> {
@Override
public T next() { /* ... */ }
}
4. Type Erasure
Java generics use type erasure for backward compatibility. At compile time, generic types are erased and replaced with bounds (e.g., Object
or upper bounds).
Example:
// Pre-compilation
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);
// Post-compilation (erasure)
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0); // Compiler inserts cast
Erasure Rules:
- Unbounded types (e.g.,
<T>
) becomeObject
. - Bounded types (e.g.,
<T extends Number>
) become the bound (e.g.,Number
).
5. Wildcards
- Upper-Bounded (
<? extends T>
):
AcceptsT
or its subtypes. Readable but not writable (exceptnull
):
List<? extends Number> numbers = new ArrayList<Double>();
Number num = numbers.get(0); // Allowed
// numbers.add(3.14); // Compile error
- Lower-Bounded (
<? super T>
):
AcceptsT
or its supertypes. Writable but readable only asObject
:
List<? super Integer> list = new ArrayList<Number>();
list.add(100); // Allowed
Object obj = list.get(0); // Read as Object
- Unbounded (
<?>
):
Represents an unknown type. Only allowsnull
writes andObject
reads:
List<?> list = new ArrayList<String>();
// list.add("Hello"); // Compile error
list.add(null); // Allowed
Object o = list.get(0);
3. PECS Principle (Producer-Extends, Consumer-Super)
- Producer (Read-Only): Use
<? extends T>
for data sources:
void printCollection(Collection<? extends Number> nums) {
for (Number num : nums) System.out.println(num);
}
- Consumer (Write-Only): Use
<? super T>
for data sinks:
void addNumbers(Collection<? super Integer> list) {
for (int i = 0; i < 10; i++) list.add(i);
}
4. Overcoming Type Erasure Limitations
1. Explicit Type Checks
Use Class
objects to retain type information:
class GenericType<T> {
private Class<T> type;
public GenericType(Class<T> type) { this.type = type; }
public boolean isInstance(Object obj) {
return type.isInstance(obj);
}
}
2. Instantiating Generic Types
Leverage factories or method references:
interface Factory<T> { T create(); }
class Creater<T> {
private Factory<T> factory;
public Creater(Factory<T> factory) { this.factory = factory; }
public T newInstance() { return factory.create(); }
}
// Usage
Creater<String> creater = new Creater<>(() -> "New Instance");
String str = creater.newInstance();
3. Generic Arrays
Create via reflection (with caution):
class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
public T get(int index) { return array[index]; }
public void set(int index, T item) { array[index] = item; }
}
5. Key Takeaways
1. Advantages of Generics:
- Type Safety: Compile-time checks prevent
ClassCastException
. - Code Reusability: Single implementation for multiple types.
- Clarity: Eliminates explicit casting.
2. Caveats:
- Runtime type information is erased.
- Primitive types (e.g., int) require wrapper classes (e.g., Integer).
- Use wildcards judiciously (follow PECS).
3. Common Use Cases:
- Collections Framework (
ArrayList<T>
,HashMap<K,V>
). - Utility classes (
Collections.sort()
,Comparator<T>
). - Callbacks (generic interfaces, event handlers).
Comments 0
There are no comments yet.