Generics
Generics let you write classes, interfaces, and methods that work with any type you specify at compile time, catching type mismatches before your program ever runs. Introduced in Java 5, they’re the engine behind the entire Collections Framework — and once you understand them, you’ll write far more reusable, self-documenting code.
Why Generics?
Before generics, a list held raw Object references. You could put a String and an Integer into the same list — which sounds flexible but leads to ClassCastException surprises at runtime.
// Pre-generics (raw type) — avoid this
import java.util.ArrayList;
public class RawExample {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("Hello");
list.add(42); // compiles — but dangerous
String s = (String) list.get(1); // ClassCastException at runtime!
}
}
With generics, the compiler rejects the bad cast entirely:
import java.util.ArrayList;
public class GenericExample {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(42); // compile-time error — great!
String first = names.get(0); // no cast needed
System.out.println(first);
}
}
Output:
Alice
The payoff: zero runtime ClassCastException, no manual casting, and self-documenting code.
Generic Classes
You define a generic class by adding a type parameter in angle brackets after the class name. By convention, single uppercase letters are used: T (type), E (element), K (key), V (value).
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public static void main(String[] args) {
Box<String> strBox = new Box<>("Hello Generics");
Box<Integer> intBox = new Box<>(100);
System.out.println(strBox.getValue());
System.out.println(intBox.getValue());
}
}
Output:
Hello Generics
100
Tip: From Java 7 onward, you can use the diamond operator
<>on the right side — the compiler infers the type argument so you don’t repeat yourself:new Box<>("text")instead ofnew Box<String>("text").
Generic Methods
A method can declare its own type parameters independently of its class. The type parameter is placed before the return type:
public class Utils {
// Works for any array type
public static <T> void printArray(T[] items) {
for (T item : items) {
System.out.print(item + " ");
}
System.out.println();
}
public static void main(String[] args) {
String[] words = {"Java", "Generics", "Rocks"};
Integer[] numbers = {1, 2, 3, 4, 5};
printArray(words);
printArray(numbers);
}
}
Output:
Java Generics Rocks
1 2 3 4 5
Bounded Type Parameters
Sometimes you want to restrict which types are allowed. Use extends to set an upper bound:
public class Stats {
// T must be a Number (or subclass: Integer, Double, etc.)
public static <T extends Number> double sum(T[] arr) {
double total = 0;
for (T n : arr) {
total += n.doubleValue();
}
return total;
}
public static void main(String[] args) {
Integer[] ints = {1, 2, 3, 4};
Double[] doubles = {1.5, 2.5, 3.0};
System.out.println(sum(ints)); // 10.0
System.out.println(sum(doubles)); // 7.0
}
}
Output:
10.0
7.0
Note: In a bounded parameter like
<T extends Comparable<T>>,extendsis used even for interfaces — it means “T implements Comparable”.
Multiple Bounds
A type parameter can have multiple bounds, separated by &. The class (if any) must come first:
// T must extend Animal AND implement Serializable
public class Cage<T extends Animal & java.io.Serializable> { ... }
Wildcards
Wildcards (?) let you write flexible method signatures that accept a family of parameterised types.
| Wildcard | Meaning | Typical use |
|---|---|---|
? | Unknown type | Read-only iteration |
? extends T | T or any subtype | Reading (producer) |
? super T | T or any supertype | Writing (consumer) |
import java.util.List;
public class WildcardDemo {
// Upper-bounded: reads from any list of Numbers
static double sumList(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue();
return total;
}
// Lower-bounded: adds integers into any list of Number or Object
static void addIntegers(List<? super Integer> list) {
list.add(10);
list.add(20);
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
System.out.println(sumList(ints)); // 6.0
}
}
Output:
6.0
Tip: A handy mnemonic is PECS — Producer Extends, Consumer Super. If a collection produces values you read, use
? extends. If it consumes values you write, use? super.
Generic Interfaces
Interfaces can be generic too. The classic example is the Comparable interface:
public class Student implements Comparable<Student> {
String name;
int grade;
Student(String name, int grade) {
this.name = name;
this.grade = grade;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.grade, other.grade);
}
}
Under the Hood: Type Erasure
Here is the most important thing to understand if you want to avoid subtle bugs: Java generics are erased at compile time.
The compiler:
- Replaces all type parameters with their bounds (or
Objectfor unbounded parameters). - Inserts casts where necessary.
- Generates bridge methods for polymorphism to work correctly.
The compiled bytecode contains no T, E, or K — just Object or the bound type. This is called type erasure, and it was chosen to maintain backward compatibility with pre-Java-5 bytecode.
// What you write
Box<String> b = new Box<>("hi");
// What the compiler effectively produces in bytecode
Box b = new Box("hi");
String s = (String) b.getValue();
Consequences of Type Erasure
- You cannot create an instance of a type parameter:
new T()is illegal. - You cannot create a generic array:
new T[10]is illegal. instanceofchecks against a generic type are not allowed:obj instanceof List<String>won’t compile.- At runtime,
Box<String>andBox<Integer>are the same class —getClass()returns the same object.
import java.util.ArrayList;
public class ErasureDemo {
public static void main(String[] args) {
var strings = new ArrayList<String>();
var integers = new ArrayList<Integer>();
System.out.println(strings.getClass() == integers.getClass()); // true
}
}
Output:
true
Warning: Avoid raw types (e.g.,
Listinstead ofList<String>) in modern code. They suppress compiler checks and re-introduce theClassCastExceptionrisks that generics were designed to prevent. The compiler will warn you — treat those warnings seriously.
Generics vs Arrays
Arrays and generics don’t mix well because arrays are covariant (an Integer[] is a String[]… wait, no — and the JVM will throw ArrayStoreException), while generics are invariant (List<Integer> is NOT a List<Number>).
| Feature | Array | Generic collection |
|---|---|---|
| Type-safety | Runtime check | Compile-time check |
| Covariance | Yes (Number[] n = new Integer[5]) | No (List<Number> ≠ List<Integer>) |
| Reifiable | Yes | No (erased) |
| Preferred for | Primitives, performance | Object collections |
For most use cases, prefer generic collections like ArrayList over raw arrays of objects.
In This Section
This section covers several Java 5 language features that complement Generics:
- Annotations — Add metadata to your code that tools, frameworks, and the compiler can read at build time or runtime.
- Autoboxing & Unboxing — How Java automatically converts between primitives (int, double) and their wrapper types (Integer, Double) so they work seamlessly with generics.
- Varargs — Write methods that accept a variable number of arguments using the
...syntax, often combined with generics for flexible utility methods. - Static Import — Import static members directly so you can write
sort(list)instead ofCollections.sort(list), reducing boilerplate in test and utility code.
Related Topics
- Collections Framework — Generics power the entire collections API; start here to see them in action.
- ArrayList — The most-used generic collection, a great place to apply what you’ve learned.
- Comparable — A classic generic interface you’ll implement to make your objects sortable.
- Comparator — Another generic interface for custom sort orders, often passed as a lambda.
- Autoboxing & Unboxing — Understand how primitives flow in and out of generic collections automatically.
- Lambda Expressions — Modern Java combines generics with lambdas and functional interfaces for powerful, concise APIs.