AlgoInsight

Free lesson · Java Basic To Advance

Introduction to Generics

Medium7 min readjava

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:

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:

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:

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:

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. List instead of List<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 of new 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:

  1. Write a generic Box<T> class. Create instances for String, Integer, your own class.
  2. Try to put a wrong type in a Box<String>. Observe the compile error.
  3. Write a method firstOrNull that's generic. Call it with different list types.
  4. Try the raw type form: List list = new ArrayList(). Note the compiler warning.
  5. Compare reading from List<String> (no cast) vs List (cast required).
  6. Create a Pair<F, S> with two type parameters. Use it with different combinations.

Each exercise builds Generics intuition.

Back to Java Basic To Advance