Generics
A complete guide to Java generics: why they exist, generic classes and methods, type parameters, bounded and wildcard types, generic interfaces, type erasure, raw types, and using generics with the collections framework.
Why Generics?
Before generics were introduced in Java 5, the collections framework worked entirely with Object. You could put anything into a List, and when you got it back out you had to cast it explicitly. The compiler could not verify that the cast was safe, so a mismatch would only surface at runtime as a ClassCastException. That is a poor deal: bugs that the compiler could catch are instead allowed to slip through and crash a running system.
Generics move that safety check to compile time. By parameterising a class or method with a type, you tell the compiler exactly what kinds of objects are allowed. The compiler then enforces the constraint, inserts any necessary casts for you invisibly, and flags mismatches as errors before the program ever runs. The result is code that is safer, cleaner, and easier to read because the intent is stated explicitly.
Before and After Generics
JavaThe same list used without generics (raw type) and with generics, showing how type errors shift from runtime to compile time.
Generic Classes
To make a class generic, you add one or more type parameters in angle brackets immediately after the class name: class Box<T>. Inside the class body, T behaves like a real type name: you can declare fields of type T, return T from methods, and accept T as a parameter. When someone instantiates the class, they supply the actual type argument: Box<String> or Box<Integer>. The compiler substitutes the actual type everywhere T appears and enforces type safety accordingly.
A class can have multiple type parameters separated by commas, which is common for key-value containers: class Pair<A, B>. You can mix generic and non-generic fields freely within the same class.
Generic Classes
JavaA single-type Box and a two-type Pair generic class, each used with several concrete types.
Generic Methods
A method can introduce its own type parameters independently of the class it belongs to. This is a generic method. You declare the type parameter list just before the return type in the method signature: static <T> T pick(T a, T b). The type parameter is scoped only to that method; it does not affect the enclosing class.
In most cases the compiler can infer the type argument from the arguments you pass, so you do not need to write it explicitly. When inference is ambiguous you can provide it manually: Main.<String>pick("a", "b"). Generic methods are particularly useful for utility classes that work on arrays or collections without needing to be tied to a specific container type.
Generic Methods
JavaUtility methods for swapping array elements, finding an array's maximum, and wrapping a value in a list.
Type Parameters: T, E, K, V, and ?
Type parameter names are just identifiers: you can use any valid Java identifier you like. However, the Java convention is to use single uppercase letters, and different letters carry different implied meanings depending on context. Following this convention makes code immediately readable to anyone familiar with the Java ecosystem.
Standard type parameter naming conventions
These names are a convention, not a requirement. The compiler does not treat them differently.
- T (Type):The most general-purpose name. Used when there is no more specific role. class Box
, static void print(T value). - E (Element):Used in collection classes for the type of element stored. interface List
, interface Set . - K (Key):Used in map types for the key type. interface Map
. - V (Value):Used in map types for the value type. interface Map
, class HashMap . - N (Number):Used when the type parameter is expected to be a numeric type.
- ? (Wildcard):Not a named parameter. It represents an unknown type in wildcard positions, such as List> or List extends Number>.
Type Parameter Names in Context
JavaEach conventional name used in its natural role.
Bounded Type Parameters
By default, a type parameter like T accepts any reference type. Sometimes you need to restrict it. Bounded type parameters let you require that the type argument be a subtype of a specific class or interface.
An upper bound uses extends: <T extends Number> means T must be Number or a subclass of it. The word extends is used regardless of whether the bound is a class or an interface. You can stack multiple interface bounds using &: <T extends Comparable<T> & Cloneable>. The class bound (if any) must come first, followed by interface bounds.
Bounding a type parameter does two things: it restricts what callers can pass, and it unlocks methods. Without a bound, the compiler only knows that T is an Object, so you can only call Object methods. After adding extends Comparable<T>, the compiler knows that compareTo() is available.
Bounded Type Parameters
JavaA statistics class that requires T to be a Number, and a sort method that requires T to implement Comparable.
Wildcard Types: ?, ? extends, ? super
Wildcards appear in type arguments, not in type parameter declarations. They express flexibility at the call site: "I accept a collection of some type, but I am not committing to a specific one." There are three forms.
The three wildcard forms
Each form serves a distinct purpose. Choosing the wrong one either restricts your API unnecessarily or introduces type unsafety.
- ? (unbounded):List> accepts a list of any type. You can read elements as Object, but you cannot add anything (except null) because the compiler does not know the actual type.
- ? extends T (upper-bounded):List extends Number> accepts a list of Number or any subtype (Integer, Double, etc.). You can safely read elements as Number, but you still cannot add elements because the actual type might be List
and the compiler cannot guarantee your Number is an Integer. - ? super T (lower-bounded):List super Number> accepts a list of Number or any supertype (Object). You can safely add Number (or subtypes) to the list, but reading gives you only Object because the actual type could be List
PECS: Producer Extends, Consumer Super
A widely used rule of thumb for choosing between ? extends and ? super.
- Producer Extends:If a collection produces (provides) elements that your code reads, use ? extends T. You can read T values out of it safely.
- Consumer Super:If a collection consumes (accepts) elements that your code writes, use ? super T. You can write T values into it safely.
- Neither?If your code both reads and writes, use the exact type parameter without a wildcard.
Wildcards in Practice
JavaThree methods using each wildcard form to demonstrate what you can and cannot do with each.
Generic Interfaces
Interfaces can be generic in exactly the same way as classes. The type parameters are declared after the interface name and can be used in method signatures throughout the interface body. When a class implements a generic interface it has two choices: provide a concrete type argument to lock the type in, or keep the type parameter open and remain generic itself.
Two ways to implement a generic interface
The choice determines how flexible or specific the implementing class is.
- Provide a concrete type:class UserRepository implements Repository
. The class is specialised for User objects. Callers get full type safety without thinking about generics. - Stay generic:class InMemoryRepository
implements Repository . The class remains general; the caller decides the type when instantiating it.
Generic Interfaces
JavaA Repository<T, ID> interface implemented two ways: once with concrete types and once generically.
Type Erasure
Generics in Java are a compile-time feature, not a runtime feature. When the compiler finishes processing your generic code, it erases all type parameters and replaces them with their upper bounds, or with Object if there is no bound. It also inserts the necessary casts at every point where a typed value is retrieved. The resulting bytecode contains no generic type information at all.
This design decision meant that generics could be introduced in Java 5 without breaking backward compatibility with existing JVMs or libraries. The trade-off is that generic type information is unavailable at runtime. You cannot write new T(), new T[10], or check obj instanceof T because T no longer exists at runtime.
Practical consequences of type erasure
Understanding erasure explains several Java restrictions that otherwise seem arbitrary.
- Cannot instantiate T:new T() is illegal. The JVM does not know what constructor to call. Workaround: pass a Class
token and use reflection, or pass a Supplier . - Cannot create generic arrays:new T[10] is illegal. Use a List
or an Object[] with an unchecked cast (with @SuppressWarnings). - Cannot use instanceof with parameterised types:obj instanceof List
is illegal. You can only write obj instanceof List (raw type check). - List<String> and List<Integer> are the same class at runtime:Both erase to List. A runtime getClass() call on either returns class java.util.ArrayList.
Type Erasure in Action
JavaDemonstrating what generic type information survives at runtime and what disappears due to erasure.
Raw Types and Warnings
A raw type is a generic class or interface used without any type argument: List instead of List<String>. Raw types exist for backward compatibility with pre-generic code written before Java 5. The compiler allows them but generates an "unchecked" warning to signal that type safety can no longer be guaranteed.
The danger with raw types is that you give up all compile-time checking. Bugs that would be caught immediately with a properly parameterised type silently escape to runtime as ClassCastException. In new code, there is almost never a good reason to use raw types. When you genuinely need to suppress the unchecked warning (for example, in framework code that uses reflection), use @SuppressWarnings("unchecked") on the smallest possible scope and add a comment explaining why it is safe.
Raw Types and their Consequences
JavaMixing raw and generic types to show how type pollution leads to a ClassCastException at an unexpected location.
Generics with Collections
The collections framework is the most visible beneficiary of generics in the Java standard library. Every collection class and interface is generic: List<E>, Set<E>, Map<K, V>, Queue<E>, and all their implementations. The Streams API, Comparator, Optional, and most of the functional interfaces in java.util.function are also generic. Generics are therefore not a niche feature: they are the foundation that makes the entire modern Java API usable without constant casting.
One important subtlety: generic types are not covariant. Even though Integer extends Number, a List<Integer> is not a List<Number>. Allowing that assignment would let someone add a Double to what the other variable believes is a List<Integer>, breaking type safety. Wildcards are the correct tool for expressing "any list of a Number subtype."
Generics with Collections: Common Patterns
JavaType-safe sorting, grouping, and transformation using generic collections and the Streams API.
Quiz - Test Your Knowledge
Ten questions covering why generics exist, generic classes and methods, type parameter naming conventions, bounded type parameters, wildcard types and the PECS rule, generic interfaces, type erasure and its practical consequences, raw types, and generics with collections. Read each option carefully before selecting your answer.
Knowledge Check
1. What is the primary benefit of using generics in Java?
2. What does the type parameter declaration <T extends Comparable<T>> mean?
3. What is type erasure in Java generics?
4. What is a raw type?
5. Given the method signature static <T> void swap(T[] arr, int i, int j), what does the <T> before the return type signify?
6. Which wildcard should you use when you want to read elements from a collection but do not care about the exact type?
7. The Producer Extends, Consumer Super (PECS) rule guides wildcard choice. Which wildcard is correct for a method that adds elements to a List?
8. Why can you not create an array of a generic type, such as new T[10], inside a generic class?
9. A generic interface Repository<T, ID> is implemented by a class UserRepository. What must UserRepository declare?
10. Which of the following statements about using generics with collections is correct?