Understanding Generic Types
Generics shift type checking from runtime to compile time, offering enhanced code readability and type safety compared to using Object references with explicit casting. This programming paradigm enables code reuse across different object types, similar to C++ templates.
Collections like ArrayList particularly benefit from generics. Without generics, collections relied on inheritance and maintained Object arrays, treating all elements as Object instances. Generics introduce type parameters to address this:
List<String> strings = new ArrayList<String>();
This approach provides:
- Readability through explicit type declaration
- Compile-time type checking to prevent invalid type insertion
- Elimination of explicit casting when retrieving elements
Generic Classes
Define generic classes with type parameters:
public class Container<T> {
private T storedElement;
}
The <T> syntax declares a type parameter. Instantiate with specific types:
Container<String> textContainer = new Container<String>();
Multiple type parameters are separated by commas: <S, D>. Use meaningful single-letter names like T (type), S (source), D (destination), or E (element) for clarity.
Generic Methods
Generic methods declare type parameters before the return type:
public static <T> T processElement(T input) {
return input;
}
Example implementation:
public class TypeProcessor {
public static <T> T findMiddle(T... elements) {
return elements[elements.length / 2];
}
}
Explicit invocation: String result = TypeProcessor.<String>findMiddle("first", "middle", "last");
Type inference allows simplified calls: TypeProcessor.findMiddle("first", "middle", "last");
Type inference determines the most specific common type:
Object mixed = TypeProcessor.findMiddle("text", 0, "end");
System.out.println(mixed.getClass()); // Outputs: class java.lang.Integer
System.out.println(mixed); // Outputs: 0
Type parameters precede the return type to avoid ambiguity with comma operators.
Generic Methods vs Generic Class Methods
When generic methods exist within generic classes:
public class GenericContainer<T> {
public <T> void displayElement(T element) {
System.out.println("Element: " + element);
}
}
Class type parameters and method type parameters are distinct:
String message = "hello";
new GenericContainer<Integer>().<String>displayElement(message); // Valid
Without the method type parameter <T>, the method uses the class type parameter.
Type Parameter Constraints
Constrain type parameters to specific class hierarchies:
<T extends Comparable>
This syntax means T must be Comparable or its subtype. The extends keyword covers both classes and interfaces for consistency.
Multiple constraints use &:
<T, S extends Comparable & Serializable>
Constraints enable:
- Type safety by restricting valid subtypes
- Access to constraint type methods
public class ConstrainedContainer<T extends List<T>> {
private T listElement;
public T getElement() {
return listElement;
}
public void setElement(T element) {
// Can access List methods like element.size()
this.listElement = element;
}
}
Note: final classes like String can appear in constraints but generate warnings.
Type Erasure
Generics exist only at compile time. The JVM processes generic types as raw types after erasure.
Erasure Process
Type parameters in definitions are removed:
public class GenericClass<T extends Comparable> {}
// Becomes: public class GenericClass {}
Type parameter references become their bounds:
public T element; // Becomes: public Comparable element;
Unbounded types become Object:
public class GenericClass<T> {
public T element; // Becomes: public Object element;
}
Multiple bounds use the first bound:
public class GenericClass<T extends Comparable & Serializable> {
public T element; // Becomes: public Comparable element;
}
The compiler inserts casts where necessary for additional bounds.
Erasure Artifacts
Bytecode retains generic signatures for reflection:
Signature: #8 // TT;
These signatures preserve original type information without affecting runtime behavior.
Generic Limitations
Primitive Type Restrictions
Type parameters cannot be primitive types (byte, char, short, int, long, float, double, boolean) due to Object incompatibility.
Runtime Type Checks
Runtime type checks with generics are limited:
if(obj instanceof Pair<String>) {} // Compilation error
Pair<String> stringPair = (Pair<String>) obj; // Warning
Pair<String> stringPair;
Pair<Integer> intPair;
intPair.getClass() == stringPair.getClass(); // true
Generic Array Creation
Generic array creation is restricted:
Processor<User>[] processors = null; // Valid
processors = new Processor<User>[10]; // Compilation error
Workaround with wildcard array and cast:
processors = (Processor<User>[]) new Processor<?>[10]; // Unsafe
Use collections like ArrayList for type-safe alternatives.
Varargs Warnings
Varargs with generics generate unchecked warnings:
public static <T> T getMiddle(T... elements) {
return elements[elements.length / 2];
}
Suppress warnings with annotations:
@SafeVarargson method declaration@SuppressWarnings("unchecked")on method call
Generic Instance Creation
Direct generic instantiation fails:
T instance = new T(); // Error
T.class.newInstance(); // Error
Solution: Pass Class<T> parameter:
public void createInstance(Class<T> typeClass) {
T instance = null;
try {
instance = typeClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Created: " + instance);
}
Static Context Restrictions
Generic types cannot appear in static contexts:
public class Singleton<T> {
private static T singleton; // Error
public static T getInstance() {} // Error
public static void process(T element) {} // Error
}
Static generic methods are valid:
public static <T> T getInstance() { return null; } // Valid
public static <T> void process(T element) {} // Valid
This restriction exists becuase type parameters are instance-specific while static members are class-level.
Generic Exception Handling
Generic Throwable subclasses are prohibited:
public class GenericException<T> extends Throwable {} // Error
However, Throwable can be used in type parameters:
public static class ExceptionWrapper<T extends Throwable> {
public void execute(T exception) throws T {
try {
int result = 3 / 0;
} catch(Throwable cause) {
exception.initCause(cause);
throw exception;
}
}
}
This pattern can bypass checked exception requirements.
Erasure Conflicts
Overloading vs Overriding
Consider inheritance with generics:
public class Parent<T> {
public void setValue(T value) {
System.out.println("Parent: " + value);
}
}
public class Child extends Parent<String> {
public void setValue(String value) {
System.out.println("Child: " + value);
}
}
After erasure, Parent's setValue becomes setValue(Object). The Child class overrides rather than overloads this method. The compiler generates bridge methods to maintain polymorphism.
Multiple Interface Implementation
A class cannot implement the same generic interface with different type arguments:
public class Parent implements Comparator<Parent> {
@Override
public int compare(Parent o1, Parent o2) {
return 0;
}
}
public class Child extends Parent implements Comparator<Child> {
// Error: Interface implemented multiple times
}
Erasure causes both to become Comparator<Object>, creating conflict.
Wildcard Types
Wildcards enable flexible generic type relationships.
Upper Bounded Wildcards
Pair<? extends Number> accepts Pair<Integer>, Pair<Double>, etc.
Upper bounded wildcards are read-only:
public static void processUpper() {
Container<Integer> intContainer = new Container<Integer>();
intContainer.setValue(123);
Container<? extends Number> numContainer = intContainer;
Integer value = 100;
numContainer.setValue(value); // Compilation error
Number retrieved = numContainer.getValue(); // Valid
System.out.println(retrieved);
}
Setter restriction ensures type safety when the actual type is unknown.
Lower Bounded Wildcards
Pair<? super Integer> accepts Pair<Integer>, Pair<Number>, Pair<Object>.
Lower bounded wildcards are write-only:
public static void processLower() {
Container<Number> numContainer = new Container<Number>();
numContainer.setValue(123);
Container<? super Integer> superContainer = numContainer;
Integer value = 100;
superContainer.setValue(value); // Valid
Integer retrieved = superContainer.getValue(); // Error
Object obj = superContainer.getValue(); // Valid
System.out.println(obj);
}
Getter restriction exists because the return type could be any supertype.
Unbounded Wildcards
Pair<?> equals Pair<? extends Object>
Unbounded wildcards are read-only and return values as Object.
Wildcard Guidelines
- Bounded wildcards include the bound type
- Upper bounds: read-only, no writes
- Lower bounds: write-only, limited reads (Object only)
- Unbounded: read-only
- For full read-write access, avoid wildcards
- extends and super cannot be combined
Wildcard Capture
Capture wildcard types with helper methods:
@Data
class KeyValuePair<T> {
private T key;
private T value;
}
private static <T> void swapHelper(KeyValuePair<T> pair) {
T temp = pair.getKey();
pair.setKey(pair.getValue());
pair.setValue(temp);
}
public static void swap(KeyValuePair<?> pair) {
swapHelper(pair);
}
This technique captures the wildcard type as a specific type parameter.
Generics and Inheritance
Inheritance Principles
Subclasses must resolve parent class type parameters:
public class Child extends Parent<String> {}
public class GenericChild<T> extends Parent<T> {}
Type Relationships
Generic types with different parameters have no inheritance relationship:
Pair<Child> childPair = new Pair<>();
Pair<Parent> parentPair = childPair; // Error
Generic classes can extend other generic classes:
ArrayList<T> implements List<T>
Raw types and parameterized types are compatible:
Parent<String> paramParent = new Parent<>();
Parent rawParent = paramParent; // Valid but unsafe
Wildcard Inheritance
Container<? extends Child> is a subtype of Container<? extends Parent>
Container<? extends Object> equals Container<?>
Generics and Reflection
Reflection API includes generic-aware components:
Class<T>.getGenericSuperclass()ParameterizedType
Practical Example: Generic DAO
Entity class:
@Data
public class User {
private Integer id;
private String name;
}
Generic DAO implementation:
public abstract class AbstractDao<T, K> {
private Class<T> entityClass;
private Class<K> keyClass;
public AbstractDao() {
Type genericType = getClass().getGenericSuperclass();
ParameterizedType paramType = (ParameterizedType) genericType;
Type[] typeArgs = paramType.getActualTypeArguments();
entityClass = (Class<T>) typeArgs[0];
keyClass = (Class<K>) typeArgs[1];
}
public void persist(T entity) {
StringBuilder sql = new StringBuilder("INSERT INTO ");
sql.append(entityClass.getSimpleName());
Field[] fields = entityClass.getDeclaredFields();
String fieldNames = Arrays.stream(fields)
.map(Field::getName)
.collect(Collectors.joining(","));
sql.append("(").append(fieldNames).append(") VALUES(");
sql.append(fieldNames.replaceAll("[^,]+", "?"));
sql.append(")");
System.out.println(sql.toString());
}
public void remove(K key) {
String sql = "DELETE FROM " + entityClass.getSimpleName() + " WHERE ID=?";
System.out.println(sql);
}
public void modify(T entity) {
StringBuilder sql = new StringBuilder("UPDATE ");
sql.append(entityClass.getSimpleName());
sql.append(" SET ");
Field[] fields = entityClass.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
if (fields[i].getName().equalsIgnoreCase("id")) continue;
sql.append(fields[i].getName()).append("=?");
if (i < fields.length - 1) sql.append(",");
}
sql.append(" WHERE ID=?");
System.out.println(sql.toString());
}
public T retrieve() throws Exception {
T entity = entityClass.newInstance();
Map<String, Object> result = new HashMap<>();
Field[] fields = entityClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
field.set(entity, result.get(field.getName()));
}
return entity;
}
}
Concrete implementation:
public class UserDao extends AbstractDao<User, Integer> {
public static void main(String[] args) {
UserDao dao = new UserDao();
User user = new User();
dao.persist(user);
dao.remove(1);
dao.modify(user);
try {
User retrieved = dao.retrieve();
System.out.println(retrieved);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Output:
INSERT INTO User(id,name) VALUES(?,?)
DELETE FROM User WHERE ID=?
UPDATE User SET name=? WHERE ID=?
User(id=1, name=Peter)