Mastering Java Generics: Parameters, Inheritance, and Wildcards

Parameterized Types in Collections and Comparators

Core Concept of Generics

Parameterized types (generics) allow classes, interfaces, and methods to be designed as templates where the specific data type of a property, return value, or parameter is represented by a placeholder. The actual type argument is supplied when the class is instantiated, the interface is implemented, or the method is invoked.

Note: Generic type parameters strictly accept reference data types, not primitives.

Pre-Generics Collection Issues

Prior to generics, collections operated on Object, leading to two major drawbacks:

  • Compromised Type Safety: Any object type could be added to a collection, causing runtime surprises.
  • Explicit Casting Requirement: Retrieving elements required manual casting, which was verbose and prone to ClassCastException.

Applying Generics to Collections and Comparators

// List implementation
public void processScores() {
    List<Integer> scores = new ArrayList<>();
    scores.add(95);
    scores.add(82);
    // Compile-time error: type safety enforced
    // scores.add("Invalid");
    
    for (Integer mark : scores) {
        int currentScore = mark; // Unboxing without explicit cast
        System.out.println(currentScore);
    }
}

// Map implementation
public void mapDemo() {
    Map<String, Integer> userAges = new HashMap<>(); // Diamond operator for type inference
    userAges.put("Alice", 28);
    userAges.put("Bob", 35);
    
    for (Map.Entry<String, Integer> entry : userAges.entrySet()) {
        System.out.println(entry.getKey() + " -> " + entry.getValue());
    }
}

Since JDK 10, the var keyword can be utilized for local variable type inference, enhancing readability when the compiler can deduce the type.

Generics in Comparators

// Natural ordering using Comparable
public class StaffMember implements Comparable<StaffMember> {
    private String name;
    
    @Override
    public int compareTo(StaffMember other) {
        return this.name.compareTo(other.name);
    }
}

// Custom ordering using Comparator
TreeSet<StaffMember> staffSet = new TreeSet<>((s1, s2) -> {
    return s1.getJoinDate().compareTo(s2.getJoinDate());
});

// Removing elements using Predicate
ids.removeIf(val -> val % 2 == 0);

Custom Generic Structures

Custom Generic Classes and Interfaces

When designing a class or interface with an uncertain type, a generic parameter (e.g., <T>, <E>) is declared. Multiple parameters can be defined if needed (e.g., <K, V>).

class Container<T> {
    private T payload;
    
    public T getPayload() { return payload; }
    public void setPayload(T item) { this.payload = item; }
}

// Instantiation with a concrete type (Wrapper required for primitives)
Container<Integer> intBox = new Container<>();

// Without specifying the type, it defaults to Object (Type Erasure)
Container rawBox = new Container();

Inheritance with Generic Classes

// Subclass fixes the generic type (Not a generic class anymore)
class StringContainer extends Container<String> {}

// Subclass retains the parent's generic parameter (Remains a generic class)
class TypedContainer<T> extends Container<T> {}

// Subclass fixes parent type but introduces its own generic parameter
class MixedContainer<T, V> extends Container<T> {
    private V extraData;
}

Key Rules for Generic Classes

  • Once a type argument is specified during instantiation, all internal usages of that parameter are replaced by the concrete type.
  • If no type argument is provided, the generic parameter is erased to Object.
  • Generic type arguments must be reference types. Primitives are strictly prohibited; use their corresponding wrapper classes.
  • Generic classes can have multiple parameters: <T1, T2, T3>.
  • The diamond operator <> allows omitting the type argument on the right side of instantiation since JDK 7.
  • Abstract parameterized classes or interfaces cannot be directly instantiated.
  • Generic arrays (new T[]) are forbidden. The workaround is casting an Object[]: T[] arr = (T[]) new Object[size];.
  • Static members (methods, properties) cannot use the class-level type parameter because static members are initialized before the type is known at runtime.
  • A Throwable class cannot be parameterized.

Custom Generic Methods

A method is only a generic method if it declares its own type parameter (placed before the return type), regardless of whether the enclosing class is generic.

// The <E> declares this as a generic method
public <E> E processElement(E input) {
    return input;
}

// Generic methods can be static, as their type is determined upon invocation
public static <T> void printArray(T[] array) {
    for (T element : array) {
        System.out.println(element);
    }
}

Practical Application: Data Access Object (DAO)

Using ORM (Object-Relational Mapping) principles, a generic DAO handles CRUD operations across different entity types.

public class BaseRepository<T> {
    public void save(T entity) { /* Persist logic */ }
    
    public T findById(int id) { return null; }
    
    // Independent generic method for specific queries
    public <R> R queryAggregate(String sql) { return null; }
}

// Subclass specifying the exact entity type
class UserRepository extends BaseRepository<User> {}

// Converting Map values to List correctly
public List<T> listAll() {
    Collection<T> records = dataMap.values();
    // Incorrect: Direct cast throws ClassCastException
    // return (List<T>) records;
    
    // Correct: Construct a new List
    return new ArrayList<>(records);
}

Generics and Inheritance

Parameterized types do not follow standard polymorphism based on their type arguments. If Parent is a superclass of Child, Container<Parent> and Container<Child> share no inheritance relationship; they are parallel, independent classes.

List<Object> objList = new ArrayList<>();
List<String> strList = new ArrayList<>();

// Compile error: Incompatible types
// objList = strList;

However, polymorphism applies if the raw types have an inheritance relationship and the type arguments are identical.

List<String> strList = new ArrayList<>();
Collection<String> colList = strList; // Valid

Wildcard Usage

Strict parameterization reduces flexibility. Wildcards (?) restore generic polymorphism. Container<?> acts as the supertype for all Container<T> instances.

  • Unbounded Wildcard (?): Allows reading (returns Object), but forbids writing (except null).
  • Upper Bounded Wildcard (? extends Type): Accepts Type or its subclasses. Allows reading (returns Type), forbids writing.
  • Lower Bounded Wildcard (? super Type): Accepts Type or its superclasses. Allows reading (returns Object), and allows writing Type or its subclasses.
List<? extends Animal> producers = new ArrayList<Dog>();
Animal a = producers.get(0); // Read OK
// producers.add(new Dog()); // Write fails

List<? super Animal> consumers = new ArrayList<Object>();
Object obj = consumers.get(0); // Read OK (returns Object)
consumers.add(new Dog()); // Write OK (Dog is subclass of Animal)
consumers.add(new Animal()); // Write OK

Tags: java generics Type Safety Collections Wildcard

Posted on Thu, 22 Oct 2026 16:51:23 +0000 by diex