What Is It?
What Are Generics in Java?
Generics allow you to write classes, interfaces, and methods that operate on a type parameter instead of a specific type. Instead of writing separate IntBox, StringBox, and DoubleBox classes, you write one Box<T> that works with any type.
// Without generics (before Java 5)
ArrayList list = new ArrayList();
list.add("Hello");
list.add(42); // No error at compile time!
String s = (String) list.get(1); // ClassCastException at runtime!
// With generics
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
// list.add(42); // Compilation error! Type safety enforced.
String s = list.get(0); // No cast needed.Generics were introduced in Java 5 to provide compile-time type safety and eliminate the need for explicit casting. The type parameter (like T) is a placeholder that gets replaced by a concrete type when the generic class or method is used.
// Generic class
class Box<T> {
private T value;
Box(T value) { this.value = value; }
T getValue() { return value; }
}
// Usage
Box<String> stringBox = new Box<>("Hello");
Box<Integer> intBox = new Box<>(42);
String s = stringBox.getValue(); // No cast
int n = intBox.getValue(); // No cast (auto-unboxing)
Why Does It Matter?
Why Do Generics Matter?
Before generics, Java collections held Object references, requiring unsafe casting at every retrieval. Generics transformed Java from a loosely typed collections world into a type-safe one.
1. Type Safety at Compile Time
Without generics, type errors surface at runtime as ClassCastException. With generics, the compiler catches type mismatches before the code runs. When Arjun writes ArrayList<Student>, the compiler guarantees that only Student objects go in. No surprises at runtime.
2. Elimination of Casting
Without generics: String s = (String) list.get(0);. With generics: String s = list.get(0);. The compiler knows the type, so no explicit cast is needed. This reduces boilerplate and potential errors.
3. Code Reusability
A single generic class or method works with all types. Kavya writes Pair<K, V> once and uses it as Pair<String, Integer>, Pair<Student, Grade>, or any other combination. One implementation, infinite type combinations.
4. Algorithm Abstraction
Generic methods allow writing type-independent algorithms. A max method works for any Comparable type without knowing whether it compares Integers, Strings, or custom objects.
5. Foundation of the Collections Framework
Every collection class (ArrayList<E>, HashMap<K,V>, HashSet<E>) uses generics. Understanding generics is essential to understanding collections.
6. Interview Importance
Generics, particularly wildcards and the PECS principle, are among the trickiest Java interview topics. Questions about type erasure, bounded types, and wildcard captures are common in placement interviews at product-based companies.
Detailed Explanation
Detailed Explanation
1. Generic Classes
A generic class declares one or more type parameters in angle brackets after the class name:
class Pair<K, V> {
private K key;
private V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
K getKey() { return key; }
V getValue() { return value; }
@Override
public String toString() {
return key + " = " + value;
}
}
// Usage
Pair<String, Integer> age = new Pair<>("Arjun", 20);
Pair<String, String> city = new Pair<>("Kavya", "Bangalore");
String name = age.getKey(); // No cast
int years = age.getValue(); // No cast (auto-unboxing)Common type parameter naming conventions: T (Type), E (Element), K (Key), V (Value), N (Number), S, U (additional types).
2. Generic Methods
A generic method declares its own type parameter(s), independent of the class's type parameters:
class Utility {
// Generic method: <T> before return type
static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
static <T> T getFirst(T[] array) {
return array[0];
}
}
// Usage: type is inferred from arguments
Integer[] ints = {1, 2, 3, 4, 5};
String[] strs = {"A", "B", "C"};
Utility.printArray(ints); // 1 2 3 4 5
Utility.printArray(strs); // A B C
Integer first = Utility.getFirst(ints); // 1
String firstStr = Utility.getFirst(strs); // A3. Bounded Type Parameters
You can restrict type parameters to certain types using extends:
// T must be a subclass of Number
class NumericBox<T extends Number> {
private T value;
NumericBox(T value) { this.value = value; }
double doubleValue() {
return value.doubleValue(); // Safe: Number has doubleValue()
}
}
NumericBox<Integer> intBox = new NumericBox<>(42);
NumericBox<Double> dblBox = new NumericBox<>(3.14);
// NumericBox<String> strBox = new NumericBox<>("Hi"); // Compilation error!4. Multiple Bounds
A type parameter can have multiple bounds using &:
// T must extend Number AND implement Comparable
class SortableBox<T extends Number & Comparable<T>> {
private T value;
SortableBox(T value) { this.value = value; }
boolean isGreaterThan(SortableBox<T> other) {
return this.value.compareTo(other.value) > 0;
}
}
SortableBox<Integer> a = new SortableBox<>(10);
SortableBox<Integer> b = new SortableBox<>(5);
System.out.println(a.isGreaterThan(b)); // trueWith multiple bounds, the class must come first, followed by interfaces: T extends ClassName & Interface1 & Interface2.
5. Wildcards
Wildcards (?) represent an unknown type. They are used in method parameters when you want flexibility:
Unbounded Wildcard: ?
// Accepts a list of any type
void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
}
printList(List.of(1, 2, 3)); // Works
printList(List.of("A", "B")); // Works
printList(List.of(new Student())); // WorksUpper Bounded Wildcard: ? extends T
// Accepts List of Number or any subclass of Number
double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> works
sum(List.of(1.5, 2.5, 3.5)); // List<Double> works
// sum(List.of("A", "B")); // List<String> fails (String is not Number)Lower Bounded Wildcard: ? super T
// Accepts List of Integer or any superclass of Integer
void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
List<Integer> intList = new ArrayList<>();
List<Number> numList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
addIntegers(intList); // Works
addIntegers(numList); // Works (Number is super of Integer)
addIntegers(objList); // Works (Object is super of Integer)6. PECS Principle (Producer Extends, Consumer Super)
This is the golden rule for deciding between extends and super:
- Producer Extends: If you only READ from the collection (it produces items), use
? extends T - Consumer Super: If you only WRITE to the collection (it consumes items), use
? super T
// Producer: reading from source (extends)
static <T> void copy(List<? extends T> source, List<? super T> dest) {
for (T item : source) { // Reading from source (producer)
dest.add(item); // Writing to dest (consumer)
}
}
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(ints, nums); // Copies Integer list into Number listThink of it this way: extends gives you a read-only view (you know you are getting at least T). super gives you a write-only view (you know you can put at least T).
7. Type Erasure
Generics are a compile-time feature. At runtime, all generic type information is erased (replaced by Object or the bound). This is called type erasure.
// At compile time
ArrayList<String> strings = new ArrayList<>();
ArrayList<Integer> ints = new ArrayList<>();
// At runtime (after erasure)
ArrayList strings = new ArrayList(); // Both become raw ArrayList
ArrayList ints = new ArrayList();
// They have the same class at runtime!
System.out.println(strings.getClass() == ints.getClass()); // trueConsequences of type erasure:
- Cannot use
new T()(type is erased at runtime) - Cannot use
instanceofwith generic types (obj instanceof List<String>is illegal) - Cannot create generic arrays (
new T[10]is illegal) - Cannot have static fields of generic type
8. Generic Interfaces
Interfaces can be generic:
interface Repository<T> {
void save(T entity);
T findById(int id);
List<T> findAll();
}
class StudentRepository implements Repository<Student> {
private List<Student> students = new ArrayList<>();
@Override
public void save(Student s) { students.add(s); }
@Override
public Student findById(int id) {
return students.stream()
.filter(s -> s.getId() == id)
.findFirst()
.orElse(null);
}
@Override
public List<Student> findAll() { return students; }
}9. Restrictions on Generics
- No primitive types:
Box<int>is illegal; useBox<Integer> - No
new T(): Cannot instantiate type parameter - No
new T[]: Cannot create generic arrays - No static fields of type T:
static T value;is illegal - No
instanceofwith parameterized types:obj instanceof List<String>is illegal (useobj instanceof List<?>) - Cannot extend
Throwable:class MyException<T> extends Exceptionis illegal
Code Examples
class Box<T> {
private T item;
Box(T item) {
this.item = item;
}
T getItem() {
return item;
}
void setItem(T item) {
this.item = item;
}
@Override
public String toString() {
return "Box[" + item + "]";
}
}
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>("Hello");
Box<Integer> intBox = new Box<>(42);
Box<Double> doubleBox = new Box<>(3.14);
System.out.println(stringBox);
System.out.println(intBox);
System.out.println(doubleBox);
// Type safety: compiler catches errors
String s = stringBox.getItem(); // No cast needed
int n = intBox.getItem(); // Auto-unboxing
System.out.println("String: " + s);
System.out.println("Int: " + n);
// stringBox.setItem(123); // Compilation error: type mismatch
}
}Box<T> can hold any type. When instantiated as Box<String>, only Strings are accepted. The compiler prevents type mismatches at compile time, and no casting is needed when retrieving values.public class Main {
// Generic method: <T> before return type
static <T> T getMiddle(T[] array) {
return array[array.length / 2];
}
static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
static <T> String arrayToString(T[] array) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < array.length; i++) {
if (i > 0) sb.append(", ");
sb.append(array[i]);
}
return sb.append("]").toString();
}
public static void main(String[] args) {
Integer[] ints = {10, 20, 30, 40, 50};
String[] strs = {"Arjun", "Kavya", "Ravi"};
System.out.println("Middle int: " + getMiddle(ints));
System.out.println("Middle str: " + getMiddle(strs));
System.out.println("Before swap: " + arrayToString(ints));
swap(ints, 0, 4);
System.out.println("After swap: " + arrayToString(ints));
System.out.println("Before swap: " + arrayToString(strs));
swap(strs, 0, 2);
System.out.println("After swap: " + arrayToString(strs));
}
}T from the arguments. getMiddle works with Integer[] and String[] without separate implementations. swap swaps elements in any array type.class MathBox<T extends Number> {
private T value;
MathBox(T value) {
this.value = value;
}
double sqrt() {
return Math.sqrt(value.doubleValue());
}
boolean isPositive() {
return value.doubleValue() > 0;
}
T getValue() {
return value;
}
}
public class Main {
// Bounded generic method
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
public static void main(String[] args) {
MathBox<Integer> intBox = new MathBox<>(16);
MathBox<Double> dblBox = new MathBox<>(2.25);
System.out.println("sqrt(16) = " + intBox.sqrt());
System.out.println("sqrt(2.25) = " + dblBox.sqrt());
System.out.println("16 positive? " + intBox.isPositive());
// MathBox<String> strBox = new MathBox<>("Hi"); // Error: String not Number
System.out.println("max(10, 20) = " + max(10, 20));
System.out.println("max(A, Z) = " + max("A", "Z"));
System.out.println("max(3.14, 2.71) = " + max(3.14, 2.71));
}
}T extends Number restricts T to Number and its subclasses. This allows calling Number methods like doubleValue(). The max method uses T extends Comparable<T> to ensure elements can be compared.import java.util.List;
import java.util.ArrayList;
public class Main {
// Unbounded: read-only, any type
static void printAll(List<?> list) {
for (Object item : list) {
System.out.print(item + " ");
}
System.out.println();
}
// Upper bounded: read Numbers (producer extends)
static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
// Lower bounded: write Integers (consumer super)
static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
public static void main(String[] args) {
// Unbounded
printAll(List.of("A", "B", "C"));
printAll(List.of(1, 2, 3));
// Upper bounded
System.out.println("Sum of ints: " + sum(List.of(1, 2, 3)));
System.out.println("Sum of doubles: " + sum(List.of(1.5, 2.5)));
// Lower bounded
List<Number> numList = new ArrayList<>();
addNumbers(numList);
System.out.println("Numbers added: " + numList);
List<Object> objList = new ArrayList<>();
addNumbers(objList);
System.out.println("Objects added: " + objList);
}
}List<?> accepts any list (read-only). List<? extends Number> reads from any Number subtype (producer). List<? super Integer> writes Integers to any supertype list (consumer). This demonstrates the PECS principle.import java.util.List;
import java.util.ArrayList;
public class Main {
// Copy from source (producer: extends) to dest (consumer: super)
static <T> void copy(List<? extends T> source, List<? super T> dest) {
for (T item : source) {
dest.add(item);
}
}
// Find max in a producer list
static <T extends Comparable<T>> T findMax(List<? extends T> list) {
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
public static void main(String[] args) {
List<Integer> ints = List.of(3, 1, 4, 1, 5);
List<Number> nums = new ArrayList<>();
copy(ints, nums); // Integer -> Number
System.out.println("Copied: " + nums);
System.out.println("Max int: " + findMax(ints));
System.out.println("Max string: " + findMax(List.of("Ravi", "Arjun", "Kavya")));
}
}copy method reads from source (producer, uses extends) and writes to dest (consumer, uses super). findMax reads from a producer list. PECS is the key to writing flexible, type-safe generic methods.import java.util.ArrayList;
import java.util.List;
interface Repository<T> {
void save(T entity);
T findById(int id);
List<T> findAll();
int count();
}
class Student {
int id;
String name;
Student(int id, String name) { this.id = id; this.name = name; }
@Override
public String toString() { return "Student{" + id + ", " + name + "}"; }
}
class StudentRepository implements Repository<Student> {
private List<Student> store = new ArrayList<>();
@Override
public void save(Student s) {
store.add(s);
System.out.println("Saved: " + s);
}
@Override
public Student findById(int id) {
for (Student s : store) {
if (s.id == id) return s;
}
return null;
}
@Override
public List<Student> findAll() { return store; }
@Override
public int count() { return store.size(); }
}
public class Main {
public static void main(String[] args) {
Repository<Student> repo = new StudentRepository();
repo.save(new Student(1, "Arjun"));
repo.save(new Student(2, "Kavya"));
repo.save(new Student(3, "Ravi"));
System.out.println("Find 2: " + repo.findById(2));
System.out.println("Count: " + repo.count());
System.out.println("All: " + repo.findAll());
}
}Repository<T> interface defines a generic CRUD contract. StudentRepository implements it with Student as the type parameter. The same interface could be implemented for Course, Teacher, or any entity.import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
ArrayList<Integer> integers = new ArrayList<>();
// At runtime, both are just ArrayList (type erasure)
System.out.println(strings.getClass().getName());
System.out.println(integers.getClass().getName());
System.out.println(strings.getClass() == integers.getClass());
// Cannot use instanceof with parameterized types
List<?> list = new ArrayList<String>();
System.out.println(list instanceof List); // OK
System.out.println(list instanceof List<?>); // OK
// System.out.println(list instanceof List<String>); // Compilation error!
// Generics provide safety at compile time, not runtime
List rawList = strings; // Raw type (no generics)
rawList.add(42); // No compile error with raw type!
// String s = strings.get(0); // ClassCastException at runtime!
System.out.println("Raw type bypasses generic safety");
}
}ArrayList<String> and ArrayList<Integer> are both just ArrayList (type parameters are erased). This is why you cannot use instanceof with parameterized types. Raw types bypass generic safety but risk runtime errors.Common Mistakes
Using Primitive Types as Type Parameters
ArrayList<int> numbers = new ArrayList<>(); // Compilation error!
Box<double> box = new Box<>(3.14); // Compilation error!ArrayList<Integer> numbers = new ArrayList<>(); // Use wrapper class
Box<Double> box = new Box<>(3.14); // Autoboxing handles conversionInteger for int, Double for double, Boolean for boolean, etc. Java autoboxes and unboxes automatically.Trying to Instantiate a Type Parameter
class Factory<T> {
T create() {
return new T(); // Compilation error!
}
}import java.util.function.Supplier;
class Factory<T> {
private Supplier<T> constructor;
Factory(Supplier<T> constructor) {
this.constructor = constructor;
}
T create() {
return constructor.get();
}
}
// Usage: pass constructor reference
Factory<ArrayList> factory = new Factory<>(ArrayList::new);
ArrayList list = factory.create();T is at runtime, so new T() is impossible. The workaround is to pass a Supplier<T> (a factory function) that creates the object.Adding to a List<? extends T> (Producer Is Read-Only)
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(42); // Compilation error!// Use ? extends for READING (producer)
List<? extends Number> producer = List.of(1, 2, 3);
Number n = producer.get(0); // Reading is safe
// Use ? super for WRITING (consumer)
List<? super Integer> consumer = new ArrayList<Number>();
consumer.add(42); // Writing is safeList<? extends Number> could be a List<Integer>, List<Double>, or List<Number>. The compiler cannot guarantee that adding an Integer is safe (what if it is a List<Double>?). PECS: Producer Extends (read-only), Consumer Super (write-only).Comparing Generic Types with == (Type Erasure Confusion)
ArrayList<String> a = new ArrayList<>();
ArrayList<Integer> b = new ArrayList<>();
// This is TRUE due to type erasure!
if (a.getClass() == b.getClass()) {
System.out.println("Same class at runtime!");
}
// This does NOT compile
// if (a instanceof ArrayList<String>) { }// Use instanceof with unbounded wildcard
Object obj = new ArrayList<String>();
if (obj instanceof ArrayList<?>) {
System.out.println("It is an ArrayList");
}
// Cannot determine if it is ArrayList<String> vs ArrayList<Integer> at runtimeArrayList<String> and ArrayList<Integer> are the same class at runtime. You cannot use instanceof with parameterized types. Use instanceof ArrayList<?> (unbounded wildcard) instead.Creating Generic Arrays
class Container<T> {
T[] items = new T[10]; // Compilation error!
}class Container<T> {
private Object[] items; // Use Object array internally
private int size = 0;
@SuppressWarnings("unchecked")
Container(int capacity) {
items = new Object[capacity];
}
void add(T item) {
items[size++] = item;
}
@SuppressWarnings("unchecked")
T get(int index) {
return (T) items[index];
}
}Object[] internally and cast when returning. The @SuppressWarnings("unchecked") annotation silences the compiler warning.Summary
- Generics allow writing classes, interfaces, and methods that work with any type while maintaining compile-time type safety. They were introduced in Java 5.
- Generic classes use type parameters: class Box<T> { T value; }. Common conventions: T (Type), E (Element), K (Key), V (Value), N (Number).
- Generic methods declare their own type parameters: static <T> T max(T a, T b). The type is inferred from arguments.
- Bounded type parameters restrict types: <T extends Number> limits T to Number and its subclasses. Multiple bounds use &: <T extends Number & Comparable<T>>.
- Wildcards (?) represent unknown types. Unbounded: List<?>. Upper bounded: List<? extends Number>. Lower bounded: List<? super Integer>.
- PECS (Producer Extends, Consumer Super): Use extends when reading from a collection (producer), super when writing to it (consumer). This is the golden rule for wildcard usage.
- Type erasure removes generic type information at runtime. ArrayList<String> and ArrayList<Integer> are both ArrayList at runtime. This limits generics: no new T(), no instanceof with parameterized types, no generic arrays.
- Generics prevent ClassCastException by catching type errors at compile time. Without generics, every retrieval from a collection requires an unsafe cast.
- Generic interfaces (Repository<T>) define type-safe contracts. Implementing classes specify the concrete type.
- Restrictions: No primitive types (use wrappers), no new T(), no T[] creation, no static generic fields, no generic exceptions.
- All Java collections (ArrayList<E>, HashMap<K,V>, HashSet<E>) use generics. Understanding generics is essential for using collections effectively.