2

Deep Dive into Java Generics

63

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>) become Object.
  • Bounded types (e.g., <T extends Number>) become the bound (e.g., Number).

5. Wildcards

  • ​Upper-Bounded (<? extends T>):
    Accepts T or its subtypes. Readable but not writable (except null):
List<? extends Number> numbers = new ArrayList<Double>(); Number num = numbers.get(0); // Allowed // numbers.add(3.14); // Compile error
  • ​Lower-Bounded (<? super T>):
    Accepts T or its supertypes. Writable but readable only as Object:
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 allows null writes and Object 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

avatar
There are no comments yet.

There are no comments yet.