Type-safe reusable code: "If it compiles, the types are correct"
// Java before generics - everything was Object List names = new ArrayList(); names.add("Alice"); names.add("Bob"); names.add(42); // Compiles! But wrong. // You had to cast on every retrieval String first = (String) names.get(0); // Cast required String oops = (String) names.get(2); // ClassCastException at RUNTIME! // The compiler couldn't help you - errors discovered in production
// With generics - type-safe collections List<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob"); names.add(42); // COMPILE ERROR: incompatible types // No casts needed - compiler knows the type String first = names.get(0); // No cast! // Error caught at compile time, not in production at 3am
Generics move type checking from runtime (crashes in production) to compile time (IDE shows error immediately). This is a fundamental safety improvement for large systems.
from typing import Generic, TypeVar T = TypeVar('T') class Box(Generic[T]): def __init__(self, value: T): self.value = value def get(self) -> T: return self.value # Type hints are optional, not enforced at runtime box: Box[str] = Box("hello") box.get() # "hello" # This "works" at runtime even though types are wrong box: Box[str] = Box(123) # Type checker warns, but runs
/** * A generic container class. * T is a type parameter - a placeholder for a real type. */ public class Box<T> { private T value; public Box(T value) { this.value = value; } public T get() { return value; } public void set(T value) { this.value = value; } } // Usage - compiler enforces types Box<String> stringBox = new Box<>("hello"); String s = stringBox.get(); // No cast needed Box<Integer> intBox = new Box<>(42); Integer i = intBox.get(); // Type mismatch caught at compile time Box<String> box = new Box<>(123); // COMPILE ERROR
public class Utilities { // Generic method - T is declared before return type public static <T> T firstOrNull(List<T> list) { return list.isEmpty() ? null : list.get(0); } // Multiple type parameters public static <K, V> Map<V, K> invert(Map<K, V> map) { Map<V, K> inverted = new HashMap<>(); for (Map.Entry<K, V> entry : map.entrySet()) { inverted.put(entry.getValue(), entry.getKey()); } return inverted; } // Generic method in non-generic class public static <T> List<T> repeat(T item, int times) { List<T> result = new ArrayList<>(); for (int i = 0; i < times; i++) { result.add(item); } return result; } } // Type inference - compiler figures out T String first = Utilities.firstOrNull(List.of("a", "b")); // T = String Integer num = Utilities.firstOrNull(List.of(1, 2, 3)); // T = Integer // Explicit type (rarely needed) String s = Utilities.<String>firstOrNull(myList);
Constrain what types can be used with your generic:
// T must be a Number or subclass (Integer, Double, etc.) public class Statistics<T extends Number> { private final List<T> values; public Statistics(List<T> values) { this.values = values; } public double average() { // We can call doubleValue() because T extends Number return values.stream() .mapToDouble(Number::doubleValue) // This works! .average() .orElse(0); } } // Usage new Statistics<>(List.of(1, 2, 3)); // OK: Integer extends Number new Statistics<>(List.of(1.5, 2.5)); // OK: Double extends Number new Statistics<>(List.of("a", "b")); // COMPILE ERROR: String not Number // Multiple bounds public <T extends Comparable<T> & Serializable> T max(T a, T b) { return a.compareTo(b) > 0 ? a : b; } // T must implement BOTH Comparable AND Serializable
This is where generics get tricky. There are three types of wildcards:
// ? means "any type" - use when you don't care about the specific type public void printAll(List<?> items) { for (Object item : items) { System.out.println(item); } } // Works with any List printAll(List.of("a", "b")); printAll(List.of(1, 2, 3)); printAll(List.of(new User(), new User())); // But you can't add to a List> (except null) public void addItem(List<?> items) { items.add("hello"); // COMPILE ERROR // Why? The list might be List<Integer>! }
// extends T> = "T or any subtype of T" // Use when you READ from a generic structure (producer) public double sumAll(List<? extends Number> numbers) { double sum = 0; for (Number n : numbers) { // Safe: we know it's at least Number sum += n.doubleValue(); } return sum; } // Works with List<Integer>, List<Double>, List<Number> sumAll(List.of(1, 2, 3)); // Integer extends Number sumAll(List.of(1.5, 2.5)); // Double extends Number // Still can't add (might be List<Integer>, can't add Double) public void addNumber(List<? extends Number> numbers) { numbers.add(1.0); // COMPILE ERROR }
// super T> = "T or any supertype of T" // Use when you WRITE to a generic structure (consumer) public void addIntegers(List<? super Integer> list) { list.add(1); // Safe: can always add Integer list.add(2); list.add(3); } // Works with List<Integer>, List<Number>, List<Object> List<Number> numbers = new ArrayList<>(); addIntegers(numbers); // OK: Number is supertype of Integer List<Object> objects = new ArrayList<>(); addIntegers(objects); // OK: Object is supertype of Integer // But reading is limited - you only know it's Object public void process(List<? super Integer> list) { Object item = list.get(0); // Only know it's Object Integer i = list.get(0); // COMPILE ERROR }
<? extends T><? super T><T>This is one of the most commonly asked generics interview questions.
// From the actual Java Collections class public static <T> void copy( List<? super T> dest, // Consumer: we WRITE to dest List<? extends T> src // Producer: we READ from src ) { for (T item : src) { dest.add(item); } } // This allows maximum flexibility: List<Number> numbers = new ArrayList<>(); List<Integer> integers = List.of(1, 2, 3); Collections.copy(numbers, integers); // OK! // - dest is List<Number>, Number is super of Integer ✓ // - src is List<Integer>, Integer extends Integer ✓ // Without wildcards, this wouldn't compile: public static <T> void badCopy(List<T> dest, List<T> src) { ... } badCopy(numbers, integers); // COMPILE ERROR: List<Number> != List<Integer>
Java generics are implemented via "type erasure" - generic type info is removed at runtime. This was for backwards compatibility with pre-generics code.
// What you write: List<String> strings = new ArrayList<String>(); strings.add("hello"); String s = strings.get(0); // What the compiler sees after erasure: List strings = new ArrayList(); strings.add("hello"); String s = (String) strings.get(0); // Cast inserted by compiler // Consequences: // 1. Can't create generic arrays T[] array = new T[10]; // COMPILE ERROR // 2. Can't use instanceof with generics if (obj instanceof List<String>) { } // COMPILE ERROR if (obj instanceof List<?>) { } // OK (wildcard) // 3. Can't distinguish generic types at runtime List<String> strings = new ArrayList<>(); List<Integer> integers = new ArrayList<>(); strings.getClass() == integers.getClass(); // TRUE! Both are just ArrayList // 4. Static fields can't use type parameters public class Box<T> { private static T instance; // COMPILE ERROR }
When generics were added in Java 5, there was a decade of existing non-generic code. Type erasure allowed generic and non-generic code to interoperate. The tradeoff was losing type information at runtime.
Contrast with C#/.NET reified generics which preserve type info at runtime (but broke compatibility).
// Self-referential generics for fluent builders public abstract class Builder<T, B extends Builder<T, B>> { protected String name; @SuppressWarnings("unchecked") protected B self() { return (B) this; } public B name(String name) { this.name = name; return self(); } public abstract T build(); } // Subclass builder returns correct type public class UserBuilder extends Builder<User, UserBuilder> { private String email; public UserBuilder email(String email) { this.email = email; return this; } @Override public User build() { return new User(name, email); } } // Fluent API works correctly User user = new UserBuilder() .name("Alice") // Returns UserBuilder, not Builder .email("a@b.com") // So this compiles! .build();
// Store different types with type safety (no casts needed) public class TypeSafeMap { private final Map<Class<?>, Object> map = new HashMap<>(); public <T> void put(Class<T> type, T instance) { map.put(type, instance); } public <T> T get(Class<T> type) { return type.cast(map.get(type)); // Safe cast via Class.cast() } } // Usage - type-safe, no casts needed TypeSafeMap context = new TypeSafeMap(); context.put(String.class, "hello"); context.put(Integer.class, 42); context.put(User.class, new User("Alice")); String s = context.get(String.class); // "hello" - no cast! Integer i = context.get(Integer.class); // 42 - no cast! User u = context.get(User.class); // User - no cast! // Used in many frameworks (Spring, Guice, etc.)
| Question | Answer |
|---|---|
| What is type erasure? | Generic type info is removed at compile time. List<String> becomes List at runtime. |
Why can't you create new T()? |
Type erasure - T is unknown at runtime. Use factory pattern or Class<T> parameter. |
Difference between <T> and <?>? |
T is a type parameter you can reference. ? is an unknown type you can't reference. |
| What is PECS? | Producer Extends, Consumer Super. Use extends when reading, super when writing. |
| Can you have generic exceptions? | No - exception types can't be generic due to erasure and catch semantics. |