Java Advanced: Generics, Streams and Concurrency
Generics
In this lesson you will
- Write generic classes and methods
- Restrict type parameters with bounds
- Use wildcards and explain type erasure
Introduction
You have used List<String> and Map<String, Integer>. The angle brackets are generics: a way to write code once and have it work safely with many types. In this lesson you will write your own generic code instead of just using it.
The problem generics solve
Before generics (Java 5), a container held plain Objects, so you had to cast and the compiler could not help:
Java
import java.util.ArrayList;
import java.util.List;
public class Main {
@SuppressWarnings({"rawtypes", "unchecked"})
public static void main(String[] args) {
List names = new ArrayList(); // "raw" list: holds any Object
names.add("Asha");
names.add(42); // compiles, but is a bug
for (Object o : names) {
String s = (String) o; // fails for 42
System.out.println(s);
}
}
}AshaAt runtime the program prints Asha and then crashes with a ClassCastException when it reaches 42. With generics the mistake becomes a compile-time error.
Java
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);
}
}Main.java:8: error: incompatible types: int cannot be converted to String
names.add(42);
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 errorQuick quiz
What is the main benefit of generics?
A generic class
Put a type parameter in angle brackets after the class name. By convention it is a single capital letter: T (type), E (element), K (key), V (value).
Java
class Box<T> {
private T content;
Box(T content) {
this.content = content;
}
T get() {
return content;
}
void set(T content) {
this.content = content;
}
}
public class Main {
public static void main(String[] args) {
Box<String> words = new Box<>("hello");
Box<Integer> numbers = new Box<>(42);
String s = words.get(); // no cast needed
int n = numbers.get();
System.out.println(s.toUpperCase());
System.out.println(n + 1);
}
}HELLO
43Box<String> and Box<Integer> are different types, and the compiler will not let you mix them. Two or more parameters are fine too:
Java
class Pair<K, V> {
private final K key;
private final V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
K getKey() { return key; }
V getValue() { return value; }
@Override
public String toString() {
return key + "=" + value;
}
}
public class Main {
public static void main(String[] args) {
Pair<String, Integer> age = new Pair<>("Asha", 14);
System.out.println(age);
System.out.println(age.getValue() + 1);
}
}Asha=14
15Generic methods
A method can have its own type parameter, written before the return type. Java works the type out from the arguments.
Java
import java.util.List;
public class Main {
static <T> void printAll(List<T> items) {
for (T item : items) {
System.out.println("- " + item);
}
}
static <T> T firstOf(List<T> items) {
return items.get(0);
}
public static void main(String[] args) {
printAll(List.of("a", "b"));
printAll(List.of(1, 2, 3));
String s = firstOf(List.of("x", "y"));
System.out.println(s);
}
}- a
- b
- 1
- 2
- 3
xBounded type parameters
What if you want to find the largest item in a list? You need to compare items, so T must be comparable. A bound with extends restricts T.
Java
import java.util.List;
public class Main {
static <T extends Comparable<T>> T max(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
public static void main(String[] args) {
System.out.println(max(List.of(3, 9, 4)));
System.out.println(max(List.of("pear", "apple", "zebra")));
}
}9
zebraT extends Comparable<T> means "any type that can be compared with itself". Integers and Strings qualify. (Here extends is used even for interfaces.) Calling max with a type that is not comparable is a compile error.
Quick quiz
What does <T extends Comparable<T>> do?
Wildcards
Surprisingly, 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. A wildcard ? solves the reading case.
Java
import java.util.List;
public class Main {
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(sum(List.of(1, 2, 3)));
System.out.println(sum(List.of(1.5, 2.5)));
}
}6.0
4.0List<? extends Number>: a list of Number or any subtype. You can read Numbers from it, but not add to it.List<? super Integer>: a list of Integer or any supertype. You can add Integers to it.List<?>: a list of anything.
A memory aid is PECS: Producer Extends, Consumer Super. If a collection produces values for you, use extends. If it consumes values you give it, use super.
Java
import java.util.ArrayList;
import java.util.List;
public class Main {
static void addNumbers(List<? super Integer> target) {
target.add(1);
target.add(2);
}
public static void main(String[] args) {
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers);
addNumbers(objects);
System.out.println(numbers);
System.out.println(objects);
}
}[1, 2]
[1, 2]Type erasure
Generics are checked by the compiler, and then the type information is mostly erased, so the JVM sees plain Box and List. This was done to stay compatible with older code. It has consequences:
- You cannot write
new T()ornew T[10]. - You cannot check
obj instanceof List<String>, onlyobj instanceof List<?>. - You cannot use primitives as type arguments:
List<int>is invalid, so you writeList<Integer>and Java converts automatically (boxing).
Java
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());
}
}trueAt runtime both are just ArrayList. The generic types live only in your source code and the compiler's checks.
Try it yourself
On your computer, write a generic method static <T> void swap(T[] array, int i, int j) that swaps two elements of an array, and test it with an array of Strings and an array of Integers. Then write a generic class Stack<T> with push, pop and isEmpty backed by an ArrayList<T>.
Summary
- Generics move type checking to compile time and remove casts.
- Write
class Box<T>for a generic class and<T> void method()for a generic method. - Bounds (
T extends Comparable<T>) limit what T can be. Wildcards (? extends,? super) allow flexible parameters (PECS). - Generic types are erased at runtime, so no
new T()and no primitives as type arguments.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading