Chapter 12
Generics and modern Java
You already know ArrayList<String>: the list knows that only strings go in. In this chapter you write such generic classes and methods yourself. On top of that come tools from newer Java versions that make code shorter and safer: var, pattern matching, Optional and sealed.
var and instanceof
With var you leave out the type of a local variable. Java infers it from the value right of the =: var names = new ArrayList<String>(); has the type ArrayList<String>. The type is just as fixed as before, it's just not written out. With var, the type goes in the angle brackets on the right. var also works in a for-each loop, but not for fields and parameters.
A variable of type Object can hold anything. instanceof checks what's inside and gives you a matching variable right away: in if (value instanceof String s), s is a String inside the if block, without a cast.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static String describe(Object value) {
if (value instanceof String s) {
return "Text with " + s.length() + " characters";
} else if (value instanceof Integer i) {
return "Integer, doubled " + (i * 2);
}
return "Something else";
}
public static void main(String[] args) {
var names = new ArrayList<String>();
names.add("Ada");
names.add("Linus");
for (var name : names) {
System.out.println(name.toUpperCase());
}
var count = names.size();
System.out.println("Count: " + count);
List<Object> things = List.of("hello", 21, 3.5);
for (var thing : things) {
System.out.println(describe(thing));
}
}
}Generic classes and methods
class Box<T> has a type parameter T. Inside the class, you use T like a normal type. Only when you create a box is it decided what T is: in Box<String> it's String, in Box<Integer> it's Integer. That way you write the box once, and get() still returns the right type. Records can have type parameters too, even several: record Pair<A, B>(A first, B second).
A single method can also have a type parameter. It goes before the return type:
public static <T extends Comparable<T>> T smallest(List<T> list)
T extends Comparable<T> means: T must be comparable. Only then may you call compareTo in the method. The method works with numbers, strings and any class of your own that implements Comparable.
import java.util.List;
class Box<T> {
private T content;
public void put(T content) {
this.content = content;
}
public T get() {
return content;
}
}
record Pair<A, B>(A first, B second) {
}
public class Main {
public static <T extends Comparable<T>> T smallest(List<T> list) {
T best = list.get(0);
for (T element : list) {
if (element.compareTo(best) < 0) {
best = element;
}
}
return best;
}
public static void main(String[] args) {
Box<String> word = new Box<>();
word.put("Java");
String w = word.get();
System.out.println(w.length());
Box<Integer> number = new Box<>();
number.put(7);
int n = number.get();
System.out.println(n + 1);
Pair<String, Integer> entry = new Pair<>("Ada", 36);
System.out.println(entry);
System.out.println(smallest(List.of(5, 2, 9)));
System.out.println(smallest(List.of("pear", "apple", "fig")));
}
}Optional
What should a search return when it finds nothing? It's easy to forget to check for null. An Optional<T> makes it visible in the type that there might be nothing. It either contains a value (Optional.of(x)) or is empty (Optional.empty()).
isPresent()checks whether there's a valueorElse(fallback)returns the value or the fallbackmap(...)transforms the content if there is one; empty stays empty
import java.util.List;
import java.util.Optional;
public class Main {
public static Optional<String> firstStartingWith(List<String> words, char letter) {
for (String w : words) {
if (w.charAt(0) == letter) {
return Optional.of(w);
}
}
return Optional.empty();
}
public static void main(String[] args) {
List<String> fruits = List.of("apple", "banana", "cherry");
Optional<String> b = firstStartingWith(fruits, 'b');
Optional<String> z = firstStartingWith(fruits, 'z');
System.out.println(b.isPresent());
System.out.println(b.orElse("none"));
System.out.println(z.orElse("none"));
System.out.println(b.map(String::length).orElse(0));
System.out.println(z.map(String::length).orElse(0));
}
}sealed and switch with patterns
sealed interface Payment permits Cash, Card, Voucher states: there are exactly these three kinds of Payment, no others. Then a switch can decide directly by kind:
case Voucher v ->checks the kind and gives you the object asvcase Cash(double amount) ->unpacks a record right away (record pattern)whenadds a condition:case Cash(double amount) when amount >= 100 ->
Because the compiler knows all permitted kinds, the switch needs no default. If a case is missing, it reports it. As with else if, the first matching case wins. So the more specific one with when must come first.
sealed interface Payment permits Cash, Card, Voucher {
}
record Cash(double amount) implements Payment {
}
record Card(double amount, String bank) implements Payment {
}
record Voucher(String code) implements Payment {
}
public class Main {
public static String describe(Payment p) {
return switch (p) {
case Cash(double amount) when amount >= 100 -> "Large cash payment: " + amount;
case Cash(double amount) -> "Cash: " + amount;
case Card(double amount, String bank) -> "Card from " + bank + ": " + amount;
case Voucher v -> "Voucher " + v.code();
};
}
public static void main(String[] args) {
System.out.println(describe(new Cash(20)));
System.out.println(describe(new Cash(150)));
System.out.println(describe(new Card(35.5, "Sparkasse")));
System.out.println(describe(new Voucher("SUMMER10")));
}
}Common mistakes
var without a value
The compiler rejects
var count;orvar x = null;withcannot infer type, because there's nothing on the right to read the type from.Box<int>
Your own generic classes also only take classes:
Box<Integer>, notBox<int>. You know this fromList<Integer>.compareTo without a bound
If there's only
<T>before the method, Java knows nothing aboutT.element.compareTo(best)then reportscannot find symbol. Only<T extends Comparable<T>>allows the call.General case before the specific one
If
case Cash c ->comes beforecase Cash(double amount) when amount >= 100 ->, the second case is never reached. The compiler reportsthis case label is dominated by a preceding case label.