RU | EN | DE

Why Generics

  • Type safety at compile time (less ClassCastException).
  • Code reuse for different types.
  • Self-documenting API: Map<String, Integer> is clearer than Map.

Basic Syntax: <T>, <E>, <K, V>

Class/Interface with Type Parameter

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; }
}
Box<String> sb = new Box<>("hi");
String s = sb.get();  // safe, no casts

Multiple Parameters

public class Pair<K, V> {
  private final K key; private final V value;
  public Pair(K key, V value) { this.key = key; this.value = value; }
  public K key() { return key; } public V value() { return value; }
}

Generic Methods

public static <T> T first(List<T> list) {
  return list.isEmpty() ? null : list.get(0);
}

Bounds

public class NumberBox<T extends Number> { /* ... */ }
// several bounds:
public static <T extends Number & Comparable<T>> T max(T a, T b) {
  return a.compareTo(b) >= 0 ? a : b;
}

Wildcards: ?, ? extends, ? super + PECS principle

Invariance: The key to understanding

List<Integer> is not a List<Number>, nor a subtype, nor a supertype.
Arrays are covariant (Number[] arr = new Integer[10]), generics are invariant.

Wildcard ?

List<?> is a list of unknown type: you can read it as Object, but you cannot add anything (except null).

Upper Bound: ? extends T (“producer”)

void printNums(List<? extends Number> src) {
  // can read as Number
  Number n = src.get(0);
  // src.add(42); // cannot - type is unknown
}

Lower Bound: ? super T (“consumer”)

void addIntegers(List<? super Integer> dst) {
  dst.add(1);  // can add Integer (and its subtypes)
  // Integer x = dst.get(0); // returns Object, unsafe to read
}

The Golden Rule PECS

  • Producer Extends: if you read (source), use ? extends.
  • Consumer Super: if you write (consumer), use ? super.
Classic Example: Collections.copy
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }

Source – extends, consumer – super.

Frequently Asked Questions and Pitfalls

Why List<Object>List<String>?

Invariance: otherwise, you could insert an Integer into List<String> via a reference to List<Object>.
Use List<?> if you need “any list for read-only”.

What is the difference between List<? extends Number> and List<Number>?

  • In List<Number>, you can add Integer, Long, Double, etc.
  • In List<? extends Number>, you cannot add (the specific type is unknown), but you can safely read as Number.

What is List<? super Integer> useful for?

It’s a “bucket” for writing Integer objects. You can only read from it as Object.

Wildcard Capture

Sometimes you need a “helper” to work with ?:

static void swapFirst(List<?> list) { swapHelper(list); }
private static <T> void swapHelper(List<T> list) {
  if (list.size() > 1) {
  T tmp = list.get(0);
  list.set(0, list.get(1));
  list.set(1, tmp);
  }
}

Type Inference

“Diamond” (diamond operator) with Java 7+

Map<String, List<Integer>> m = new HashMap<>();

Inference in Generic Methods

var x = List.of(1, 2, 3);  // List<Integer>
var y = first(List.of("a"));  // String

Sometimes explicitly specify <T> in the call: MyClass.<Long>first(...).

Type Erasure

What it is: Type parameters exist only at compile time.
JVM sees “raw” types: List<T>List. From here the restrictions:

Consequences of Erasure

  1. Cannot create an array of parameterized type:
List<String>[] arr = new List<String>[10]; // error
  1. Cannot create new T() and T.class:
T t = new T();  // error
Class<T> c = T.class; // error

Workaround: pass Class<T> to the constructor/method:

class Factory<T> {
  private final Class<T> type;
  Factory(Class<T> type) { this.type = type; }
  T create() throws Exception { return type.getDeclaredConstructor().newInstance(); }
}
  1. Cannot overload methods differing only in type parameters:
void foo(List<String> x) {}
void foo(List<Integer> y) {} // same erasure → conflict
  1. instanceof only with wildcards or raw types:
if (obj instanceof List<?>) { /* ok */ }
// if (obj instanceof List<String>) // compile error
  1. Bridge methods: The compiler generates them to preserve polymorphism during erasure (often visible in bytecode/debug).
  2. Reifiable vs non-reifiable:
  • Reifiable (preserve info at runtime): List<?>, List, primitive arrays.
  • Non-reifiable: List<String>, Map<Integer, String> – the type parameter is erased.

Type token / preserving type

They use a “type holder”:

abstract class TypeRef<T> {
  final Type type = ((ParameterizedType) getClass()
  getGenericSuperclass()).getActualTypeArguments()[0];
}
TypeRef<List<String>> ref = new TypeRef<>() {};
System.out.println(ref.type); // java.util.List<java.lang.String>

Generics and Arrays/Varargs

  • Arrays are covariant and checked at runtime; generics are invariant and checked at compile time.
  • Varargs with generics lead to a heap pollution warning.
    Mark the method @SafeVarargs (only static/final/private) and don’t mix internals:
@SafeVarargs
public static <T> List<T> asList(T... items) { return Arrays.asList(items); }

Practical Signatures for Interviews

Maximum on Comparator

public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
  if (list.isEmpty()) return null;
  T best = list.get(0);
  for (T t : list) if (t.compareTo(best) > 0) best = t;
  return best;
}

Copying from source to consumer (PECS)

public static <T> void copyAll(List<? super T> dest, List<? extends T> src) {
  for (T t : src) dest.add(t);
}

Generalized repository in Spring style

public interface Repository<T extends BaseEntity, ID> {
  T save(T entity);
  Optional<T> findById(ID id);
}

Typical Errors

  • ❌ Raw types (List list) → lose safety, ClassCastException arises.
  • ❌ Returning List<? extends T> from an API method. It’s better to return List<T>; wildcards are in arguments, not return types.
  • ❌ Overusing ? extends where you should write. Remember PECS.
  • ❌ Trying to overload methods differing only in type parameters (erasure will break).
  • List<Object> instead of List<?> for “any read-only collection”.

Best practices

  • Follow PECS: extends – read, super – write.
  • In public APIs use wildcards in parameters, not in return types.
  • Avoid raw types; if you have to, isolate and accompany @SuppressWarnings("unchecked") with a comment.
  • Give meaningful names to type parameters: T, E, K, V, ID, R, U.
  • For factories/reflection, take Class<T> or Type (type token), don’t invent new T().

Mini-cheat sheet of differences (for memory)

  • List<?> – read as Object, don’t write.
  • List<? extends T> – read as T, don’t write.
  • List<? super T> – write T, read as Object.
  • Generics are invariant; arrays are covariant.
  • Type erasure: no new T(), instanceof List<String> and overloads “only by generics”.