Key Takeaways
- Java generics let you reuse classes and methods with different reference types while retaining compile-time type checking.
- A useful generic method connects types—for example, linking a list's element type to the method's return type.
- The diamond operator infers type arguments when constructing objects; generic method calls can also infer type arguments without diamond syntax.
- Bounds and wildcards make generic APIs more expressive, while type erasure limits which generic checks are possible at runtime.
Java generics let classes, interfaces, and methods accept type parameters: placeholders such as T that callers fill with reference types such as String or Integer. Type inference lets the compiler determine many of those type arguments from method arguments and the surrounding context, without weakening compile-time checks.
What Are Java Generics?
Generics were introduced in Java 5. Instead of writing separate containers for strings and integers, you write one generic container and choose its type argument wherever you use it.
Start with familiar Java arrays. These declarations are typed, but they aren't examples of generics:
// Fragment inside a method.
int[] myInts = {1, 2, 3};
String[] myStrings = {"hello", "goodbye"};A generic collection instead takes an element type as an argument. List<String> means a list whose elements are strings—not a list that freely accepts unrelated types. Reading an element returns a String without an explicit cast.
// Fragment inside a method; requires these imports:
// import java.util.ArrayList;
// import java.util.List;
List<String> names = new ArrayList<>();
names.add("hello");
String name = names.get(0);
// names.add(23); // Compile-time error: Integer is not String.The distinction in terminology matters: T in a generic declaration is a type parameter; String in List<String> is a type argument. Type arguments must be reference types, not primitives such as int.
Java Generic Methods: Declare a Type Relationship
A generic method declares its own type parameters before its return type. In public static <E> void printVar(E var), <E> declares the type parameter, while E var declares the value parameter named var.
Save this complete example as GenericMethods.java:
import java.util.List;
public class GenericMethods {
public static <E> void printVar(E var) {
System.out.println(var);
}
public static <T> T first(List<T> values) {
if (values.isEmpty()) {
throw new IllegalArgumentException("values must not be empty");
}
return values.get(0);
}
public static void main(String[] args) {
printVar("John");
printVar(23);
String name = first(List.of("John", "Josh"));
Integer age = first(List.of(23, 42));
System.out.println(name + ": " + age);
}
}printVar accepts different types, but it doesn't need generics: an Object parameter would serve this printing example. The useful relationship appears in first: its List<T> input and T result share the same type parameter, preserving the element type for callers. The implementation explicitly rejects an empty list.
In printVar(23), the primitive value is boxed to Integer; the inferred type argument isn't int. Review Java wrapper classes and autoboxing if that conversion is unfamiliar.
Type parameter names are identifiers, not special keywords. Common conventions are T for type, E for element, K for key, and V for value.
Java Generic Classes: MyPerson<T>
A generic class declares its type parameters after the class name. Here, one T connects the field, setter parameter, and getter result. Save this example as MyPerson.java.
public class MyPerson<T> {
private T t;
public T get() {
return this.t;
}
public void set(T value) {
this.t = value;
}
public static void main(String[] args) {
MyPerson<String> stringType = new MyPerson<>();
stringType.set("Josh");
String name = stringType.get();
MyPerson<Integer> integerType = new MyPerson<>();
integerType.set(23);
Integer age = integerType.get();
System.out.println(name + ": " + age);
// stringType.set(23); // Compile-time error.
}
}For MyPerson<String>, calls to set require a value compatible with String, and get returns String. The Integer instance uses the same class declaration with a different type argument; you don't write another implementation.
Java Type Inference and the Diamond Operator
Explicit Type Arguments Versus Diamond Syntax
The diamond operator, <>, has been available since Java 7. In the second declaration below, the assignment target MyPerson<String> lets the compiler infer String for the object creation expression. Both declarations retain the same compile-time type checks.
// Fragment inside a method; uses MyPerson from above.
MyPerson<String> explicit = new MyPerson<String>();
MyPerson<String> inferred = new MyPerson<>();Method Inference and Target Typing
Diamond syntax is only one form of inference. Generic methods usually infer their type arguments without any angle brackets at the call site. You can supply an explicit method type argument—a type witness—when useful.
// Fragment inside a method; requires import java.util.List;
// and GenericMethods from above.
String inferred = GenericMethods.first(List.of("Josh"));
String explicit = GenericMethods.<String>first(List.of("Josh"));
List<String> empty = List.of();The first call links the list's element type to the result. In the last declaration, List.of() has no element arguments, so the expected List<String> result provides the context. This is target typing: using the type an expression is expected to have. Inference happens at compile time; it doesn't inspect runtime values or later statements.
Bounds, Invariance, and Wildcards
Bounded Type Parameters Restrict Accepted Types
A declaration such as <T extends Number> restricts T to Number or its subtypes. It also lets the method call operations declared by that bound, such as Number.doubleValue(). In a type parameter bound, extends is also used when the bound is an interface.
Why List<Integer> Is Not List<Number>
Generic type parameters are invariant: even though Integer extends Number, List<Integer> isn't a subtype of List<Number>. Otherwise, code receiving that reference as a List<Number> could insert a Double into a list intended for integers.
// Fragment inside a method; requires import java.util.List;
List<Integer> integers = List.of(1, 2, 3);
// List<Number> numbers = integers; // Compile-time error.
List<? extends Number> numbers = integers; // Allowed.Wildcards: Producer Extends, Consumer Super
A wildcard, ?, stands for an unknown type. Use ? extends T when your code reads values as T, and ? super T when it writes values of type T. The mnemonic is PECS: producer extends, consumer super—from the perspective of the method using the collection.
| List type | Read elements as | Add non-null elements of these types |
|---|---|---|
List<Number> | Number | Number and its subtypes |
List<?> | Object | None safely |
List<? extends Number> | Number | None safely |
List<? super Integer> | Object | Integer |
These are type-checking permissions, not guarantees that a list supports mutation. In particular, ? extends Number doesn't make a list immutable; operations such as clearing it may still be permitted.
Save this complete example as NumberExamples.java:
import java.util.ArrayList;
import java.util.List;
public class NumberExamples {
public static <T extends Number> double sum(List<T> values) {
double total = 0;
for (T value : values) {
total += value.doubleValue();
}
return total;
}
public static double sumNumbers(List<? extends Number> values) {
double total = 0;
for (Number value : values) {
total += value.doubleValue(); // Reading is safe.
}
// values.add(1); // Compile-time error: unknown element type.
return total;
}
public static void addInteger(List<? super Integer> values) {
values.add(23); // Writing Integer is safe.
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3)));
System.out.println(sumNumbers(List.of(1.5, 2.5)));
List<Number> destination = new ArrayList<>();
addInteger(destination);
System.out.println(destination);
}
}sum demonstrates a named bound. For an API that only reads numbers and returns a double, sumNumbers is usually the simpler signature: no caller needs a named relationship involving T. Both examples convert values to double; that conversion can lose precision, so don't treat them as exact-decimal arithmetic utilities.
Type Erasure: The Runtime Boundary
Java implements generics using type erasure. The compiler replaces an unbounded type variable with Object, or a bounded variable with the erasure of its leftmost bound, and inserts casts where necessary. It doesn't create a separate runtime class for each type argument.
Compile-time parameterizations Runtime class
ArrayList<String> --+
+-----------> ArrayList
ArrayList<Integer> --+That boundary explains several practical restrictions:
- You cannot construct a type variable with
new T(). Supply a factory when generic code needs to create values. - You cannot create
new T[10]ornew List<String>[10]. The required runtime array element type isn't fully available; prefer a generic collection when appropriate. - For a variable declared as
Object,value instanceof List<String>is rejected.value instanceof List<?>can check whether it is a list, but doesn't validate its element types. - You cannot overload methods solely by changing a parameter from
List<String>toList<Integer>: their parameter types erase to the sameListtype.
Generics therefore provide compile-time protection, not automatic runtime validation of every collection element. A cast inserted by the compiler can still fail if unsafe code has put an incompatible value into the collection.
Common Mistakes
- Using raw types.
new MyPerson()is raw;new MyPerson<>()requests inference. Raw types and unchecked conversions can bypass generic checks, producing warnings and possible runtimeClassCastExceptions rather than an immediate compilation error. - Combining
varand diamond without enough context. With the class above,var person = new MyPerson<>();infersMyPerson<Object>. Prefer an explicit target type orvar person = new MyPerson<String>();when you want strings. See OpenJDK's explanation of var with diamond. - Expecting generics to guarantee non-null values. In this example,
MyPerson.get()returns null beforesetis called because its reference field starts with the default value null. Define null and empty-input behavior separately from your generic type constraints.
After saving the three complete examples, compile them with detailed unchecked-operation and raw-type warnings enabled:
javac -Xlint:unchecked,rawtypes MyPerson.java GenericMethods.java NumberExamples.javaThe javac lint options help expose places where the compiler cannot fully verify type safety. Don't suppress those warnings merely to make them disappear.
FAQ
How does the diamond operator infer a type in Java?
The diamond operator asks the compiler to infer a generic class's type arguments from context, including the assignment target and constructor arguments. In MyPerson<String> person = new MyPerson<>(), the target supplies String.
Can Java generics use primitive types?
No. Java type arguments must be reference types, so use Integer instead of int. Passing an int to a generic method can trigger autoboxing to Integer.
Why are Java generic types invariant?
Java generic types are invariant. Allowing that assignment would let code insert another Number subtype, such as Double, into a list intended for Integer values; use List<? extends Number> when you only need to read numbers.
When should I use a wildcard in Java generics?
Use ? when the exact element type doesn't matter, ? extends T when a collection produces values you read as T, and ? super T when it consumes values you write as T. Use a named type parameter when you need to connect multiple parameter or return types.
What is type erasure in Java generics?
Type erasure replaces generic type variables with their erased bounds during compilation and removes type arguments from runtime parameterized types. For example, ArrayList<String> and ArrayList<Integer> share the same runtime class, so their element type arguments aren't distinguished by that class.
Further Reading
- Introducing Generics — generic types, methods, naming conventions, and raw types.
- Type Inference — diamond syntax, method inference, and target typing.
- Wildcards — upper bounds, lower bounds, and wildcard capture.
- Type Erasure — erased types, inserted casts, and bridge methods.
- Restrictions on Generics — construction, arrays, and erasure-related limitations.
- Java Language Specification: Expressions — precise rules for object creation, arrays, and instanceof.
- List API Reference — element types, list operations, and List.of behavior.
- javac Command Reference — compiler usage and lint categories.
