Java course menu
Java Generics: Generic Classes, Generic Methods, Bounds and Wildcards
Learn Java generics: why type parameters make code type-safe, generic classes like Box<T> and Pair<K, V>, generic methods, bounded types with extends, wildcards (? extends and ? super), the PECS rule, type erasure, and common restrictions.
On this page
Every time you write List<String> or Map<String, Integer>, you are using generics. Generics let a class or method work with any type, while the compiler still checks that you use the types consistently. You get reusable code and type safety.
Why generics exist#
Before generics (Java 5), collections held plain Objects. You had to cast when reading, and mistakes only showed up at run time. With generics, the compiler catches them:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Asha");
// names.add(42); // compile error: int is not a String
String first = names.get(0); // no cast needed
System.out.println(first.toUpperCase());
}
}ASHAA generic class#
Declare one or more type parameters in angle brackets after the class name. By convention they are single capital letters: T (type), E (element), K and V (key and value):
class Box<T> {
private T value;
Box(T value) {
this.value = value;
}
T get() {
return value;
}
void set(T value) {
this.value = value;
}
}
public class Main {
public static void main(String[] args) {
Box<String> text = new Box<>("hello");
Box<Integer> number = new Box<>(42);
System.out.println(text.get().length() + " " + (number.get() + 8));
// text.set(5); // compile error: Box<String> only holds Strings
}
}5 50Box<String> and Box<Integer> share one class definition, and each is checked for its own type.
Several type parameters#
record Pair<K, V>(K key, V value) {
Pair<V, K> swap() {
return new Pair<>(value, key);
}
}
public class Main {
public static void main(String[] args) {
Pair<String, Integer> p = new Pair<>("age", 21);
Pair<Integer, String> swapped = p.swap();
System.out.println(p + " " + swapped);
}
}Pair[key=age, value=21] Pair[key=21, value=age]Records, classes and interfaces can all be generic.
Generic methods#
A method can declare its own type parameter, written before the return type. The compiler infers it from the arguments:
import java.util.List;
public class Main {
static <T> T firstOrDefault(List<T> list, T fallback) {
return list.isEmpty() ? fallback : list.get(0);
}
static <T> void printAll(List<T> items) {
for (T item : items) {
System.out.print(item + " ");
}
System.out.println();
}
public static void main(String[] args) {
System.out.println(firstOrDefault(List.of("x", "y"), "none"));
System.out.println(firstOrDefault(List.<Integer>of(), -1));
printAll(List.of(1, 2, 3));
printAll(List.of("a", "b"));
}
}x
-1
1 2 3
a b Bounded type parameters#
<T extends Something> restricts T to types that are (or extend/implement) Something, which lets the method use that type's methods:
import java.util.List;
public class Main {
static <T extends Comparable<T>> T largest(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
public static void main(String[] args) {
System.out.println(largest(List.of(3, 9, 4)));
System.out.println(largest(List.of("pear", "apple", "plum")));
System.out.println(sum(List.of(1, 2, 3)) + " " + sum(List.of(1.5, 2.5)));
}
}9
plum
6.0 4.0Without the bound, item.compareTo(best) would not compile, because an arbitrary T has no compareTo.
Wildcards: ? extends and ? super#
A List<Integer> is not a List<Number>, even though Integer is a Number. (If it were, you could add a Double to a list of integers through the List<Number> reference.) Wildcards make methods flexible about this:
List<? extends Number>: a list of some subtype ofNumber. You can readNumbers from it, but not add to it. It is a producer.List<? super Integer>: a list ofIntegeror a supertype. You can addIntegers to it. It is a consumer.
import java.util.ArrayList;
import java.util.List;
public class Main {
static void addNumbers(List<? super Integer> target, int count) {
for (int i = 1; i <= count; i++) {
target.add(i);
}
}
public static void main(String[] args) {
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers, 3);
addNumbers(objects, 2);
System.out.println(numbers + " " + objects);
}
}[1, 2, 3] [1, 2]The rule of thumb is PECS: Producer Extends, Consumer Super. Collections.copy(List<? super T> dest, List<? extends T> src) follows it exactly.
Type erasure and restrictions#
Generics are checked by the compiler and then erased: at run time, a List<String> and a List<Integer> are both just List. That design kept compatibility with older code, and it explains the main restrictions:
- Type arguments must be reference types:
List<int>is not allowed; useList<Integer>. - You cannot create
new T()ornew T[10]. - You cannot check
obj instanceof List<String>(onlyinstanceof List<?>). - Static fields cannot use the class's type parameter.
Common mistakes#
Practice#
Practice exercise
Write a generic method <T> int countMatches(List<T> items, T target) that counts elements equal to target (using equals), and test it with a list of integers and a list of strings.
Show solution
import java.util.List;
public class CountMatches {
static <T> int countMatches(List<T> items, T target) {
int count = 0;
for (T item : items) {
if (item.equals(target)) count++;
}
return count;
}
public static void main(String[] args) {
System.out.println(countMatches(List.of(3, 7, 3, 3, 9), 3));
System.out.println(countMatches(List.of("to", "be", "or", "not", "to", "be"), "be"));
}
}3
2Collections.frequency(items, target) does the same in the standard library.
Key takeaway#
Finished this lesson?