Java Generics
Updated 12 min read Aimerscrew
Every time you've written List<String> or Map<String, Integer>, you've been using Java generics. Generics let one class or method work with many different types while the compiler still checks that you use those types correctly. In this lesson, you'll see why generics exist, write your own generic classes and methods, limit them with bounded types, and understand the wildcards ? extends and ? super in plain words.
🌍 Real-life example
Think of labeled storage boxes. A box labeled "Shoes" can technically hold anything, but the label tells everyone what belongs inside, and a good roommate refuses to put a sandwich in it. When you open it later, you know you'll find shoes. A generic type is that label: Box<Shoe> tells the compiler what goes in, and the compiler refuses anything else.
What are generics in Java?
Generics are a way to give a class, interface or method a type parameter: a placeholder type, usually written as a single capital letter like T, that gets filled in when the class is used. ArrayList<E> is written once by the Java team, and you choose what E means: ArrayList<String>, ArrayList<Integer>, ArrayList<Flight>.
The value inside the angle brackets is called a type argument. It must be a class type, which is why you use wrapper classes like Integer rather than int.
Why use generics?
Before Java 5 there were no generics. A list held plain Objects, so you could put anything in and had to cast every element when taking it out:
List names = new ArrayList(); // no type: a "raw" list
names.add("Grace");
names.add(42); // allowed, but a bug waiting to happen
String first = (String) names.get(0);
String second = (String) names.get(1); // crashes at run time: ClassCastException
The mistake (adding a number to a list of names) is only discovered when the program runs, maybe weeks later, in front of a user. With generics, the compiler catches it immediately, and you don't need casts:
import java.util.ArrayList;
import java.util.List;
public class TypeSafeList {
public static void main(String[] args) {
List<String> passengers = new ArrayList<>();
passengers.add("Grace");
passengers.add("Tomas");
// passengers.add(42); // compile error: int cannot be converted to String
String first = passengers.get(0); // no cast needed
System.out.println(first.toUpperCase() + " has " + first.length() + " letters");
}
}
GRACE has 5 letters
If you remove the // before passengers.add(42), the program won't even compile: incompatible types: int cannot be converted to String. Generics give you two wins: type safety (errors at compile time, not run time) and no casts (cleaner code).
📝 Note
A generic type used without angle brackets, like List names above, is called a raw type. It only still exists so old code keeps working. The compiler warns about it; never use raw types in new code.
How do you create a generic class?
Add a type parameter in angle brackets after the class name, then use it like any other type inside the class. Here's a gift box that can hold one item of any type:
public class GiftBoxes {
public static void main(String[] args) {
Box<String> card = new Box<>();
card.put("Happy birthday, Sam!");
String message = card.get();
System.out.println("Card says: " + message);
Box<Integer> voucher = new Box<>();
voucher.put(50);
int amount = voucher.get();
System.out.println("Voucher worth $" + amount);
Box<String> empty = new Box<>();
System.out.println("Empty box? " + empty.isEmpty());
}
}
class Box<T> {
private T item;
void put(T item) {
this.item = item;
}
T get() {
return item;
}
boolean isEmpty() {
return item == null;
}
}
Card says: Happy birthday, Sam!
Voucher worth $50
Empty box? true
Inside Box, T stands for "whatever type the user picks". For Box<String>, every T acts like String, so card.get() returns a String and card.put(50) would not compile. For Box<Integer>, T means Integer.
Naming conventions for type parameters
By convention, type parameters are single capital letters: T for "type", E for "element" (used by collections), K and V for "key" and "value" (used by maps), and R for "result".
A class with two type parameters
A class can have several type parameters, separated by commas. This Pair<K, V> holds two values that can be different types, here a city name and a temperature in degrees Celsius:
import java.util.ArrayList;
import java.util.List;
public class CityTemps {
public static void main(String[] args) {
List<Pair<String, Double>> readings = new ArrayList<>();
readings.add(new Pair<>("Reykjavik", 4.5));
readings.add(new Pair<>("Dubai", 38.0));
readings.add(new Pair<>("Sydney", 21.25));
for (Pair<String, Double> reading : readings) {
System.out.println(reading.getFirst() + ": " + reading.getSecond() + " C");
}
Pair<String, Integer> seat = new Pair<>("Row", 14);
System.out.println(seat);
}
}
class Pair<K, V> {
private final K first;
private final V second;
Pair(K first, V second) {
this.first = first;
this.second = second;
}
K getFirst() {
return first;
}
V getSecond() {
return second;
}
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
}
Reykjavik: 4.5 C
Dubai: 38.0 C
Sydney: 21.25 C
(Row, 14)
Notice List<Pair<String, Double>>: generic types can be nested. (For a simple data holder like this, a generic record works too: record Pair<K, V>(K first, V second) {}.)
The diamond operator
Since Java 7, you don't have to repeat the type arguments on the right side of new. The empty <>, nicknamed the diamond, tells the compiler to work them out from the variable's type:
Map<String, List<Integer>> scores = new HashMap<String, List<Integer>>(); // old style
Map<String, List<Integer>> scores = new HashMap<>(); // diamond
Don't drop the diamond completely: new HashMap() without <> creates a raw type.
How do you write a generic method?
A single method can be generic too, even inside a normal class. Put the type parameter in angle brackets before the return type:
import java.util.ArrayList;
import java.util.List;
public class GenericMethods {
public static void main(String[] args) {
List<String> queue = List.of("Ines", "Kwame", "Lucia");
List<Integer> lapTimes = List.of(61, 58, 60);
String nextCustomer = firstOf(queue);
int firstLap = firstOf(lapTimes);
System.out.println("Next customer: " + nextCustomer);
System.out.println("First lap: " + firstLap + "s");
List<String> cheers = repeat("Go!", 3);
System.out.println(cheers);
}
static <T> T firstOf(List<T> list) {
return list.get(0);
}
static <T> List<T> repeat(T item, int times) {
List<T> result = new ArrayList<>();
for (int i = 0; i < times; i++) {
result.add(item);
}
return result;
}
}
Next customer: Ines
First lap: 61s
[Go!, Go!, Go!]
The <T> in static <T> T firstOf(List<T> list) declares the type parameter, and the next T is the return type. You never write the type when calling the method: Java infers it from the arguments, so firstOf(queue) returns a String and firstOf(lapTimes) returns an Integer.
Bounded type parameters: <T extends Number>
Sometimes "any type" is too broad. To average a list, you need to call doubleValue(), which only numbers have. A bounded type parameter limits T to a class and its subclasses (or to types implementing an interface):
import java.util.List;
public class BoundedTypes {
public static void main(String[] args) {
List<Integer> goals = List.of(2, 0, 3, 1);
List<Double> prices = List.of(4.50, 12.00, 7.50);
System.out.println("Average goals: " + average(goals));
System.out.println("Average price: " + average(prices));
System.out.println("Latest name: " + maxOf(List.of("Mia", "Zoe", "Ava")));
System.out.println("Highest score: " + maxOf(goals));
}
static <T extends Number> double average(List<T> numbers) {
double sum = 0;
for (T number : numbers) {
sum += number.doubleValue(); // allowed because T is a Number
}
return sum / numbers.size();
}
static <T extends Comparable<T>> T maxOf(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
}
Average goals: 1.5
Average price: 8.0
Latest name: Zoe
Highest score: 3
<T extends Number> means "T can be Integer, Double, Long or any other Number", so calling average(List.of("a", "b")) won't compile. In bounds, extends is used for both classes and interfaces, as in <T extends Comparable<T>>, which means "T can be compared with other Ts". That's why maxOf works for both names and numbers.
Wildcards: ? extends and ? super
Here's a surprise: a List<Integer> is not a List<Number>, even though Integer is a Number. If it were, someone could add a Double to your list of Integers through the List<Number> variable. To write methods that accept related list types, Java uses the wildcard ?, meaning "some type":
List<? extends Number>: a list of some kind of Number. You can safely read Numbers out of it, but you can't add to it.List<? super Integer>: a list that can hold Integers (aList<Integer>,List<Number>orList<Object>). You can safely add Integers to it.
import java.util.ArrayList;
import java.util.List;
public class Wildcards {
public static void main(String[] args) {
List<Integer> tips = List.of(5, 10, 3);
List<Double> fares = List.of(12.5, 8.75);
System.out.println("Tips total: " + total(tips));
System.out.println("Fares total: " + total(fares));
List<Number> mixed = new ArrayList<>();
mixed.add(2.5);
addStarterScores(mixed);
System.out.println("Number list: " + mixed);
List<Object> anything = new ArrayList<>();
addStarterScores(anything);
System.out.println("Object list: " + anything);
}
// Producer: we only READ numbers out of the list
static double total(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) {
sum += n.doubleValue();
}
return sum;
}
// Consumer: we only PUT Integers into the list
static void addStarterScores(List<? super Integer> target) {
target.add(0);
target.add(10);
}
}
Tips total: 18.0
Fares total: 21.25
Number list: [2.5, 0, 10]
Object list: [0, 10]
An easy way to remember which to use is PECS: Producer Extends, Consumer Super. If the list produces values for you to read, use extends. If it consumes values you put in, use super. If you do both, use an exact type like List<Integer>. A plain List<?> means "a list of anything", useful when you only need methods like size().
Type erasure: generics disappear at run time
Generics are a compile-time feature. After the compiler checks your types, it removes them, a process called type erasure. At run time, a List<String> and a List<Integer> are both just ArrayLists:
import java.util.ArrayList;
import java.util.List;
public class Erasure {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
System.out.println(names.getClass().getName());
System.out.println(numbers.getClass().getName());
System.out.println("Same class at run time? " + (names.getClass() == numbers.getClass()));
}
}
java.util.ArrayList
java.util.ArrayList
Same class at run time? true
Erasure explains a few rules you may bump into: you can't write new T() or new T[10], you can't check list instanceof List<String>, and primitives can't be type arguments. None of this affects everyday use; it just means the safety checks happen when you compile.
Common mistakes with generics
1. Using raw types
List items = new ArrayList(); throws away type checking. Always add a type argument: List<String>.
2. Using a primitive as a type argument
Box<int> doesn't compile. Use Box<Integer>.
3. Expecting List<Integer> to be a List<Number>
Passing a List<Integer> to a method that takes List<Number> fails to compile. Make the parameter List<? extends Number>.
4. Adding to a ? extends list
numbers.add(5) on a List<? extends Number> won't compile, because the list might really be a List<Double>. Use ? super Integer when you need to add.
5. Putting <T> in the wrong place on a method
The type parameter goes before the return type: static <T> T firstOf(...), not static T firstOf<T>(...).
Practice exercise
🏋️ Try it yourself
Airlines limit how many items fit in a carry-on. Write a generic class Bag<T> with a capacity set in the constructor. Its add(T item) method should return true if the item fits and false if the bag is full. Add get(int index) and size(). Test it with a Bag<String> of capacity 2 and a Bag<Integer>.
Show solution
import java.util.ArrayList;
import java.util.List;
public class LimitedBag {
public static void main(String[] args) {
Bag<String> carryOn = new Bag<>(2);
System.out.println("Add laptop: " + carryOn.add("laptop"));
System.out.println("Add book: " + carryOn.add("book"));
System.out.println("Add drone: " + carryOn.add("drone"));
System.out.println("Items: " + carryOn.size());
Bag<Integer> coins = new Bag<>(3);
coins.add(25);
coins.add(10);
System.out.println("Coins: " + coins.size() + ", first: " + coins.get(0));
}
}
class Bag<T> {
private final List<T> items = new ArrayList<>();
private final int capacity;
Bag(int capacity) {
this.capacity = capacity;
}
boolean add(T item) {
if (items.size() >= capacity) {
return false;
}
items.add(item);
return true;
}
T get(int index) {
return items.get(index);
}
int size() {
return items.size();
}
}
Add laptop: true
Add book: true
Add drone: false
Items: 2
Coins: 2, first: 25
The bag stores its items in a List<T>, so the same class safely holds Strings in one bag and Integers in another.
What's next?
You've written a generic maxOf that relies on Comparable. Next, put that idea to work and learn to sort lists of numbers, Strings and your own objects in any order you like: Java sorting collections.
Key takeaways
- Generics add type parameters such as <T> so one class or method works with many types safely.
- They catch type mistakes at compile time and remove the need for casts.
- Write a generic class as class Box<T> and a generic method as static <T> T firstOf(List<T> list).
- The diamond operator <> lets Java infer type arguments: new ArrayList<>().
- Bounded types like <T extends Number> restrict T and let you call that type's methods.
- Use ? extends for lists you read from and ? super for lists you add to (PECS: Producer Extends, Consumer Super).
- Type erasure removes generic types after compiling, so List<String> and List<Integer> share one class at run time.
Frequently asked questions
What are generics in Java?
Generics let you write classes, interfaces and methods with type parameters, such as List<E> or Box<T>. The caller picks the actual type, and the compiler checks that only values of that type are used, so no casts are needed.
Why do we use generics in Java?
For type safety and cleaner code. Generics catch mistakes like adding a number to a list of names at compile time instead of crashing at run time, and they remove the need to cast values when you read them.
What does T mean in Java generics?
T is a type parameter, a placeholder for a real type that is chosen when the class or method is used. By convention T means type, E means element, K means key and V means value.
What is the difference between ? extends and ? super?
List<? extends Number> accepts lists of Number or any subclass and is safe to read from. List<? super Integer> accepts lists of Integer or any superclass and is safe to add Integers to. Remember PECS: Producer Extends, Consumer Super.
What is type erasure in Java?
Type erasure means the compiler removes generic type information after checking it. At run time a List<String> and a List<Integer> are both plain ArrayList objects, which is why you cannot write new T() or test instanceof List<String>.