
Introduction
Java generics are a language feature that lets you write classes, interfaces, and methods that operate on types specified by the caller. In short: generics help you write reusable, type-safe code without losing readability. This article will explain what java generics are, why they matter, how they work (including common pitfalls like type erasure), and show practical examples and best practices you can use right away.
What are Java Generics?
Generics let you parameterize types. Instead of hard-coding a specific type into a class or method, you use a type parameter (commonly T
, E
, K
, V
, etc.). That allows the same class or method to work with different types while preserving compile-time type checking.
Simple idea:
List<String> names = new ArrayList<>();
Here List<String>
tells the compiler: this list must only hold String
objects. Without generics you’d have List
of Object
and you’d need casts when retrieving elements casts that can fail at runtime. With generics, many errors are caught at compile time.
Why use Java Generics? (Benefits)
- Type safety: The compiler checks that you don’t put the wrong type into a collection or pass incompatible types to a method.
- Eliminate casts: No more explicit casting when you get items out of a collection.
- Reusability: Write one class or method and reuse it for many types.
- Expressiveness: Method signatures make intent clearer (
Map<String, Integer>
means something different thanMap<Object, Object>
).
Generic classes and interfaces
Define a class with a 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; }
}
Use it:
Box<String> b = new Box<>("hello");
String s = b.get(); // no cast needed
Generic interfaces work the same:
public interface Pair<K, V> {
K getKey();
V getValue();
}
Generic methods
Methods can declare their own type parameters, independent of the class:
public static <T> T pickFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
You can call pickFirst(List<String>)
and T
becomes String
for that invocation.
Bounded type parameters
Sometimes you want to restrict the types that can be used with a generic parameter. Use extends
for an upper bound:
public class NumberBox<T extends Number> {
private T value;
public double doubleValue() {
return value.doubleValue();
}
}
Now NumberBox<Integer>
and NumberBox<Double>
are allowed, but NumberBox<String>
is not.
You can use multiple bounds with &
(first bound must be a class or another type parameter; the rest are interfaces):
<T extends SomeClass & Comparable<T>>
Wildcards: ?
, ? extends
, ? super
Wildcards let you be flexible in method parameters.
List<?>
— a list of unknown element type (read-only for element-specific operations).List<? extends Number>
— a list of some subtype ofNumber
. You can read elements asNumber
but you cannot safely add new elements (exceptnull
).List<? super Integer>
— a list of some supertype ofInteger
(e.g.,List<Number>
orList<Object>
). You can addInteger
objects safely but reading yieldsObject
(the least specific type guaranteed).
Rule of thumb (PECS):
- Producer extends: use
? extends T
when you only read (produce)T
from the structure. - Consumer super: use
? super T
when you only write (consume)T
into the structure.
Example:
void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
Type erasure — how generics work at runtime
Java generics are implemented with type erasure. That means type parameters are enforced at compile time, but at runtime the JVM sees only the raw classes (e.g., ArrayList
). Generic type information is mostly removed.
Consequences:
- You cannot use primitives as type parameters (
List<int>
is invalid; useList<Integer>
). - You cannot create
new T()
inside a generic class becauseT
is erased. - You cannot create arrays of parameterized types (
new List<String>[]
is unsafe and disallowed). List<String>.class
does not exist you only haveList.class
.
Type erasure preserves backwards compatibility with older Java versions but requires some care.
Common pitfalls and mistakes
- Mixing raw types: Avoid raw types (e.g.,
List
without<T>
). They disable compile-time checks and reintroduce unsafe casts. - Unchecked casts: You may see
@SuppressWarnings("unchecked")
in some code. Use it sparingly and only when you’re sure the cast is safe. - Creating generic arrays:
List<String>[] arr = new List<String>[10];
is illegal. - Static members with type parameters: You cannot reference a class’s type parameter in a static context (
static T value;
is illegal). Type parameters belong to instances. - Overloading ambiguity: Methods that differ only by generic type parameters can cause compiler confusion; avoid relying on it.
Advanced: Generic constructors and diamond operator
Generic constructors let you specify type parameters at constructor level:
public class Utility {
public <T> List<T> singletonList(T item) {
List<T> list = new ArrayList<>();
list.add(item);
return list;
}
}
Since Java 7 the diamond operator (<>
) infers generic types:
Map<String, Integer> map = new HashMap<>(); // compiler infers types
Interacting with legacy code
When interacting with older libraries without generics, you may need to use raw types and casts, but wrap those interactions carefully and narrow the unsafe parts to small, well-tested places.
Practical examples
- Generic repository (simplified)
public interface Repository<T, ID> {
T findById(ID id);
void save(T entity);
}
public class InMemoryRepository<T, ID> implements Repository<T, ID> {
private Map<ID, T> store = new HashMap<>();
public T findById(ID id) { return store.get(id); }
public void save(T entity) {
// simplistic: assume T has getId() — in real code use functional interfaces
}
}
- Utility swap method
public static <T> void swap(T[] arr, int i, int j) {
T tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
- Comparator with generics
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
Best practices
- Prefer generics over raw types for collections and APIs.
- Use meaningful type parameter names when it helps (
K
andV
for maps,E
for elements,T
for general type). - Favor bounded wildcards in public APIs to increase flexibility (
List<? extends Number>
vsList<Number>
). - Keep generic constraints as loose as possible only restrict types when necessary.
- Avoid
@SuppressWarnings("unchecked")
unless you understand why the warning occurs and you’ve minimized the unsafe code region. - Document tricky usages: since erasure hides runtime types, include comments where the intent or safety is not obvious.
When not to use generics
- When the added complexity reduces readability for simple classes.
- When you need to work directly with runtime type metadata (reflection-heavy code might require other approaches).
- When performance profiling shows generics-based abstractions cause measurable overhead in tight hot paths (rare).
Quick reference (cheat sheet)
- Declaration of a generic class:
class Box<T> { ... }
- Generic method:
public static <T> T id(T x) { return x; }
- Upper bound:
<T extends Number>
- Multiple bounds:
<T extends Number & Comparable<T>>
- Wildcards:
?
,? extends T
,? super T
- Diamond operator:
new ArrayList<>()
- Type erasure: runtime lacks generic type params
Conclusion
Java generics are a powerful tool to make your code safer, clearer, and more reusable. They bring compile-time checks and remove many runtime cast errors. Understanding type parameters, bounds, wildcards, and type erasure gives you the confidence to design generic APIs and use Java’s collections and libraries safely. Start by adding generics to your collections and small utility classes you’ll quickly notice fewer bugs and cleaner code.
Leave a Reply