What are Generics in Java
The main idea: you write code once, and it works with any type.
🟢 Junior Level
Generics are a mechanism in Java that allows creating classes, interfaces, and methods that work with different types while maintaining type safety.
The main idea: you write code once, and it works with any type.
// Without generics (before Java 5)
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0); // cast needed!
// With generics (Java 5+)
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0); // no cast!
Why they are needed:
- Type safety — error is caught at compile time
- No cast needed — code is cleaner and safer
- Reusability — one code for different types
Example:
// One class works with any type
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
Box<Integer> intBox = new Box<>();
intBox.set(42);
🟡 Middle Level
How it works
Type parameter (<T>) is a placeholder for a type that is specified at usage:
// Declaration
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// Usage
Box<String> box = new Box<>(); // T = String
box.set("Hello");
String s = box.get(); // automatically String, no cast
Multiple parameters:
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
Pair<String, Integer> pair = new Pair<>("age", 25);
Bounded Type Parameters
You can constrain the type:
// Only Number and its subclasses
public class NumberBox<T extends Number> {
private T value;
public double doubleValue() { return value.doubleValue(); }
}
NumberBox<Integer> intBox = new NumberBox<>(); // ✅
NumberBox<String> strBox = new NumberBox<>(); // ❌ error
Common Mistakes
- Using raw types: ```java // ❌ Raw type — no type safety List list = new ArrayList(); list.add(“Hello”); list.add(42); // compiler doesn’t complain, but this is a bug!
// ✅ Parameterized type
List
2. **Expecting primitives to work:**
```java
// ❌ Primitives cannot be used as type parameters
List<int> list = new ArrayList<>(); // compilation error
// ✅ Use wrapper classes
List<Integer> list = new ArrayList<>();
Practical Application
1. Generic methods:
public static <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
List<String> strings = List.of("a", "b", "c");
String first = getFirst(strings); // T is inferred as String
2. Generic interfaces:
public interface Repository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
}
public class UserRepository implements Repository<User, Long> {
public Optional<User> findById(Long id) { /* ... */ }
}
🔴 Senior Level
Internal Implementation
Type Erasure:
// Source code
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// After compilation (type erasure)
public class Box {
private Object value; // T -> Object (upper bound)
public void set(Object value) { this.value = value; }
public Object get() { return value; }
}
// Compiler adds cast at usage
Box<String> box = new Box<>();
box.set("Hello");
String s = (String) box.get(); // implicit cast
Bounded type erasure:
public class NumberBox<T extends Number> {
private T value;
}
// After erasure: T -> Number (first bound)
public class NumberBox {
private Number value;
}
Architectural Trade-offs
Type Erasure:
| Pros | Cons |
|---|---|
| Binary compatibility (Java 5 code works with Java 1.4) | Cannot create new T() |
| No runtime overhead | Cannot check obj instanceof T |
Problems with arrays (new T[]) |
|
| Bridge methods needed to preserve polymorphism |
// Bridge Method — a synthetic method that the compiler adds // to preserve polymorphism after type erasure.
Edge Cases
1. Generic array creation:
// ❌ Cannot create array of generic type
public class Box<T> {
private T[] array = new T[10]; // compilation error
}
// ✅ Solution — Object array with cast
public class Box<T> {
private T[] array = (T[]) new Object[10];
// ⚠️ Dangerous: if you return this array externally, caller may get ClassCastException.
// ArrayList uses Object[] internally, but controls all writes — this is safe.
// Your code — not always.
}
2. Instanceof with generics:
// ❌ Cannot do
if (obj instanceof Box<String>) { } // compilation error
// ✅ Can — raw type or wildcard
if (obj instanceof Box<?>) { } // OK
3. Static context:
public class Box<T> {
// ❌ Static field cannot use T
private static T value; // compilation error
// ✅ Generic method — its own T
public static <E> void process(E item) { }
}
Performance
Type erasure overhead:
- Runtime: Zero overhead (no additional checks)
- Memory: No extra data
- Cast: Implicit casts on access (negligible)
Box<Integer> box = new Box<>();
box.set(42); // autoboxing int -> Integer
Integer val = box.get(); // cast + unboxing
Total overhead: autoboxing + cast (~1-2 ns)
Production Experience
Generic Repository:
public interface BaseEntity {
Long getId();
}
public interface Repository<T extends BaseEntity, ID extends Serializable> {
Optional<T> findById(ID id);
List<T> findAll(Pageable pageable);
<S extends T> S save(S entity);
long count();
boolean existsById(ID id);
}
public abstract class JpaRepository<T extends BaseEntity, ID extends Serializable>
implements Repository<T, ID> {
private final Class<T> entityType;
private final EntityManager em;
protected JpaRepository(Class<T> entityType, EntityManager em) {
this.entityType = entityType;
this.em = em;
}
public Optional<T> findById(ID id) {
return Optional.ofNullable(em.find(entityType, id));
}
}
Best Practices
// ✅ Use generics for collections
List<User> users = new ArrayList<>();
// ✅ Generic methods for reusability
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
return list.stream().filter(predicate).toList();
}
// ✅ Bounded types for constraints
public static <T extends Comparable<T>> T max(List<T> list) {
return list.stream().max(Comparable::compareTo).orElse(null);
}
// ❌ Raw types
// ❌ new T[] or new T()
// ❌ instanceof T
🎯 Interview Cheat Sheet
Must know:
- Generics appeared in Java 5 (JEP 14) for type safety when working with collections
- Type parameters:
<T>,<K, V>,<E>— placeholders for types - Bounded type parameters:
<T extends Number>— type constraint - Type erasure — compiler removes type information, T -> Object or bound
- Raw types — usage without type argument, disables type checking
- Primitives cannot be used:
List<int>— error, needList<Integer>
Common follow-up questions:
- Why are generics needed? — Compile-time type safety, elimination of casts, code reuse
- What is type erasure? — After compilation T is replaced with Object or bound, JVM doesn’t see generics
- Can you instanceof T? — No,
obj instanceof T— compilation error due to type erasure - **Why doesn’t List
inherit from List
Red flags (DO NOT say):
- ❌ “Generics work at runtime” — Type erasure removes types at compilation
- ❌ “List
is a subtype of List - ❌ “You can create new T()” — Type erasure doesn’t allow knowing T’s constructor
- ❌ “Primitive types can be used as parameters” — Only wrapper classes (Integer, Double)
Related topics:
- [[12. What are the advantages of using Generics]]
- [[13. What is type erasure]]
- [[15. What are bounded type parameters]]
- [[18. Can you use primitive types as generic parameters]]