Generics in Java – Type-Safe Classes and Methods
Generics let classes, interfaces and methods work with type parameters, such as List<String> or Map<String, Integer>. The compiler then checks that only the right types are used, and the casts disappear from your code. Generics were added in Java 5 and are the foundation of the Collections Framework and most modern APIs.
This tutorial covers why generics exist, generic classes and methods, bounded type parameters, wildcards and the PECS rule, type erasure, and the restrictions it imposes.
1. Why Generics?
import java.util.ArrayList;
import java.util.List;
public class WhyGenerics {
@SuppressWarnings({"rawtypes", "unchecked"})
public static void main(String[] args) {
// Without generics (raw type): anything goes in, casts needed, errors at run time
List raw = new ArrayList();
raw.add("Java");
raw.add(42); // compiles
try {
for (Object o : raw) {
String s = (String) o; // ClassCastException on 42
System.out.println("raw: " + s.toUpperCase());
}
} catch (ClassCastException e) {
System.out.println("raw: ClassCastException at run time");
}
// With generics: the compiler enforces the element type, no casts
List<String> names = new ArrayList<>();
names.add("Java");
// names.add(42); // compile error
for (String s : names) System.out.println("generic: " + s.toUpperCase());
}
}
raw: JAVA raw: ClassCastException at run time generic: JAVA
- Type safety: errors are caught at compile time instead of as
ClassCastExceptionat run time. - No casts:
list.get(0)already has the right type. - Reusable code: one implementation works for many types.
2. Generic Classes and Records
Declare type parameters in angle brackets after the class name. By convention they are single capital letters: T (type), E (element), K/V (key/value), R (result):
public class GenericClass {
// A generic class with one type parameter T
static class Box<T> {
private T value;
Box(T value) { this.value = value; }
T get() { return value; }
void set(T value) { this.value = value; }
boolean isEmpty() { return value == null; }
}
// Two type parameters
record Pair<K, V>(K key, V value) {
<R> Pair<K, R> withValue(R newValue) { return new Pair<>(key, newValue); }
}
public static void main(String[] args) {
Box<String> name = new Box<>("Asha");
Box<Integer> age = new Box<>(29);
String n = name.get(); // no cast
int a = age.get(); // auto-unboxing
System.out.println(n + " is " + a);
Pair<String, Integer> score = new Pair<>("Ravi", 87);
Pair<String, String> grade = score.withValue("A");
System.out.println(score + " -> " + grade);
}
}
Asha is 29 Pair[key=Ravi, value=87] -> Pair[key=Ravi, value=A]
The diamond operator <> (Java 7) lets the compiler infer type arguments: new Box<>("Asha").
3. Generic Methods and Bounded Types
A method can declare its own type parameters before the return type. Bounds restrict which types are allowed, so you can call methods of the bound:
import java.util.*;
public class GenericMethods {
// Generic method: <T> before the return type
static <T> T firstOrDefault(List<T> list, T fallback) {
return list.isEmpty() ? fallback : list.get(0);
}
// Bounded type parameter: T must be Comparable to itself
static <T extends Comparable<T>> T max(List<T> items) {
T best = items.get(0);
for (T item : items) if (item.compareTo(best) > 0) best = item;
return best;
}
// Multiple bounds: a Number that is also Comparable
static <T extends Number & Comparable<T>> double sumAbove(List<T> nums, T limit) {
double sum = 0;
for (T n : nums) if (n.compareTo(limit) > 0) sum += n.doubleValue();
return sum;
}
public static void main(String[] args) {
System.out.println(firstOrDefault(List.of("kafka", "spring"), "none"));
System.out.println(firstOrDefault(List.<String>of(), "none"));
System.out.println(max(List.of(4, 19, 7)));
System.out.println(max(List.of("pear", "apple", "zucchini")));
System.out.println(sumAbove(List.of(5, 12, 30, 8), 10));
System.out.println(Collections.<Integer>emptyList()); // explicit type argument
}
}
kafka none 19 zucchini 42.0 []
<T extends Comparable<T>>: T must be comparable, socompareTocan be called.<T extends Number & Comparable<T>>: multiple bounds; a class bound must come first.extendsis used for both classes and interfaces in bounds.
4. Wildcards and PECS
Generic types are invariant: a List<Integer> is not a List<Number>, even though Integer is a Number. Wildcards add flexibility:
import java.util.*;
public class Wildcards {
// Upper bounded: read Numbers from any list of Number subtypes (producer)
static double total(List<? extends Number> nums) {
double sum = 0;
for (Number n : nums) sum += n.doubleValue();
return sum;
}
// Lower bounded: write Integers into any list that can hold them (consumer)
static void addSquares(List<? super Integer> target, int count) {
for (int i = 1; i <= count; i++) target.add(i * i);
}
// Unbounded: works with any element type when only Object methods are needed
static void printAll(Collection<?> items) {
System.out.println("size " + items.size() + ": " + items);
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
System.out.println("total ints = " + total(ints));
System.out.println("total doubles = " + total(doubles));
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addSquares(numbers, 3);
addSquares(objects, 2);
printAll(numbers);
printAll(objects);
printAll(Set.of("x"));
// List<Number> n = ints; // compile error: List<Integer> is NOT a List<Number>
}
}
total ints = 6.0 total doubles = 4.0 size 3: [1, 4, 9] size 2: [1, 4] size 1: [x]
| Wildcard | Accepts | Can read as | Can add |
|---|---|---|---|
<? extends T> | T or any subtype | T | Nothing (except null) |
<? super T> | T or any supertype | Object | T and its subtypes |
<?> | Any type | Object | Nothing (except null) |
Remember PECS: Producer Extends, Consumer Super. If a parameter produces values you read, use extends; if it consumes values you write, use super. Collections.copy(List<? super T> dest, List<? extends T> src) uses both.
5. Type Erasure
Generics are a compile-time feature. The compiler checks types, inserts casts, and then erases type arguments, replacing them with their bounds (or Object). At run time a List<String> and a List<Integer> are the same class:
import java.util.*;
public class TypeErasure {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
// Type arguments are erased at run time: both are just ArrayList
System.out.println("Same runtime class: " + (strings.getClass() == numbers.getClass()));
System.out.println("Runtime class: " + strings.getClass().getName());
// Things erasure forbids (compile errors):
// new T(); cannot instantiate a type parameter
// new List<String>[10]; no generic array creation
// obj instanceof List<String> type arguments not available at run time
// List<int> primitives not allowed; use List<Integer>
}
}
Same runtime class: true Runtime class: java.util.ArrayList
Restrictions caused by erasure
- No primitives as type arguments: use
List<Integer>, notList<int>. - Cannot create instances (
new T()) or arrays (new T[10]) of a type parameter; pass aClass<T>orSupplier<T>instead. - Cannot use
instanceofwith parameterized types, or overload methods that differ only in type arguments. - Static fields cannot use the class's type parameter.
- Generic classes cannot extend
Throwable.
6. Raw Types
Using a generic type without type arguments (List list) creates a raw type, which exists only for compatibility with pre-Java 5 code. Raw types switch off generic type checking and produce "unchecked" warnings. Use List<?> when you truly do not care about the element type.
7. Summary
- Generics add type parameters to classes, interfaces and methods for compile-time type safety without casts.
- Bounded types (
T extends X) allow calling methods of the bound. - Generic types are invariant; wildcards follow PECS: producer
extends, consumersuper. - Type arguments are erased at run time, which explains the main restrictions.
- Avoid raw types in new code.
Related Tutorials
Frequently Asked Questions (FAQ)
Q1. What are generics in Java?
A feature that lets types and methods take type parameters, giving compile-time type checking and removing the need for casts.
Q2. Why were generics introduced?
To make collections and other reusable code type-safe, catching type errors at compile time rather than at run time.
Q3. What is a type parameter?
A placeholder for a type, such as T in class Box<T>, replaced by a real type argument when the class is used.
Q4. What is a generic method?
A method that declares its own type parameters before the return type, like static <T> T first(List<T> list).
Q5. What is a bounded type parameter?
A type parameter restricted to a type and its subtypes, such as <T extends Number>.
Q6. What is the difference between <? extends T> and <? super T>?
? extends T accepts T or subtypes and is used for reading (producers). ? super T accepts T or supertypes and is used for writing (consumers).
Q7. What does PECS mean?
Producer Extends, Consumer Super: a guideline for choosing wildcard bounds in method parameters.
Q8. Why is List<Integer> not a List<Number>?
If it were, you could add a Double to a list of integers through the List<Number> reference. Invariance prevents that.
Q9. What is type erasure?
The compiler's removal of generic type information after type checking, so generic types share a single runtime class.
Q10. Can I use primitive types with generics?
No. Use wrapper classes like Integer and rely on autoboxing, or primitive-specialised types such as IntStream.
Q11. Why can't I create a generic array?
Arrays check element types at run time, but erased generic types have no runtime type information, so the check could not be enforced safely.
Q12. What is a raw type?
A generic type used without type arguments, like List. It disables type checking and exists only for backward compatibility.
Q13. What is the diamond operator?
The empty <> in new ArrayList<>(), which lets the compiler infer type arguments from context.
Q14. What is the difference between List<?> and List<Object>?
List<?> accepts a list of any element type but you cannot add to it. List<Object> accepts only lists declared with Object elements, and you can add anything.
Q15. Can static methods be generic?
Yes, by declaring their own type parameters. They cannot use the class's type parameters.
Q16. Can interfaces be generic?
Yes. Comparable<T>, List<E> and Function<T, R> are generic interfaces.
Q17. What does an unchecked warning mean?
The compiler cannot verify a generic operation, usually because of raw types or casts to generic types. Fix the code or suppress the warning only when you are sure it is safe.
Q18. What is a recursive type bound?
A bound that refers to the type parameter itself, such as <T extends Comparable<T>>, meaning T can be compared to other T values.
Q19. Can generic types be used with exceptions?
A generic class cannot extend Throwable, and you cannot catch a type parameter. Methods can still declare throws with a type parameter bounded by Exception.
Q20. How do I create an instance of T?
Pass a Supplier<T> (such as ArrayList::new) or a Class<T> and call getDeclaredConstructor().newInstance().