Free lesson · Java Basic To Advance
Introduction to Generics
Type Safety Without the Boilerplate
Brandon Carter, opening Module 15. Generics were added to Java in 2004 (Java 5) and changed the language fundamentally. Before generics, collections held Object — every element required a cast, type errors weren't caught until runtime, and code was full of defensive casting. After generics, collections are parameterized by type — the compiler enforces correctness, no casts needed, errors caught at compile time.
This module covers generics from the ground up: what they are, how to declare and use them, bounded types and wildcards, the canonical PECS rule, and Nikhil's deep dive on type erasure (the JVM-level reality behind generics).
Life Before Generics
Pre-generics Java code looked like this:
// Java 1.4 - no generics
List names = new ArrayList();
names.add("Alice");
names.add("Bob");
names.add(42); // OOPS - int instead of String, compiles fine
// Reading requires cast
String first = (String) names.get(0); // OK
String oops = (String) names.get(2); // ClassCastException at runtime!
The problems:
- No type safety. You could put anything in a collection. The compiler didn't enforce a consistent type.
- Casts everywhere. Every read from a collection required
(String),(User), etc. - Runtime errors. Type mismatches blew up at runtime instead of compile time.
- Documentation by comment. "This list holds Strings" had to be in a comment because the type system didn't capture it.
Production code from this era was littered with casts, and ClassCastException was a common failure mode.
Life With Generics
The same code with generics:
// Java 5+ - with generics
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add(42); // COMPILE ERROR - int is not String
// Reading - no cast needed
String first = names.get(0); // type is String, no cast
The benefits:
- Compile-time type safety. The compiler catches type mismatches.
- No casts. Reading from a typed collection returns the right type.
- Self-documenting.
List<String>makes the intent clear. - IDE support. Autocomplete works because the IDE knows the element type.
This is what made Java much safer and more pleasant to work with. The same concept applies beyond collections — any class can have type parameters.
The Diamond Operator
Java 7 added the diamond operator <> for type inference:
// Java 5-6
List<String> names = new ArrayList<String>();
// Java 7+
List<String> names = new ArrayList<>(); // compiler infers <String>
The right side uses <> and the compiler infers the type from the left side. Less typing, same safety. Almost all modern Java code uses the diamond operator.
For local variables (Java 10+), var can infer the whole type:
var names = new ArrayList<String>(); // type: ArrayList<String>
But for fields and method parameters, you still write the explicit type.
Type Parameters
A type parameter is a placeholder for a type that gets filled in when the class or method is used:
// T is the type parameter
public class Box<T> {
private T contents;
public void put(T item) { this.contents = item; }
public T get() { return contents; }
}
// Usage - specify what T is
Box<String> stringBox = new Box<>();
stringBox.put("hello");
String s = stringBox.get();
Box<Integer> intBox = new Box<>();
intBox.put(42);
Integer i = intBox.get();
The class Box is generic over a type T. When you create a Box<String>, every T in the class becomes String for that instance. When you create a Box<Integer>, every T becomes Integer.
This is parametric polymorphism — the same code works for many types without rewriting.
Common Type Parameter Names
By convention, type parameters use single uppercase letters:
| Letter | Meaning | Example |
|---|---|---|
| T | Type | Box<T> — generic container |
| E | Element | List<E> — collection element |
| K | Key | Map<K, V> — map key |
| V | Value | Map<K, V> — map value |
| R | Result | Function<T, R> — function return |
| N | Number | when type is numeric |
| S, U | Second, third type parameters | when multiple types needed |
You can use longer names when single letters are unclear:
public class Cache<KeyType, ValueType> { ... }
But conventions exist for a reason — most Java code uses the single-letter conventions, and reading other code is easier when you follow them.
Multiple Type Parameters
A class or method can have multiple type parameters:
public class Pair<F, S> {
private final F first;
private final S second;
public Pair(F first, S second) {
this.first = first;
this.second = second;
}
public F getFirst() { return first; }
public S getSecond() { return second; }
}
// Usage
Pair<String, Integer> nameAge = new Pair<>("Alice", 30);
String name = nameAge.getFirst();
Integer age = nameAge.getSecond();
Map<K, V> is the most common example — two parameters for key and value types.
Generic Methods
Methods can have their own type parameters, independent of any class generics:
public class Util {
public static <T> T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
}
// Usage - T is inferred
String s = Util.firstOrNull(List.of("a", "b")); // T = String
Integer i = Util.firstOrNull(List.of(1, 2)); // T = Integer
The <T> before the return type declares T as a method-local type parameter. The compiler infers what T is each time you call the method based on the arguments.
We cover generic methods in detail in Topic 15.3.
What Generics Cost
Generics are a compile-time feature — they don't add runtime cost. The compiler erases type parameters after type-checking and emits regular bytecode. Nikhil covers this in detail in Topic 15.10 (Type Erasure).
The practical implications:
- No performance penalty
- No bytecode bloat
- BUT: some runtime limitations (can't get the generic type at runtime, etc.)
We'll see these limitations throughout the module. They're worth knowing about, especially for advanced patterns.
A Real Production Pattern
In AWS code, generics make APIs clean and safe:
public class S3Cache<T> {
private final Map<String, T> cache = new ConcurrentHashMap<>();
public T get(String key) {
return cache.get(key);
}
public void put(String key, T value) {
cache.put(key, value);
}
}
// Usage - one class, many types
S3Cache<BucketMetadata> bucketCache = new S3Cache<>();
S3Cache<UserPermissions> permCache = new S3Cache<>();
S3Cache<List<S3Object>> listCache = new S3Cache<>();
Before generics, this would either be three separate classes (BucketCache, PermissionCache, ListCache) or one class returning Object with casts everywhere. Generics give you both: one implementation, type-safe usage.
This pattern — generic container/cache/wrapper — appears constantly.
A Misconception to Drop
A common confusion: thinking generics are like C++ templates.
C++ templates generate separate code for each type. vector<int> and vector<string> produce different compiled code. The compiler effectively duplicates the template body for each instantiation.
Java generics do NOT do this. The bytecode is the same regardless of type parameters. There's only ONE compiled version of ArrayList, regardless of how many ArrayList<String>, ArrayList<Integer>, etc. you use. The type parameters disappear after compilation (erasure — Topic 15.10).
This has consequences:
- No runtime type information (
new T()doesn't work) - No instanceof checks against parameterized types
- Same
ArrayListclass at runtime regardless of parameter
These are the limitations we'll cover throughout the module.
graph TD
A["Generics"] --> B["Compile-time only"]
A --> C["Type safety"]
A --> D["No runtime cost"]
E["Benefits"] --> F["Compile-time errors instead of runtime"]
E --> G["No casts needed"]
E --> H["Self-documenting types"]
E --> I["Better IDE support"]
J["Type parameter conventions"] --> K["T - generic Type"]
J --> L["E - collection Element"]
J --> M["K, V - map Key, Value"]
J --> N["R - function Result"]
style A fill:#7c3aed,color:#fff
This diagram shows what Generics provide and the conventions for using them. Generics are a compile-time feature — they exist in source code and during type-checking but get erased before bytecode generation. They provide type safety without runtime cost. The benefits: compile-time errors instead of runtime ClassCastException, no casts needed when reading from typed collections, self-documenting types in code, and better IDE autocomplete and refactoring. Type parameter naming conventions: T for a generic Type, E for a collection Element, K and V for Map Key and Value, R for a function Result. Following these conventions makes Java code easier to read across the ecosystem.
Common Pitfalls
Warning: Using raw types.
Listinstead ofList<String>defeats the purpose. Bypass type-checking.Using Object instead of a generic type. When you need flexibility, generics are better than Object — type safety is preserved.
Over-parameterizing. Not every class needs to be generic. Use generics when you have multiple types served by the same code.
Confusing generic syntax in declarations vs usage. Class declaration uses
<T>to introduce; usage uses<String>to fill in.Forgetting the diamond operator. Writing
new ArrayList<String>()instead ofnew ArrayList<>()works but is more verbose.
Why This Matters in Interviews
Interview: Generics questions appear in every Java interview at the mid-level and beyond. "Why do generics exist?" is the entry-level version. Deeper questions cover wildcards, PECS, type erasure (Topic 15.10), and the limitations that erasure causes.
Topic 15.12 covers the Generics Interview Lens with Nikhil leading.
Try It
Exercise: Explore each:
- Write a generic
Box<T>class. Create instances for String, Integer, your own class.- Try to put a wrong type in a
Box<String>. Observe the compile error.- Write a method
firstOrNullthat's generic. Call it with different list types.- Try the raw type form:
List list = new ArrayList(). Note the compiler warning.- Compare reading from
List<String>(no cast) vsList(cast required).- Create a
Pair<F, S>with two type parameters. Use it with different combinations.Each exercise builds Generics intuition.