jario_
Zur Übersicht

Kapitel 12

Generics und modernes Java

ArrayList<String> kennst du schon: Die Liste weiß, dass nur Texte hineinkommen. In diesem Kapitel schreibst du solche generischen Klassen und Methoden selbst. Dazu kommen Werkzeuge aus neueren Java-Versionen, die Code kürzer und sicherer machen: var, Pattern Matching, Optional und sealed.

var und instanceof

Mit var lässt du den Typ einer lokalen Variablen weg. Java leitet ihn aus dem Wert rechts vom = ab: var names = new ArrayList<String>(); hat den Typ ArrayList<String>. Der Typ steht danach genauso fest wie sonst, er steht nur nicht da. Bei var gehört der Typ in die spitzen Klammern rechts. var geht auch in der for-each-Schleife, aber nicht für Attribute und Parameter.

Eine Variable vom Typ Object kann alles enthalten. instanceof prüft, was drinsteckt, und gibt dir gleich eine passende Variable: In if (value instanceof String s) ist s im if-Block ein String, ohne Umwandlung.

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));
        }
    }
}

Generische Klassen und Methoden

class Box<T> hat einen Typparameter T. In der Klasse benutzt du T wie einen normalen Typ. Erst beim Anlegen steht fest, was T ist: In Box<String> ist es String, in Box<Integer> ist es Integer. So schreibst du die Box einmal, und get() liefert trotzdem den richtigen Typ. Records können auch Typparameter haben, sogar mehrere: record Pair<A, B>(A first, B second).

Auch eine einzelne Methode kann einen Typparameter haben. Er steht vor dem Rückgabetyp:

public static <T extends Comparable<T>> T smallest(List<T> list)

T extends Comparable<T> heißt: T muss vergleichbar sein. Nur dann darfst du in der Methode compareTo aufrufen. Die Methode funktioniert mit Zahlen, Texten und jeder eigenen Klasse, die Comparable umsetzt.

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

Was gibt eine Suche zurück, die nichts findet? null vergisst man leicht zu prüfen. Ein Optional<T> macht im Typ sichtbar, dass vielleicht nichts da ist. Es enthält entweder einen Wert (Optional.of(x)) oder ist leer (Optional.empty()).

  • isPresent() prüft, ob ein Wert da ist
  • orElse(fallback) liefert den Wert oder den Ersatz
  • map(...) wandelt den Inhalt um, wenn einer da ist; leer bleibt leer
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 und switch mit Patterns

sealed interface Payment permits Cash, Card, Voucher legt fest: Es gibt genau diese drei Arten von Payment, keine weiteren. Dann kann ein switch direkt nach der Art entscheiden:

  • case Voucher v -> prüft die Art und gibt dir das Objekt als v
  • case Cash(double amount) -> packt einen Record gleich aus (Record-Pattern)
  • when hängt eine Bedingung an: case Cash(double amount) when amount >= 100 ->

Weil der Compiler alle erlaubten Arten kennt, braucht der switch kein default. Fehlt ein Fall, meldet er es. Wie bei else if gewinnt der erste passende Fall. Der speziellere mit when muss deshalb zuerst kommen.

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")));
    }
}

Typische Fehler

  • var ohne Wert

    var count; oder var x = null; meldet der Compiler mit cannot infer type, weil rechts nichts steht, woraus er den Typ ablesen kann.

  • Box<int>

    Auch bei eigenen generischen Klassen sind nur Klassen erlaubt: Box<Integer>, nicht Box<int>. Das kennst du schon von List<Integer>.

  • compareTo ohne Schranke

    Steht nur <T> vor der Methode, weiß Java nichts über T. element.compareTo(best) meldet dann cannot find symbol. Erst <T extends Comparable<T>> erlaubt den Aufruf.

  • Allgemeiner Fall vor dem speziellen

    Steht case Cash c -> vor case Cash(double amount) when amount >= 100 ->, wird der zweite Fall nie erreicht. Der Compiler meldet this case label is dominated by a preceding case label.