All Posts

June 15, 2025

4 min read
JavaProgrammingGenericsType SafetyGeneric MethodsWildcards

Java Generics: Type Safety and Generic Programming Mastery

Welcome back to the Java Fundamentals series! 👋

What Are Generics?

Generics enable types (classes and methods) to operate on objects of various types while providing compile-time type safety. They were introduced in Java 5 to eliminate ClassCastException and provide stronger type checks at compile time.

Without Generics (Pre-Java 5)

List list = new ArrayList();
list.add("abc");
String s = (String) list.get(0); // explicit cast needed

With Generics (Java 5+)

List<String> list = new ArrayList<>();
list.add("abc");
String s = list.get(0); // no cast needed

Why Use Generics?

Generics provide several key benefits:

  • Type Safety: Compile-time error detection instead of runtime ClassCastException
  • Elimination of Type Casting: No need for explicit casting
  • Code Reusability: Write flexible, reusable code that works with different types
  • Better Documentation: Code is self-documenting about expected types

Generic Classes

Generic classes allow you to define a class with type parameters that can be specified when creating instances.

Basic Generic Class

class Box<T> {
    private T value;

    public void set(T value) { 
        this.value = value; 
    }
    
    public T get() { 
        return value; 
    }
}

Using Generic Classes

// Creating instances with different types
Box<String> stringBox = new Box<>();
stringBox.set("Hello Generics!");
String value = stringBox.get(); // No casting needed

Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer number = intBox.get();

You can substitute T with any reference type at instantiation time.

Generic Methods

Generic methods allow you to define methods with their own type parameters, independent of the class type parameters.

Basic Generic Method

public class Utility {
    // Generic method with type parameter <T>
    public static <T> void printArray(T[] array) {
        for (T item : array) {
            System.out.println(item);
        }
    }
}

Using Generic Methods

String[] names = {"Alice", "Bob", "Charlie"};
Integer[] numbers = {1, 2, 3, 4, 5};

Utility.printArray(names);    // Works with String[]
Utility.printArray(numbers);  // Works with Integer[]

Note: The <T> before the return type declares it as a generic method.

Bounded Type Parameters

You can restrict the types used in generics to subclasses (or superclasses) of a particular type using bounded type parameters.

Upper Bound with extends

public class NumberProcessor<T extends Number> {
    private T number;
    
    public NumberProcessor(T number) {
        this.number = number;
    }
    
    public double getDoubleValue() {
        return number.doubleValue(); // Can call Number methods
    }
}

Example Usage

// Valid - Integer extends Number
NumberProcessor<Integer> intProcessor = new NumberProcessor<>(42);

// Valid - Double extends Number  
NumberProcessor<Double> doubleProcessor = new NumberProcessor<>(3.14);

// Invalid - String does not extend Number
// NumberProcessor<String> stringProcessor = new NumberProcessor<>("text"); // Compile error

Accepts Integer, Double, Float, etc.

Multiple Bounds

public class Example<T extends Number & Comparable<T>> {
    // T must extend Number AND implement Comparable
}

Lower Bound with super

Lower bounds are primarily used with wildcards:

List<? super Integer> list = new ArrayList<Number>();
list.add(42);        // Can add Integer and its subtypes
list.add(100);       // Can add Integer and its subtypes
// Object obj = list.get(0); // Can only retrieve as Object

Wildcards ?

Wildcards are used when you don't know the exact type or want to work with a range of types.

Types of Wildcards

Wildcard SyntaxDescription
List<?>List of unknown type
List<? extends Number>List of Number or its subclasses
List<? super Integer>List of Integer or its superclasses

Practical Examples

// Unbounded wildcard
public void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

// Upper bounded wildcard
public double sumNumbers(List<? extends Number> numbers) {
    double sum = 0.0;
    for (Number num : numbers) {
        sum += num.doubleValue();
    }
    return sum;
}

// Lower bounded wildcard
public void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

Generic Interfaces

You can create generic interfaces that define contracts for generic classes.

Example

interface Container<T> {
    void add(T item);
    T get();
    boolean isEmpty();
}

Implementation

class StringContainer implements Container<String> {
    private String item;
    
    @Override
    public void add(String item) { 
        this.item = item; 
    }
    
    @Override
    public String get() { 
        return item; 
    }
    
    @Override
    public boolean isEmpty() { 
        return item == null; 
    }
}

Type Erasure (Important Concept)

Type Erasure is a crucial concept in Java generics that affects how generics work at runtime.

What is Type Erasure?

  • At runtime, all generic type information is erased
  • List<String> and List<Integer> both become just List
  • Generics are a compile-time only feature in Java
  • This maintains backward compatibility with pre-Java 5 code

Consequences of Type Erasure

1. No Generic Arrays

// This will cause a compile error
List<String>[] stringLists = new List<String>[10]; // Error

// This is allowed
List<String>[] stringLists = new List[10]; // Works (with warning)

2. Cannot Use instanceof with Type Parameters

public <T> void checkType(Object obj) {
    // This will cause a compile error
    if (obj instanceof T) { // Error
        // ...
    }
    
    // This is allowed
    if (obj instanceof List<?>) { // Works
        // ...
    }
}

3. No Static Generic Fields

class MyClass<T> {
    // This will cause a compile error
    private static T staticField; // Error
    
    // This is allowed
    private T instanceField; // Works
}

Difference Between <? extends T> and <? super T>

Understanding the difference between upper and lower bounded wildcards is crucial for effective use of generics:

<? extends T><? super T>
Accepts T and subclasses of TAccepts T and superclasses of T
Producer — you can safely read items as TConsumer — you can safely write items as T
Good for reading valuesGood for writing values

Mnemonic: PECSProducer Extends, Consumer Super

Examples

// Upper bounded wildcard - for reading
List<? extends Number> numbers = new ArrayList<Integer>(); // OK
// You can read from it safely
Number num = numbers.get(0); // Works

// Lower bounded wildcard - for writing  
List<? super Integer> integers = new ArrayList<Number>(); // OK
// You can write to it safely
integers.add(42); // Works

Advanced Type Erasure Concepts

Why Can't We Create Generic Arrays?

A common misconception is that "arrays are static" — this is imprecise.

Precise Reason: Because of type erasure — at runtime, Java does not retain generic type information. Since arrays in Java are reifiable (they know their element type at runtime), you cannot create an array of non-reifiable types like List<String>[].

// This causes a compile error
List<String>[] stringLists = new List<String>[10]; // ❌ Error

// Workaround (with unchecked warning)
List<String>[] arr = (List<String>[]) new List[10]; // ⚠️ Warning

Static Generic Methods

Important clarification: You can declare a static generic method. The confusion arises because class-level type parameters are not available in static contexts, but you can declare method-level type parameters for a static method:

public class Utility {
    // ✅ This works - method declares its own type parameter
    public static <T> void display(T value) {
        System.out.println(value);
    }
    
    // ❌ This doesn't work - trying to use class-level type parameter
    // public static void display(T value) { ... }
}

Key Point: The method declares its own type parameter <T> before the return type.

Practical Use Cases

1. Collections Framework

List<String> names = new ArrayList<>();
Set<Integer> uniqueNumbers = new HashSet<>();
Map<String, Person> personMap = new HashMap<>();

2. Custom Data Structures

class GenericStack<T> {
    private List<T> stack = new ArrayList<>();
    
    public void push(T item) {
        stack.add(item);
    }
    
    public T pop() {
        if (stack.isEmpty()) {
            throw new IllegalStateException("Stack is empty");
        }
        return stack.remove(stack.size() - 1);
    }
    
    public boolean isEmpty() {
        return stack.isEmpty();
    }
}

3. Utility Methods

public class CollectionUtils {
    public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
        return list.stream()
                   .filter(predicate)
                   .collect(Collectors.toList());
    }
    
    public static <T extends Comparable<T>> T findMax(List<T> list) {
        return Collections.max(list);
    }
}

4. Type-Safe APIs

public class Repository<T, ID> {
    public T findById(ID id) {
        // Implementation
        return null;
    }
    
    public List<T> findAll() {
        // Implementation
        return new ArrayList<>();
    }
    
    public void save(T entity) {
        // Implementation
    }
}

Best Practices

1. Use Meaningful Type Parameter Names

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

// Less clear
class Pair<T, U> {
    private T first;
    private U second;
}

2. Prefer Generic Methods Over Generic Classes When Possible

// Good - more flexible
public static <T> void swap(T[] array, int i, int j) {
    T temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

3. Use Bounded Wildcards for Greater Flexibility

// More flexible - accepts List<Integer>, List<Double>, etc.
public void processNumbers(List<? extends Number> numbers) {
    // Process numbers
}

Conclusion

ConceptSyntaxDescription
Generic Classclass Box<T>Class with type parameters
Generic Method<T> void method(T param)Method with its own type parameter
Upper Bound<T extends Number>T must be Number or its subtype
Lower Bound<? super Integer>Wildcard for Integer or its supertypes
Unbounded WildcardList<?>List of unknown type
Upper Bounded Wildcard<? extends Number>Number or its subtypes
Lower Bounded Wildcard<? super Integer>Integer or its supertypes

Generics are a powerful feature that makes Java code more type-safe, readable, and maintainable. They're essential for modern Java development and are used extensively in the Collections Framework and many other APIs.

Happy coding! 💻