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 anObject[]: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
Throwableclass 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; // ValidWildcard Usage
Strict parameterization reduces flexibility. Wildcards (?) restore generic polymorphism. Container<?> acts as the supertype for all Container<T> instances.
- Unbounded Wildcard (
?): Allows reading (returnsObject), but forbids writing (exceptnull). - Upper Bounded Wildcard (
? extends Type): AcceptsTypeor its subclasses. Allows reading (returnsType), forbids writing. - Lower Bounded Wildcard (
? super Type): AcceptsTypeor its superclasses. Allows reading (returnsObject), and allows writingTypeor 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