record, switch und Muster
Kurz gesagt: Ein Record ist eine Datenklasse, für die Java den Konstruktor, equals, hashCode und toString selbst schreibt, und ein switch mit Mustern wählt nach dem Typ und den Feldern des Objekts.
Problem
Der Platz ist belegt, aber die HashMap findet ihn nicht
Reservierungen im Kino: Der Schlüssel ist der Platz (Reihe und Nummer), der Wert ist der Name der Person.
class Seat { private int row; private int number;
public Seat(int row, int number) { this.row = row; this.number = number; }}Map<Seat, String> reservations = new HashMap<>();reservations.put(new Seat(5, 12), "Maria");
System.out.println(reservations.get(new Seat(5, 12)));System.out.println(reservations.containsKey(new Seat(5, 12)));nullfalseget gibt null zurück. Warum, weißt du noch aus der Lektion über Hashcodes: new Seat(5, 12) zweimal geschrieben sind zwei verschiedene Objekte, und Seat sagt nicht, wann zwei davon gleich sind. Die Lösung sind equals und hashCode, dazu toString für die Ausgabe und Getter. Rund 30 Zeilen für eine Klasse, die nur zwei Zahlen trägt.Die Idee
record: eine Klasse für Daten
record Seat(int row, int number) {}Map<Seat, String> reservations = new HashMap<>();reservations.put(new Seat(5, 12), "Maria");
System.out.println(reservations.get(new Seat(5, 12)));System.out.println(reservations.containsKey(new Seat(5, 12)));
Seat seat = new Seat(5, 12);System.out.println(seat);System.out.println("Reihe " + seat.row() + ", Platz " + seat.number());MariatrueSeat[row=5, number=12]Reihe 5, Platz 12Ein record ist eine Klasse, deren Aufgabe es ist, Daten zu tragen. In den Klammern stehen seine Komponenten, und Java schreibt für dich:
- ein privates
final-Feld für jede Komponente; - einen Konstruktor mit allen Komponenten;
- eine Methode, die jedes Feld liest, mit demselben Namen:
row(), nichtgetRow(); equalsundhashCode, die die Felder vergleichen. Deshalb findetgetMaria jetzt;toStringin der FormSeat[row=5, number=12].
Setter gibt es nicht. Ein Objekt eines record ändert sich nach dem Erzeugen nicht.
Map<Seat, String> reservations = new HashMap<>();reservations.put(new Seat(5, 12), "Maria");
System.out.println(reservations.get(new Seat(5, 12)));System.out.println(reservations.containsKey(new Seat(5, 12)));
Seat seat = new Seat(5, 12);System.out.println(seat);System.out.println("Reihe " + seat.row() + ", Platz " + seat.number());Schritt für Schritt, in Worten
- Eine leere Map: Der Schlüssel ist ein Kinoplatz, der Wert ist ein Name.
- put legt den Schlüssel Seat(5, 12) mit dem Wert „Maria“ an.
- get bekommt ein neues Seat-Objekt. Das ist ein anderes Objekt im Speicher, nicht der Schlüssel aus put.
- Der hashCode eines Records wird aus den Feldern berechnet, deshalb ist er gleich: Die Suche geht an dieselbe Stelle.
- equals eines Records vergleicht Feld für Feld: Reihe 5 und 5, Platz 12 und 12. Sie sind gleich, und get gibt „Maria“ zurück.
- containsKey macht dasselbe mit einem weiteren neuen Seat-Objekt: true.
- Auch toString und die Methoden row() und number() schreibt der Record selbst.
Genauer, für Neugierige
- Ein
recordistfinal: Man kann nicht von ihm erben. Er erbt implizit vonjava.lang.Recordund kann von keiner anderen Klasse erben, aber er kann Interfaces implementieren. - Er kann eigene Methoden, statische Felder und statische Methoden haben. Objektfelder außerhalb der Komponenten darf er nicht haben:
record Cart(List<String> items) { private int count;}Main.java:4: error: field declaration must be static private int count; ^ (consider replacing field with record component)- Die Unveränderlichkeit reicht nur eine Ebene tief. Wenn eine Komponente eine
Listist, zeigt das Feld immer auf dieselbe Liste, aber die Liste selbst kann sich ändern. - Im Modul „Collections“ hast du
equalsundhashCodevon Hand geschrieben. Mitrecordhast du sie schon. Wenn du eine andere Regel für die Gleichheit brauchst, kannst du sie trotzdem überschreiben.
Die Idee
Prüfungen und Methoden in einem Record
record Price(double amount, String currency) { Price { if (amount < 0) { throw new IllegalArgumentException("Der Preis darf nicht negativ sein: " + amount); } }
Price withDiscount(double percent) { return new Price(amount * (1 - percent / 100), currency); }}Price coffee = new Price(18.50, "EUR");Price sale = coffee.withDiscount(20);System.out.println(coffee);System.out.println(sale);
try { new Price(-5, "EUR");} catch (IllegalArgumentException e) { System.out.println(e.getMessage());}Price[amount=18.5, currency=EUR]Price[amount=14.8, currency=EUR]Der Preis darf nicht negativ sein: -5.0- Im kompakten Konstruktor sind
amountundcurrencydie Parameter. Du schreibst nichtthis.amount = amount: Das passiert erst nach der Prüfung. withDiscountändert den Preis nicht. Es gibt einen neuenPricezurück. So arbeitest du mit Objekten, die sich nicht ändern:coffeebleibt bei 18,50 €.
Probier es aus
Sind sie gleich?
Seat ist ein record, wie in der Karte davor.Was gibt das Programm aus?
Seat a = new Seat(5, 12);Seat b = new Seat(5, 12);System.out.println(a == b);System.out.println(a.equals(b));System.out.println(a.hashCode() == b.hashCode());a und b sind zwei Objekte, deshalb ist == false: Es vergleicht die Pfeile. Das equals eines record vergleicht die Felder, und die sind gleich. Auch hashCode wird aus den Feldern berechnet, also ist es ebenfalls gleich. Genau das will die HashMap.Probier es aus
Die Lieferung wird zu teuer
Die Lieferung in eine Filiale kostet 4,50 €, aber das Programm sagt 6,90 €. In welcher Zeile ist der Fehler?
Wähle die Zeile mit dem Fehler.
price = 4.50; steht kein break. Der alte switch hält am Ende des Falls nicht an, sondern fällt durch in den nächsten: Er führt auch price = 6.90; aus und kommt erst dann zum break. Die Lösung ist ein break nach Zeile 5. Noch besser ist ein switch ganz ohne Durchfallen. Das ist die nächste Karte.Die Idee
Ein switch, der einen Wert zurückgibt
switch ein Ausdruck sein: Er liefert einen Wert, den du in einer Variable speicherst oder zurückgibst.String method = "filiale";double price = switch (method) { case "filiale" -> 4.50; case "kurier", "adresse" -> 6.90; case "laden" -> 0; default -> throw new IllegalArgumentException("Unbekannte Lieferart: " + method);};System.out.println("Lieferung: " + price + " €");Lieferung: 4.5 €- Nach
->kommt der Wert des Falls. Es gibt kein Durchfallen, und du schreibst keinbreak. - Ein Fall kann mehrere Werte haben:
case "kurier", "adresse". - Ein Fall kann eine Exception werfen, wie bei
default. - Der Ausdruck muss für jede mögliche Eingabe einen Wert liefern. Bei Text gibt es unendlich viele Eingaben, deshalb ist
defaultPflicht. Ohne es:
double price = switch (method) { case "filiale" -> 4.50; case "kurier" -> 6.90;};Main.java:4: error: the switch expression does not cover all possible input values double price = switch (method) { ^Wenn ein Fall mehrere Zeilen hat, kommt er in { }, und der Wert wird mit yield geliefert:
static double delivery(String method, double weightKg) { return switch (method) { case "filiale" -> 4.50; case "kurier" -> { double base = 6.90; yield weightKg > 10 ? base + 3 : base; } default -> 0; };}System.out.println(delivery("kurier", 4));System.out.println(delivery("kurier", 12));6.99.9Genauer, für Neugierige
- Pfeile kannst du auch in einem
switchschreiben, der keinen Wert zurückgibt, sondern nur etwas tut. Auch dann gibt es kein Durchfallen. yieldist wiereturn, aber für denswitch, nicht für die Methode.- Die alte Form
case "filiale": ... break;funktioniert weiterhin, und du wirst ihr in altem Code begegnen. Die beiden Formen werden in einemswitchnicht gemischt.
Die Idee
sealed: eine geschlossene Familie
Der Shop nimmt drei Zahlungsarten an: Karte, Banküberweisung und Nachnahme. Andere gibt es nicht. Das kannst du dem Compiler sagen:
sealed interface Payment permits CardPayment, BankTransfer, CashOnDelivery {}
record CardPayment(String cardNumber) implements Payment {}record BankTransfer(String iban) implements Payment {}record CashOnDelivery() implements Payment {}sealed bedeutet „versiegelt“. Nach permits kommt die Liste der Typen, die Payment implementieren dürfen. Ein Versuch mit einem vierten kompiliert nicht:
record Voucher(String code) implements Payment {}Main.java:6: error: class is not allowed to extend sealed class: Payment (as it is not listed in its 'permits' clause)record Voucher(String code) implements Payment {}^Die Untertypen sind hier Records, weil jede Zahlungsart nur Daten ist: eine Kartennummer, eine IBAN oder nichts.
Genauer, für Neugierige
- Jeder Untertyp eines
sealed-Typs istfinal,sealed(mit eigenempermits) odernon-sealed(wieder offen). Einrecordist per Definitionfinal. - Wenn alle Untertypen in derselben Datei stehen, kann
permitswegfallen. sealedkann auch eine abstrakte Klasse sein:sealed abstract class Shape permits Circle, Square.
Sieh es dir an
Muster in switch
switch den Typ des Objekts:static double fee(Payment payment, double amount) { return switch (payment) { case CardPayment card -> amount * 0.015; case BankTransfer transfer -> 0.50; case CashOnDelivery cash -> 2.00; };}System.out.println(fee(new CardPayment("4111222233334444"), 200));System.out.println(fee(new BankTransfer("DE89370400440532013000"), 200));System.out.println(fee(new CashOnDelivery(), 200));3.00.52.0case CardPayment card bedeutet „wenn das Objekt ein CardPayment ist, nenne es card“. Das ist instanceof mit Muster aus dem Modul „Vererbung und Polymorphie“, aber für viele Typen auf einmal.
Ein default gibt es nicht. Der Compiler kennt die drei Untertypen und prüft, dass jeder abgedeckt ist. Wenn du einen vergisst, kompiliert der Code nicht:
static double fee(Payment payment, double amount) { return switch (payment) { case CardPayment card -> amount * 0.015; case BankTransfer transfer -> 0.50; };}Main.java:9: error: the switch expression does not cover all possible input values return switch (payment) { ^Das ist der Nutzen von sealed. Wenn du morgen eine vierte Zahlungsart hinzufügst, zeigt dir der Compiler jeden switch, der von ihr erfahren muss.
Ein Record-Muster holt die Felder direkt heraus, und when fügt dem Fall eine Bedingung hinzu:
static String describe(Payment payment) { return switch (payment) { case CardPayment(String number) -> "Karte ****" + number.substring(number.length() - 4); case BankTransfer(String iban) when iban.startsWith("DE") -> "Überweisung von einem deutschen Konto"; case BankTransfer(String iban) -> "Überweisung aus dem Ausland"; case CashOnDelivery() -> "Nachnahme"; };}System.out.println(describe(new CardPayment("4111222233334444")));System.out.println(describe(new BankTransfer("DE89370400440532013000")));System.out.println(describe(new BankTransfer("AT611904300234573201")));System.out.println(describe(new CashOnDelivery()));Karte ****4444Überweisung von einem deutschen KontoÜberweisung aus dem AuslandNachnahmeGenauer, für Neugierige
- Ein Record-Muster funktioniert auch mit
instanceof:
Payment payment = new CardPayment("4111222233334444");if (payment instanceof CardPayment(String number)) { System.out.println("Die Karte endet auf " + number.substring(12));}Die Karte endet auf 4444- Wenn das Objekt
nullist, wirft derswitcheineNullPointerException. Wennnulleine sinnvolle Eingabe ist, fügecase null -> ...hinzu. - Muster lassen sich verschachteln:
case Order(Customer(String name), double total).
Probier es aus
Welcher Fall gewinnt?
Was gibt das Programm aus?
static String label(Payment payment, double amount) { return switch (payment) { case CashOnDelivery cash when amount > 500 -> "Nachnahme: über 500 € nicht möglich"; case CashOnDelivery cash -> "Nachnahme"; case CardPayment card when amount > 1000 -> "Karte: Bestätigung nötig"; case CardPayment card -> "Karte"; case BankTransfer transfer -> "Überweisung"; };}
System.out.println(label(new CashOnDelivery(), 650));System.out.println(label(new CashOnDelivery(), 80));System.out.println(label(new CardPayment("4111222233334444"), 650));when und landen beim zweiten. Die Kartenzahlung beträgt 650 €, und eine Bestätigung braucht es erst über 1000 €, also gewinnt der Fall ohne Bedingung: „Karte“.Genauer, für Neugierige
Der Fall mit when muss vor dem allgemeinen für denselben Typ stehen. Andersherum kompiliert es nicht, weil der allgemeine Fall alles fängt und der mit when nie erreicht würde:
case CashOnDelivery cash -> "Nachnahme";case CashOnDelivery cash when amount > 500 -> "Nachnahme: über 500 € nicht möglich";Main.java:11: error: this case label is dominated by a preceding case label case CashOnDelivery cash when amount > 500 -> "Nachnahme: über 500 € nicht möglich"; ^Die Idee
Textblöcke und var
Text über mehrere Zeilen, wie ein Kassenbon oder JSON, ist mit " und \n in jeder Zeile schwer zu lesen. Seit Java 15 gibt es den Textblock zwischen """ und """:
String receipt = """ Der Eckladen Kaffeebohnen 18.50 € Honig 12.00 € Summe: 30.50 € """;System.out.print(receipt);
String json = """ {"customer": "%s", "total": %.2f} """.formatted("Maria", 75.5);System.out.print(json);Der EckladenKaffeebohnen 18.50 €Honig 12.00 €Summe: 30.50 €{"customer": "Maria", "total": 75.50}- Nach den öffnenden
"""beginnt der Text in einer neuen Zeile. - Die gemeinsame Einrückung links wird entfernt. Maßstab ist die Zeile, die am weitesten links steht, einschließlich der schließenden
""". - Anführungszeichen darin werden nicht maskiert.
formatted(...)füllt%sund%.2faus, wieString.format.
Und eine Erinnerung aus „Sprachgrundlagen“: var lässt den Compiler den Typ ermitteln. Am meisten spart es bei den langen Typen aus den Collections:
var prices = new LinkedHashMap<String, Double>();prices.put("Kaffee", 18.50);prices.put("Honig", 12.00);for (var entry : prices.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue() + " €");}Kaffee: 18.5 €Honig: 12.0 €var gibt es nur für lokale Variablen mit einem Wert. Der Typ wird einmal festgelegt und ändert sich nicht. Felder, Parameter und var x = null; sind nicht erlaubt.
Übung
Modernes Java
Löse die Aufgaben in deinem Editor.
1. Formen. Einsealed interface Shape mit drei record-Untertypen: Circle(double radius), Rectangle(double width, double height) und Triangle(double base, double height). Schreib area(Shape) mit einem switch und Record-Mustern, ohne default. Schreib auch name(Shape), das für ein Rechteck mit gleichen Seiten „Quadrat“ sagt (mit when). Gib einen Kreis mit Radius 1, ein Rechteck 2 mal 3, ein Quadrat 4 mal 4, ein Dreieck mit Grundseite 3 und Höhe 5 und die Gesamtfläche aus.Kreis: 3.14Rechteck: 6.00Quadrat: 16.00Dreieck: 7.50Gesamt: 32.64sealed interface Delivery mit ToOffice(String office), ToAddress(String city, double weightKg) und Pickup(). Der Preis: in eine Filiale 4,50 €; an eine Adresse 5,90 € in Berlin und 6,90 € anderswo, plus 1 € für jedes Kilogramm über 5; Abholung im Laden 0 €. Gib jede Lieferung (mit dem toString des Records) und ihren Preis aus.ToOffice[office=Hamburg Zentrum] -> 4.50 €ToAddress[city=Berlin, weightKg=2.0] -> 5.90 €ToAddress[city=Hamburg, weightKg=8.0] -> 9.90 €Pickup[] -> 0.00 €record Point(int x, int y) und zähle mit einer LinkedHashMap<Point, Integer> und merge(p, 1, Integer::sum), wie oft er an jedem Punkt war. Schlag am Ende new Point(2, 3) in der Map nach.Point[x=2, y=3]: 3Point[x=5, y=1]: 2Point[x=0, y=0]: 1Besuche bei (2, 3): 3Weitere Aufgaben
- Ein Schüler als Record.
record Student(String name, double grade)mit einem kompakten Konstruktor, der eine Note außerhalb von 2 bis 6 ablehnt. Sortiere eine Liste von Schülern mitComparator.comparing(Student::grade). - Wochentag. Schreib die Aufgabe „Wochentag“ aus der Lektion „Bedingungen“ mit einem
switch-Ausdruck neu:case 1, 2, 3, 4, 5 -> "Werktag". - Ein Brief. Mach mit einem Textblock und
formattedeinen Brief, der eine Reservierung bestätigt: Name, Datum, Anzahl der Gäste und Preis in Euro.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: HashMap und HashSet · Hashcodes und Hashfunktionen · Lambdas · Streams
- Geht weiter in: Entwurfsmuster · Datenbanken · Observer · Factory Method und Simple Factory
- Aufgaben zum Thema: Modernes Java