Generics in Java
Generics let a class or method work with a type supplied by the caller, moving whole families of errors from runtime to compile time.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
The problem generics solve
List names = new ArrayList(); // a raw type
names.add("Meera");
names.add(42); // nothing objects
String first = (String) names.get(0); // a cast at every read
String second = (String) names.get(1); // ClassCastException at runtimeList<String> names = new ArrayList<>();
names.add("Meera");
// names.add(42); // compile error, caught immediately
String first = names.get(0); // no cast neededGenerics give a collection a type parameter. The compiler then rejects the wrong type at the point it is added, and removes every cast on the way out.
Generic classes
public class Box<T> {
private T content;
public void put(T content) {
this.content = content;
}
public T get() {
return content;
}
public boolean isEmpty() {
return content == null;
}
}Box<String> textBox = new Box<>();
textBox.put("a note");
String value = textBox.get(); // typed, no cast
Box<Integer> numberBox = new Box<>();
numberBox.put(42);
// numberBox.put("no"); // compile errorT is a placeholder that the caller fills in. The diamond <> on the right hand side lets the compiler infer it from the declaration.
Naming conventions for type parameters
| Letter | Meaning |
|---|---|
T | Type |
E | Element, used by collections |
K, V | Key and Value |
R | Result |
N | Number |
S, U | Second and third types |
More than one parameter
public record Pair<K, V>(K key, V value) {
public Pair<V, K> swapped() {
return new Pair<>(value, key);
}
}Pair<String, Integer> entry = new Pair<>("java", 15);
System.out.println(entry.key()); // java
System.out.println(entry.swapped()); // Pair[key=15, value=java]Generic methods
public static <T> List<T> repeat(T value, int times) {
List<T> result = new ArrayList<>();
for (int i = 0; i < times; i++) {
result.add(value);
}
return result;
}List<String> blanks = repeat("n/a", 3); // T inferred as String
List<Integer> zeros = repeat(0, 5); // T inferred as Integer
List<String> explicit = Notes.<String>repeat("x", 2); // rarely neededThe type parameter is declared before the return type. A generic method can live in a non generic class, and its parameter is independent of any class level one.
public static <K, V> Map<V, K> invert(Map<K, V> source) {
Map<V, K> result = new LinkedHashMap<>();
source.forEach((key, value) -> result.put(value, key));
return result;
}Generic interfaces
public interface Repository<T, ID> {
Optional<T> findById(ID id);
T save(T entity);
void deleteById(ID id);
}
public class NoteRepository implements Repository<Note, Long> {
@Override public Optional<Note> findById(Long id) { return Optional.empty(); }
@Override public Note save(Note entity) { return entity; }
@Override public void deleteById(Long id) { }
}Fixing the parameters when implementing gives concrete, checked method signatures for free.
Generics are not covariant
List<String> texts = new ArrayList<>();
// List<Object> objects = texts; // compile error, and deliberately so
Object[] array = new String[3]; // arrays ARE covariant
array[0] = 42; // compiles, throws ArrayStoreExceptionAList<String>is not aList<Object>, even thoughStringis anObject. If it were, anything could be added through the wider reference. Arrays allow it and pay for the mistake at runtime; generics refuse at compile time. Wildcards exist to restore the flexibility safely.
A practical example
public class Cache<K, V> {
private final Map<K, V> entries = new LinkedHashMap<>();
private final int capacity;
public Cache(int capacity) {
this.capacity = capacity;
}
public V get(K key, Function<K, V> loader) {
V existing = entries.get(key);
if (existing != null) {
return existing;
}
V loaded = loader.apply(key);
if (entries.size() >= capacity) {
Iterator<K> it = entries.keySet().iterator();
it.next();
it.remove();
}
entries.put(key, loaded);
return loaded;
}
}Cache<Long, Note> notes = new Cache<>(100);
Note note = notes.get(42L, id -> repository.load(id));What cannot be generic
- A static field cannot use a class type parameter, because it is shared by every parameterisation.
new T()is not allowed; the type is not available at runtime.new T[10]is not allowed either.- A generic class cannot extend
Throwable, so exceptions cannot be generic. - A primitive cannot be a type argument; use the wrapper.
All of these follow from type erasure, which is covered in its own note.
Common mistakes
- Using raw types and losing every compile time check.
- Expecting
List<String>to be assignable toList<Object>. - Writing
List<int>instead ofList<Integer>. - Repeating the type argument on both sides instead of using the diamond.
- Adding a type parameter that appears only once in a signature, where a wildcard would be clearer.
Best practices
- Never use raw types in new code.
- Use the diamond on the right hand side.
- Name parameters with the conventional single letters.
- Let type inference do the work; specify explicitly only when the compiler cannot.
- Prefer generic methods over generic classes when only one method needs the parameter.
Practice
- Write a generic
Stack<E>withpush,popandisEmpty. - Explain why
List<Object> o = new ArrayList<String>();is rejected, with an example of what it would allow. - Write a generic method returning the first element of any list, or an empty
Optional. - Why can a generic class not declare
private static T shared;? - Convert a class that stores
Objectand casts on retrieval into a generic one.
Conclusion
Generics move type errors from runtime to compile time and remove the casts that used to hide them. Parameterise the class or the method, avoid raw types entirely, and remember that generic types are not covariant.