Rheinwerk Computing < openbook >


 
Inhaltsverzeichnis
Materialien zum Buch
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Arrays und ihre Anwendungen
5 Der Umgang mit Zeichen und Zeichenketten
6 Eigene Klassen schreiben
7 Objektorientierte Beziehungsfragen
8 Schnittstellen, Aufzählungen, versiegelte Klassen, Records
9 Ausnahmen müssen sein
10 Geschachtelte Typen
11 Besondere Typen der Java SE
12 Generics<T>
13 Lambda-Ausdrücke und funktionale Programmierung
14 Architektur, Design und angewandte Objektorientierung
15 Java Platform Module System
16 Die Klassenbibliothek
17 Einführung in die nebenläufige Programmierung
18 Einführung in Datenstrukturen und Algorithmen
19 Einführung in grafische Oberflächen
20 Einführung in Dateien und Datenströme
21 Einführung ins Datenbankmanagement mit JDBC
22 Bits und Bytes, Mathematisches und Geld
23 Testen mit JUnit
24 Die Werkzeuge des JDK
A Java SE-Module und Paketübersicht
Stichwortverzeichnis


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 8 Schnittstellen, Aufzählungen, versiegelte Klassen, Records
Pfeil 8.1 Schnittstellen
Pfeil 8.1.1 Schnittstellen sind neue Typen
Pfeil 8.1.2 Schnittstellen deklarieren
Pfeil 8.1.3 Abstrakte Methoden in Schnittstellen
Pfeil 8.1.4 Implementieren von Schnittstellen
Pfeil 8.1.5 Ein Polymorphie-Beispiel mit Schnittstellen
Pfeil 8.1.6 Die Mehrfachvererbung bei Schnittstellen
Pfeil 8.1.7 Keine Kollisionsgefahr bei Mehrfachvererbung *
Pfeil 8.1.8 Erweitern von Interfaces – Subinterfaces
Pfeil 8.1.9 Konstantendeklarationen bei Schnittstellen
Pfeil 8.1.10 Nachträgliches Implementieren von Schnittstellen *
Pfeil 8.1.11 Statische ausprogrammierte Methoden in Schnittstellen
Pfeil 8.1.12 Erweitern und Ändern von Schnittstellen
Pfeil 8.1.13 Default-Methoden
Pfeil 8.1.14 Erweiterte Schnittstellen deklarieren und nutzen
Pfeil 8.1.15 Öffentliche und private Schnittstellenmethoden
Pfeil 8.1.16 Erweiterte Schnittstellen, Mehrfachvererbung und Mehrdeutigkeiten *
Pfeil 8.1.17 Bausteine bilden mit Default-Methoden *
Pfeil 8.1.18 Markierungsschnittstellen *
Pfeil 8.1.19 (Abstrakte) Klassen und Schnittstellen im Vergleich
Pfeil 8.2 Aufzählungstypen
Pfeil 8.2.1 Methoden auf Enum-Objekten
Pfeil 8.2.2 Aufzählungen mit eigenen Methoden und Initialisierern *
Pfeil 8.2.3 enum mit eigenen Konstruktoren *
Pfeil 8.3 Versiegelte Klassen und Schnittstellen
Pfeil 8.3.1 Versiegelte Klassen und Schnittstellen (sealed classes/interfaces)
Pfeil 8.3.2 Unterklassen sind final, sealed, non-sealed
Pfeil 8.3.3 Abkürzende Schreibweisen
Pfeil 8.4 Records
Pfeil 8.4.1 Einfache Records
Pfeil 8.4.2 Records mit Methoden
Pfeil 8.4.3 Konstruktoren von Records anpassen
Pfeil 8.4.4 Konstruktoren ergänzen
Pfeil 8.4.5 Versiegelte Schnittstellen und Records
Pfeil 8.4.6 Zusammenfassung
Pfeil 8.5 Zum Weiterlesen
 

Zum Seitenanfang

8.4    Records Zur vorigen ÜberschriftZur nächsten Überschrift

In Java 16 ist ein neuer Datentyp eingezogen: Records.[ 179 ](https://openjdk.java.net/jeps/395) Genauso wie bei Klassen lassen sich von Records Objekte mit new bilden, aber im Gegensatz zu Klassen sind die Record-Objekte in der ersten Ebene grundsätzlich immutable. Bei der Syntax ist nur wenig Code nötig, um mehrere Datenelemente in einer leichtgewichtigen Datenstruktur zusammenzufassen; die Spezifikation spricht hier von nominalen Tupeln (engl. nominal tuples).

 

Zum Seitenanfang

8.4.1    Einfache Records Zur vorigen ÜberschriftZur nächsten Überschrift

Die Syntax bei der Deklaration unterscheidet sich auch ein wenig von Klassen. Zunächst einmal wird das neue Schlüsselwort record verwendet. Es folgt der Name des Datentyps, und dann, nicht wie sonst üblich, direkt eine geschweifte Klammer, sondern eine runde Klammer. In den Klammern folgen die Record-Komponenten mit ihrem Typ und Namen – optional können die Parameter annotiert werden. Die Schreibweise mit runden Klammern erinnert an eine Konstruktor-Parameterliste, und genau so werden später auch Records instanziiert. Nach der geschlossenen runden Klammer kommt ein Block in geschweiften Klammern. Der Körper des Blocks kann leer sein.

Ein erstes Beispiel: Ein Record Location soll die geografischen Koordinaten speichern.

Listing 8.40     src/main/java/com/tutego/insel/records/v1/Location.java, Location

public record Location(  // Name

double latitude, // Komponenten im Header

double longitude

) { } // Body

Das Record Location hat somit zwei Komponenten: latitude und longitude. Fast alle Bezeichner sind gültig, aber clone, finalize, getClass, hashCode, notify, notifyAll, toString oder wait darf die Komponente nicht lauten.

Die Deklaration eines Records ist kompakt, und automatisch legt der Compiler einen einzigen Konstruktor an. Während reguläre Klassen einen Default-Konstruktor haben, besitzen Records einen kanonischen Konstruktor, der alle Zustände initialisiert. Es gibt mindestens diesen kanonischen Konstruktor, weitere lassen sich ergänzen. Die Sichtbarkeit ist automatisch die des Records; da in unserem Beispiel das Location-Record public ist, ist auch der Konstruktor public.

Außerdem erzeugt der Compiler eine Reihe von Methoden. Da sind zum einen die Anfragemethoden. Dabei kommt nicht die Schreibweise der Getter zum Tragen, sondern die Zugriffsmethode heißt wie der Komponentenname. Mutationsmethoden fehlen, da die Zustände eines Records immutable sind.

Jedes Record erbt automatisch von einer Oberklasse java.lang.Record, und diese dokumentiert, wie sich equals(…), hashCode() und toString() bei jedem Record verhalten. Das ist vergleichbar mit Aufzählungstypen, wo es auch eine Vererbungsbeziehung zu java.lang.Enum gibt. Ein Record kann keine andere Klasse und kein anderes Record erweitern.

Ein Beispiel zu den Methoden:

Listing 8.41     src/main/java/com/tutego/insel/records/v1/LocationDemo.java, LocationDemo

Location manila = new Location( 14.60416, 120.98222 );

System.out.printf( "latitude=%f, longitude=%f%n",

manila.latitude(), manila.longitude() );

System.out.println( manila );



Location location1 = new Location( 14.60416, 120.98222 );

System.out.println( location1.equals( manila ) );

Location location2 = new Location( 14, 120 );

System.out.println( location2.equals( manila ) );

Ausgabe:

latitude=14,604160, longitude=120,982220

Location[latitude=14.60416, longitude=120.98222]

true

false

Records sind Objekte, die durch ihre Werte identifiziert werden. Die Gleichwertigkeit ist also von Bedeutung, nicht die Identität. Deshalb hat jedes Record eine equals(…)-Implementierung, wobei referenzierte Unterelemente auch eine sinnvolle equals(…)-Implementierung haben müssen.

[»]  Hinweis

Eine Entkopplung von den internen Zuständen ist nicht möglich, dafür sind Records nicht gemacht. So etwas hier wäre mit einem Record nicht möglich – die Speicherung von zwei int-Werten in einem long:

class Coordinate {

private final long value;

Coordinate( int x, int y ) { value = (((long) x) << 32) | (y & 0xffffffffL); }

int x() { return (int) (value >> 32); }

int y() { return (int) value;}

}

Nach außen gibt es wie bei einem Record einen Konstruktor und auch die Zugriffsmethoden, aber die interne Abbildung ist eine ganz andere.

 

Zum Seitenanfang

8.4.2    Records mit Methoden Zur vorigen ÜberschriftZur nächsten Überschrift

Unser Record Location war sehr einfach aufgebaut. Dass bei einem Record der Zustand im Vordergrund steht, lässt sich schon allein daran ablesen, wie weit oben die Deklaration seiner Zustände steht. Doch Records können noch ein bisschen mehr: Sie können im Körper Objektmethoden und statische Methoden deklarieren.[ 180 ](Native Methoden sind in Records verboten. ) Das nächste Beispiel zeigt das:

Listing 8.42     src/main/java/com/tutego/insel/records/v2/Location.java, Location

public record Location(

double latitude,

double longitude

) {



Point2D.Double toPoint() {

return new Point2D.Double( longitude, latitude );

}



Location withLatitude( double latitude ) {

return new Location( longitude, latitude );

}



Location withLongitude( double longitude ) {

return new Location( longitude, latitude );

}



@Override public double longitude() {

System.out.println( "Access" + longitude );

return longitude;

}



@Override public String toString() {

return latitude + "," + longitude;

}



static boolean isValid( double latitude, double longitude ) {

return ( -90 <= latitude && latitude <= +90)

&& (-180 <= longitude && longitude <= +180);

}

}

Die erste Methode, toPoint(), konvertiert die Koordinaten und liefert sie als Point2D-Objekt zurück. toPoint() greift dazu direkt auf die – privaten und finalen – Objektvariablen zurück, die im kanonischen Konstruktor automatisch gesetzt werden. Innerhalb eines Records ist auch der direkte Zugriff auf die Zustandsvariablen möglich. Die Wither-Methoden withLatitude(double) und withLongitude(double) zeigen einen Weg, wie sich bei immutable Typen Zustände »ändern« lassen, nämlich durch ein neues immutable Location-Objekt eben mit den geänderten Zuständen. Wither sind üblich bei immutable Datentypen – bei der Date-Time-API sind sie sehr prominent. Des Weiteren zeigt das Beispiel, dass die Zugriffsmethoden auch überschrieben werden können. Auch die toString() Methode lässt sich überschreiben. Statische Methoden sind ebenfalls kein Problem, nur abstrakte Methoden darf ein Record natürlich nicht haben; ein Record kann ja auch nicht abstrakt sein, da der Datentyp final ist.

Listing 8.43     src/main/java/com/tutego/insel/records/v2/LocationDemo.java, LocationDemo

Location manila = new Location( 14.60416, 120.98222 );

System.out.println( manila.toPoint() );

System.out.println( manila.withLatitude( 14 ) );

System.out.println( manila.withLongitude( 120 ) );

System.out.println( manila.longitude() );

System.out.println( manila );

System.out.println( Location.isValid( 15, 120 ) );

System.out.println( Location.isValid( 200, 0 ) );

Ausgabe:

Point2D.Double[120.98222, 14.60416]

120.98222,14.0

120.0,14.60416

Access120.98222

120.98222

14.60416,120.98222

true

false
 

Zum Seitenanfang

8.4.3    Konstruktoren von Records anpassen Zur vorigen ÜberschriftZur nächsten Überschrift

Records haben automatisch einen Konstruktor, in dem die Zustandsvariablen initialisiert werden. Wenn wir noch einmal record Location(double latitude, double longitude) { } herannehmen, dann steht im vom Compiler generierten Konstruktor automatisch: this.latitude = latitude; this.longitude = longitude;.

Es ist möglich, Code im Konstruktor mit einzubauen, um die Parameter zum Beispiel zu validieren oder zu normalisieren. Dabei müssen wir zwei Schreibweisen unterscheiden:

Listing 8.44     src/main/java/com/tutego/insel/records/v3/Location.java, Location

public record Location(

double latitude,

double longitude

) {



// -- Schreibweise 1 -----------------------------------------

public Location {

if ( ! isValid( latitude, longitude ) )

throw new IllegalArgumentException( "Invalid range" );

}



// -- Schreibweise 2 -----------------------------------------

// public Location( double latitude, double longitude ) {

// if ( ! isValid( latitude, longitude ) )

// throw new IllegalArgumentException( "Invalid range" );

// this.latitude = latitude;

// this.longitude = longitude;

// }



private static boolean isValid( double latitude, double longitude ) {

return ( -90 <= latitude && latitude <= +90)

&& (-180 <= longitude && longitude <= +180);

}

}

Beim zweiten Konstruktor können wir direkt auf die Parametervariablen zurückgreifen und sie verändern und dann sichern. Wichtig ist, dass die Zuweisungen this.latitude = latitude; this.longitude = longitude; auf jeden Fall stattfinden, sonst gibt es einen Compilerfehler – es ist immer so, dass finale Variablen im Konstruktor initialisiert werden müssen. In der ersten kompakten Schreibweise setzt uns der Compiler automatisch die Initialisierung in den Bytecode. Eine weitere Regel gibt es: Die Sichtbarkeit des eigenen Konstruktors muss mindestens so wie beim Record sein.

Ein Demo des Records:

Listing 8.45     src/main/java/com/tutego/insel/records/v3/LocationDemo.java, LocationDemo

System.out.println( new Location( 14.60416, 120.98222 ) );

try {

System.out.println( new Location( -1000, +1000 ) );

}

catch ( Exception e ) {

e.printStackTrace();

}

Ausgabe:

Location[latitude=14.60416, longitude=120.98222]

java.lang.IllegalArgumentException: Invalid range

at com.tutego.insel.records.v3.Location.<init>(Location.java:12)

at com.tutego.insel.records.v3.LocationDemo.main(LocationDemo.java:9)
 

Zum Seitenanfang

8.4.4    Konstruktoren ergänzen Zur vorigen ÜberschriftZur nächsten Überschrift

Records haben immer einen kanonischen Konstruktor, der alle Record-Komponenten annimmt. Dieser kann nicht entfernt werden. Es ist möglich, weitere überladene Konstruktor zu ergänzen, und natürlich können auch statische Erzeugermethoden eine Alternative sein.

Ein kleines Anwendungsbeispiel mit drei Konstruktoren und einer statischen Fabrikmethode fromPoint(…):

Listing 8.46     src/main/java/com/tutego/insel/records/v4/Location.java, Location

public record Location(

double latitude,

double longitude

) {



public Location( double latitude, double longitude ) {

this.latitude = latitude;

this.longitude = longitude;

}



public Location( Point point ) {

this( point.y, point.x );

}



public Location() {

this( 0, 0 );

}



public static Location fromPoint( Point point ) {

return new Location( point.y, point.x );

}

}

Bei einem zweiten Konstruktor ist wichtig, dass dieser an den kanonischen Konstruktor weiterleitet. Wer versucht zu schreiben

public Location( Point point ) {       // inline image

this.latitude = point.y;

this.longitude = point.x;

}

bekommt einen Compilerfehler der Art: »Non-canonical record constructor must delegate to another constructor«.

 

Zum Seitenanfang

8.4.5    Versiegelte Schnittstellen und Records Zur vorigen ÜberschriftZur nächsten Überschrift

Records passen gut mit versiegelten Schnittstellen zusammen, da Records automatisch final sind. Ein Beispiel: GeoJSON[ 181 ](https://de.wikipedia.org/wiki/GeoJSON gibt einen kleinen Einblick. ) ist ein offenes Format, um geografische Daten in JSON auszudrücken. Es gibt gewisse Primitive, wie Punkte, Linien, Polygonzüge. Ein Programm soll durch die Schnittstelle GeoJSONshape die erlaubten Unterklassen aufführen, die exemplarisch GeoJSONpoint und GeoJSONline sind:

Listing 8.47     src/main/java/com/tutego/insel/sealed/GeoJSONshape.java, GeoJSONshape

sealed interface GeoJSONshape

permits GeoJSONpoint, GeoJSONline {

record Coordinate(int x, int y) {

@Override public String toString() { return '[' + x + "," + y + ']'; }

}

}

Als geschachtelter Typ wird ein Record GeoJSONline mit den Record-Komponenten x und y deklariert.

Zwei Records implementieren die Schnittstelle. Ein Punkt besteht aus genau einer Koordinate:

Listing 8.48     src/main/java/com/tutego/insel/sealed/GeoJSONpoint.java, GeoJSONpoint

record GeoJSONpoint(Coordinate coordinate) implements GeoJSONshape { }

Eine Linie besteht aus einer Sammlung von Koordinaten. Es ist sehr nützlich, dass ein Vararg verwendet werden kann:

Listing 8.49     src/main/java/com/tutego/insel/sealed/GeoJSONline.java, GeoJSONline

record GeoJSONline(Coordinate... coordinates) implements GeoJSONshape { }

Zwar soll unser Programm kein komplettes GeoJSON-Dokument schreiben, aber zumindest die Teilbäume für Punkte und Linien:

Listing 8.50     src/main/java/com/tutego/insel/sealed/GeoJSONwriter.java, printGeoJSONcoordinates()

static void printGeoJSONcoordinates( GeoJSONshape shape ) {

Objects.requireNonNull( shape );

System.out.print( "\"coordinates\": " );

if ( shape instanceof GeoJSONpoint point )

System.out.printf( "{ \"type\": \"Point\", \"coordinates\": %s }%n",

point.coordinate() );

else if ( shape instanceof GeoJSONline line )

System.out.printf( "{ \"type\": \"LineString\", \"coordinates\": [ %s ] }%n",

Arrays.stream( line.coordinates() )

.map( GeoJSONshape.Coordinate::toString )

.collect( Collectors.joining( "," ) ) );

else

throw new IllegalStateException( "Unknown shape " + shape.getClass() );

}

Zur Nutzung:

Listing 8.51     src/main/java/com/tutego/insel/sealed/GeoJSONdemo.java, main()

GeoJSONpoint point = new GeoJSONpoint( new Coordinate( 10, 20 ) );

GeoJSONline poly = new GeoJSONline( new Coordinate( 20, 30 ),

new Coordinate( 50, 90 ) );

GeoJSONwriter.printGeoJSONcoordinates( point );

GeoJSONwriter.printGeoJSONcoordinates( poly );

Das führt zur Ausgabe:

"coordinates": { "type": "Point", "coordinates": 101,20] }

"coordinates": { "type": "LineString", "coordinates": [ 111,30],141,90] ] }
 

Zum Seitenanfang

8.4.6    Zusammenfassung Zur vorigen ÜberschriftZur nächsten Überschrift

Records dienen zur einfachen Aggregation von Werten, es geht nicht um das Bauen von Vererbungsbeziehungen; wohl kann allerdings ein Record Schnittstellen implementieren, wie wir gerade gesehen haben.

Im besten Fall sind Records bis ins letzte Glied immutable. Da kommt es ganz gelegen, dass natürlich der Komponententyp selbst wieder ein Record sein kann:

Listing 8.52     src/main/java/com/tutego/insel/records/v3/City.java, City

record City(String name, double population, int area, Location center) {}

Listing 8.53     src/main/java/com/tutego/insel/records/v3/CityWithLocation.java, CityWithLocation

City city = new City( "Gothic", 234534, 8374, new Location( 14, 120 ) );

System.out.println( city.center() ); // Location[latitude=14.0, longitude=120.0]
[»]  Hinweis

Records haben zwar immutable Komponenten, aber selbstverständlich können diese Komponenten änderbar sein. Ein record Line(Point from, Point to) {} ist nicht »tief« immutable; die Punkte selbst lassen sich zwar nicht durch neue Point-Objekte ersetzen, doch die Koordinaten, und somit indirekt das Record, lassen sich ändern:

Line line = new Line( new Point(), new Point() );

line.from().setLocation( 1,1 );

System.out.println(line); // Line[from=[...]Point[x=1,y=1], to=[…]Point[x=0,y=0]]

Das ganze Konzept der Immutability hängt ganz zentral daran, dass nur primitive Werte oder andere immutable Datentypen referenziert werden und keine veränderbaren Objekte.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java ist auch eine Insel Java ist auch eine Insel

Jetzt Buch bestellen


 Buchempfehlungen
Zum Rheinwerk-Shop: Captain CiaoCiao erobert Java

Captain CiaoCiao erobert Java




Zum Rheinwerk-Shop: Algorithmen in Java

Algorithmen in Java




Zum Rheinwerk-Shop: Spring Boot 3 und Spring Framework 6

Spring Boot 3 und Spring Framework 6




Zum Rheinwerk-Shop: Java SE 9 Standard-Bibliothek

Java SE 9 Standard-Bibliothek




 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2024

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.

Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de



Cookie-Einstellungen ändern