1. Besondere Typen der Java-Bibliothek
Die Klassenbibliothek umfasst Tausende von Typen, eine riesige Anzahl kommt über die Java Enterprise Frameworks und quelloffene Bibliotheken hinzu. Zum Glück muss man nicht alle diese Typen kennen, um erfolgreich Software schreiben zu können. Vieles in der Java SE ist auch sehr low level und eher für Entwickler von Frameworks gedacht.
Einige allgemeine Typen sind besonders eng mit der Sprache verbunden, sodass selbst der Compiler sie kennt. Diese müssen wir verstehen, damit wir die Möglichkeiten der Sprache optimal nutzen können. Es geht daher in diesem Kapitel um die absolute Oberklasse Object
, welche Methoden für uns relevant sind, um Ordnungsprinzipien, die Umwendung von primitiven Typen und Wrapper-Typen (Autoboxing).
Voraussetzungen
Unterschied zwischen
==
(Identität) undequals(…)
(Gleichwertigkeit) kennenequals(…)
undhashCode()
implementieren könnenComparator
undComparable
für Ordnungskriterien implementierenFunktion von Autoboxing verstehen
Aufzählungstypen deklarieren und mit Eigenschaften ausstatten
Verwendete Datentypen in diesem Kapitel:
Noch mehr Aufgaben findest du im Buch: ›Captain CiaoCiao erobert Java: Das Trainingsbuch für besseres Java. 300 Java-Workshops, Aufgaben und Übungen mit kommentierten Lösungen‹
1.1. Absolute Oberklasse java.lang.Object
Aus Object
gibt es drei Methoden, die Unterklassen in der Regel überschreiben: toString()
, equals(Object)
und hashCode()
. Während die Identität mit ==
und !=
getestet wird, ist für die Gleichwertigkeit die Methode equals(Object)
zuständig. equals(Object)
und hashCode()
werden immer zusammen implementiert, sodass beide zueinanderpassen; wenn zum Beispiel der Hashcode von zwei Objekten nicht gleich ist, muss auch equals(…)
false
ergeben und wenn equals(…)
true
ergibt, müssen auch die beiden Hashcodes gleich sein. Bei der Implementierung sind gewisse Regeln zu beachten, weshalb die nächsten beiden Aufgaben equals(Object)
und hashCode()
in den Fokus setzen.
1.1.1. equals(Object) und hashCode() generieren lassen ⭐
Jede moderne Java-Entwicklungsumgebung kann diverse Methoden automatisch generieren, zum Beispiel toString()
, aber auch equals(Object)
und hashCode()
.
Die Entwicklungsumgebungen haben etwas andere Menüpunkte und Dialoge. equals(Object)
/hashCode()
lassen sich für die drei bekannten IDEs wie folgt erzeugen:
- IntellIJ
In dieser IDE drücken wir den Shortcut Alt+Einfg. Es folgt eine Liste von Dingen, die generiert werden können, und darunter steht equals() and hashCode() aufgeführt. Aktivieren wir den Eintrag, öffnet sich zunächst ein Dialog, in dem wir verschiedene Vorlagen auswählen können. IntelliJ kann die Methoden auf unterschiedliche Arten generieren. Wir bleiben bei der Default-Einstellung und wechseln auf den nächsten Dialog mit Next. Wir wählen jetzt die Objektvariablen aus, die für die
equals(…)
-Methode verwendet werden; standardmäßig sind das alle. Wir gehen auf Next. Im nächsten Schritt kommt der gleiche Dialog, doch nun wählen wir die Objektvariablen für diehashCode()
-Methode aus; standardmäßig sind wieder alle vorausgewählt. Wir drücken Next und kommen in den nächsten Dialog, wo wir noch bestimmen dürfen, ob der Namenull
sein darf oder nicht. Da wir annehmen, er könntenull
sein, wählen wir das Feld nicht an, sondern gehen direkt auf Finish.- Eclipse
Bei Eclipse setzen wir den Cursor in den Rumpf der Klasse, aktivieren das Kontextmenü, navigieren auf den Menüpunkt Source, und gehen auf Generate hashCode() and equals(). Anders als in IntelliJ werden in Eclipse die Objektvariablen nur einmal angezeigt und für die
equals(Object)
- sowiehashCode()
-Methode gleichzeitig verwendet. Die Codegeneration startet mit einem Klick auf Generate.- NetBeans
Gehe im Menüpunkt unter Source (oder aktiviere das Kontextmenü im Editor), dann wähle Insert Code; alternativ aktiviere über die Tastatur Alt+Einfg. Es folgt ein kleiner Dialog, in dem man equals() and hashCode()… auswählen kann. Auch andere Dinge wie Setter, Getter, Konstruktor,
toString()
lassen sich so generieren.
Aufgabe:
Kopiere folgende Klasse in das Projekt:
public class Person { public long id; public int age; public double income; public boolean isDrugLord; public String name; }
Erzeuge mit der IDE für die Klasse
Person
die Methodenequals(Object)
undhashCode()
.Studiere die generierten Methoden ganz genau.
1.1.2. Existierende equals(Object)-Implementierungen ⭐⭐
Was sagt die Javadoc, bzw. wie sehen die equals(Object)
-Implementierungen bei den folgenden Klassen aus?
java.awt.Rectangle
(Moduljava.desktop
)java.lang.String
(Moduljava.base
)java.lang.StringBuilder
(Moduljava.base
)java.net.URL
(Moduljava.base
)
Online lässt sich der Code vom OpenJDK unter https://github.com/openjdk/jdk/tree/master/src/ für die einzelnen Module einsehen; die Klassen finden sich unter share/classes.
1.2. Schnittstellen Comparator und Comparable
Ein Vergleich mit equals(…)
sagt aus, ob zwei Objekte gleichwertig sind, aber besagt nichts über die Ordnung, welches Objekt größer oder kleiner ist. Dafür gibt es in Java zwei Schnittstellen:
Comparable
wird von den Typen implementiert, die eine natürliche Ordnung haben, für die es also in der Regel ein übliches Ordnungskriterium gibt. Bei zwei Datumswerten ist klar, welches vorher und welches nachher lag oder ob beide Datumswerte gleich sind.Von der Schnittstelle
Comparator
gibt es für jedes Ordnungskriterium eine Implementierung. Personen können wir nach dem Namen sortieren, aber auch nach dem Alter: Das wären zwei Implementierungen vonComparator
.
Comparator
und Comparable
1.2.1. Superhelden verarbeiten
Bonny Brain interessiert sich schon seit ihrer Kindheit für Superhelden. Und es gibt so viele spannende Dinge zu wissen. Damit Bonny Brain Antworten auf ihre Fragen bekommt, soll zunächst die Datenbasis definiert werden.
Aufgabe: Kopiere folgende Klassendeklaration in das eigene Java-Projekt:[1]
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
public class Heroes {
private Heroes() { }
public record Hero( String name, Heroes.Hero.Sex sex, int yearFirstAppearance ) {
public enum Sex { MALE, FEMALE }
public Hero {
Objects.requireNonNull( name ); Objects.requireNonNull( sex );
}
}
public static class Universe {
private final String name;
private final List<Hero> heroes;
public Universe( String name, List<Hero> heroes ) {
this.name = Objects.requireNonNull( name );
this.heroes = Objects.requireNonNull( heroes );
}
public String name() { return name; }
public Stream<Hero> heroes() { return heroes.stream(); }
}
// https://github.com/ullenboom/fivethirtyeight-data/tree/master/comic-characters
private static final Hero DEADPOOL = new Hero( "Deadpool (Wade Wilson)", Hero.Sex.MALE, 1991 );
private static final Hero LANA_LANG = new Hero( "Lana Lang", Hero.Sex.FEMALE, 1950 );
private static final Hero THOR = new Hero( "Thor (Thor Odinson)", Hero.Sex.MALE, 1950 );
private static final Hero IRON_MAN = new Hero( "Iron Man (Anthony 'Tony' Stark)", Hero.Sex.MALE, 1963 );
private static final Hero SPIDERMAN = new Hero( "Spider-Man (Peter Parker)", Hero.Sex.MALE, 1962 );
private static final Hero WONDER_WOMAN = new Hero( "Wonder Woman (Diana Prince)", Hero.Sex.FEMALE, 1941 );
private static final Hero CAPTAIN_AMERICA = new Hero( "Captain America (Steven Rogers)", Hero.Sex.MALE, 1941 );
private static final Hero SUPERMAN = new Hero( "Superman (Clark Kent)", Hero.Sex.MALE, 1938 );
private static final Hero BATMAN = new Hero( "Batman (Bruce Wayne)", Hero.Sex.MALE, 1939 );
public static final List<Hero> DC =
List.of( SUPERMAN, LANA_LANG, WONDER_WOMAN, BATMAN );
public static final List<Hero> MARVEL =
List.of( DEADPOOL, CAPTAIN_AMERICA, THOR, IRON_MAN, SPIDERMAN );
public static final List<Hero> ALL =
Stream.concat( DC.stream(), MARVEL.stream() ).toList();
public static final List<Universe> UNIVERSES =
List.of( new Universe( "DC", DC ), new Universe( "Marvel", MARVEL ) );
}
Wer die Klasse nun in das Aufgabenprojekt gesetzt hat, ist mit der Aufgabe auch schon fertig! Die Klassendeklaration ist eine Vorbereitung für die nächsten Aufgaben. Zum Inhalt der Klasse: Heroes
deklariert die zwei geschachtelten Klassen Hero
und Universe
und zudem Sammlungen mit Helden. Mit welcher Java-API die Variablen initialisiert werden und welche privaten Variablen es gibt, ist für die Lösung nicht relevant. Wir kommen im Rahmen der Java-Stream-API noch einmal auf die Klasse Heroes
zurück. Interessierte können die Klassen gerne in Records umschreiben.
1.2.2. Superhelden vergleichen ⭐⭐
Nicht alle Helden sind gleich! Einige erscheinen früher oder haben eine Glatze. Wir können Comparator
-Objekte nutzen, um individuell die Ordnung zwischen Helden zu bestimmen.
Aufgabe:
Baue als Erstes eine veränderbare Liste mit allen Helden auf:
List<Hero> allHeroes = new ArrayList<>( Heroes.ALL );
Schreibe einen
Comparator
, damit Helden nach dem Erscheinungsjahr in eine Reihe gebracht werden. Nutze zur Implementierung:eine lokale Klasse
eine anonyme Klasse
einen Lambda-Ausdruck
Die Schnittstelle
List
hat einesort(…)
-Methode. Sortiere mit dem neuenComparator
die ListeallHeroes
.Erweitere den einen
Comparator
, sodass bei gleichem Erscheinungsjahr zusätzlich nach dem Namen verglichen wird. Bewerte den Ansatz, dass derComparator
mehrere Kriterien gleichzeitig prüft.
1.2.3. Helden-Comparatoren verketten ⭐
Die Sortierung erfolgt oftmals nicht nur nach einem Kriterium, sondern nach mehreren. Ein typisches Beispiel ist das Telefonbuch — falls das heute noch bekannt ist … Zunächst werden die Einträge nach Nachnamen sortiert und bei einer Gruppe von Personen mit den gleichen Nachnamen anschließend nach Vornamen.
Oftmals sind bei der Ordnung mehrere Kriterien involviert. Die Verkettung der Comparator
-Exemplare müssen wir nicht selbst übernehmen, sondern wir können auf die Default-Methode thenComparing(…)
zurückgreifen.
Aufgabe:
Studiere die API-Dokumentation (oder Implementierung) zur
Comparator
-MethodethenComparing(Comparator<? super T> other)
.Einige Helden haben das gleiche Erscheinungsjahr.
Schreibe eine
Comparator
-Implementierung, die nur die Helden nach ihren Namen vergleicht.Schreibe einen zweiten
Comparator
, der die Helden nur nach ihrem Erscheinungsjahr vergleicht.Sortiere alle Helden im ersten Kriterium nach dem Erscheinungsjahr, dann nach dem Namen. Implementiere den zusammengesetzten
Comparator
mitthenComparing(…)
.
1.2.4. Mit einem Key-Extraktor schnell zum Comparator ⭐⭐
Ein Comparator
»extrahiert« in aller Regel Kernelemente und vergleicht sie. Damit kann der Comparator
aber eigentlich zwei Dinge: erstens die relevanten Informationen extrahieren und zweitens diese extrahierten Werte vergleichen. Nach guter objektorientierter Programmierung sollen diese zwei Schritte getrennt werden. Das ist das Ziel der statischen comparing*(…)
-Methoden der Schnittstelle Comparator
. Denn diesen Methoden wird ein lediglich ein Key-Extraktor übergeben, und den Vergleich der extrahierten Werte übernehmen die comparing*(…)
Methoden selbst.
Schauen wir uns drei Implementierungen an, und beginnen wir mit der Implementierung der Methode comparing(Function)
:
java.util.Comparator
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
Am Anfang findet der obligatorische null
-Test statt. Anschließend holt sich keyExtractor.apply(…)
den Wert aus dem ersten Objekt und dem zweiten Objekt. Da beide Objekte eine natürliche Ordnung haben (sie sind Comparable
), liefert compareTo(…)
diese Ordnung zurück. comparing(Function)
liefert einen Comparator
zurück, hier als Lambda-Ausdruck.
Der Key-Extraktor ist eine Funktion, die einen Wert liefert, und genau dieser Wert wird intern verglichen. comparing(Function)
kann verwendet werden, wenn die Objekte eine natürliche Ordnung haben. Es gibt nun unterschiedliche Fabrikmethoden für Comparator
-Exemplare, die neben dem Vergleich von Comparable
-Objekten ausgewählte primitive Datentypen extrahieren und diese vergleichen. Schauen wir uns die zweite Methode comparingInt(ToIntFunction)
an, wenn zwei Ganzzahlen extrahiert werden über eine ToIntFunction
:
java.util.Comparator
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}
Der Key-Extraktor extrahiert aus den zu vergleichenden Objekten einen Ganzzahlwert und geht dann an Integer.compare(…)
, um diese beiden Ganzzahlen zu vergleichen.
Schauen wir uns noch die letzte Funktion an. Sie verbindet einen Key-Extraktor mit einem Comparator
. Das ist dann praktisch, wenn die Objekte keine natürliche Ordnung haben, sondern ein fremder Comparator
die Ordnung feststellen muss.
java.util.Comparator
public static <T, U> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator)
{
Objects.requireNonNull(keyExtractor);
Objects.requireNonNull(keyComparator);
return (Comparator<T> & Serializable)
(c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
keyExtractor.apply(c2));
}
Zunächst einmal wird der Key-Extraktor für die beiden Objekte c1
und c2
die Werte extrahieren. Danach kommen die Werte in die compare(…)
-Methode der übergebenen Comparator
-Instanz. Der Lambda-Ausdruck liefert einen neuen Comparator
zurück.
Vergleicht man das mit eigenen Comparator
-Implementierungen, dann wird man im Allgemeinen das Gleiche tun, nämlich von zwei Objekten die Werte extrahieren und vergleichen. Genau das übernehmen die Fabrikfunktionen! Wir müssen lediglich mitgeben, wie ein Schlüssel extrahiert werden muss, und dann wird dieser Key-Extraktor auf die beiden Werte, die verglichen werden sollen, angewendet.
Variiere die vorherige Aufgabe wie folgt:
Erzeuge mit der statischen Methode
Comparator.comparingInt(ToIntFunction<? super T> keyExtractor)
und einem Lambda-Ausdruck einenComparator
für Helden-Erscheinungsjahre, und sortiere damit die Liste.Verwende für den Namensvergleich ebenfalls eine
Comparator
-Methode, die einen Key-Extractor benutzt.Sortiere nach Namen und anschließend nach Alter, wieder mit
thenComparing(…)
. Ändere dann die Verkettungsmethode, und verwendethenComparingInt(…)
anstelle vonthenComparing(…)
.Schreibe einen
Comparator<Hero>
, der sich aufCASE_INSENSITIVE_ORDER
ausString
stützt, um den Heldennamen unabhängig von der Groß-/Kleinschreibung. Greife auf dieComparator
-Methodecomparing(Function, Comparator)
zurück.
1.2.5. Punkte nach Abstand zum Zentrum sortieren ⭐
Captain CiaoCiao betreibt am Nordpol seine Absolutus Zero-Zero Vodka-Destillerie. Auf einer gedachten rechteckigen Karte befindet sich die Brennerei genau auf dem Nullpunkt. Ein java.awt.Point
ist durch x/y-Koordinaten repräsentiert, was für die Speicherung für Ortsangaben durchaus geeignet ist. Nun ist die Frage, ob gewisse Orte näher oder ferner der Brennerei liegen.
Aufgabe:
Schreibe einen Vergleichs-Comparator
PointDistanceToZeroComparator
fürPoint
-Objekte. Für den Vergleich soll die Distanz zum Nullpunkt verwendet werden. Ist der Abstand eines Punktes p1 vom Nullpunkt kleiner als der Abstand eines Punktes p2, so soll gelten p1 < p2.Baue ein Array von
Point
-Objekten auf und sortiere sie mit derArrays
-Methodesort(T[] a, Comparator<? super T> c)
.
Beispiel:
Point[] points = { new Point( 9, 3 ), new Point( 3, 4 ), new Point( 4, 3 ), new Point( 1, 2 ) };
Arrays.sort( points, new PointDistanceToZeroComparator() );
System.out.println( Arrays.toString( points ) );
Die Ausgabe ist:
[java.awt.Point[x=1,y=2], java.awt.Point[x=3,y=4], java.awt.Point[x=4,y=3], java.awt.Point[x=9,y=3]]
Die Klasse |
1.2.6. Geschäfte in der Nähe ermitteln ⭐⭐
Für die Spirituosen der Absolutus Zero-Zero Vodka-Destillerie baut Bonny Brain die Vertriebswege auf und plant Läden an verschiedenen Orten.
Aufgabe:
Lege ein neues Record
Store
an.Gib dem
Store
zwei ObjektvariablenPoint location
undString name
.Sammle diverse
Store
-Objekte in einer Liste.Schreibe eine Methode
List<Store> findStoresAround(Collection<Store> stores, Point center)
, die eine Liste zurückliefert, nach Abständen zumcenter
sortiert; vorne in der Liste stehen diejenigen, die der Destillerie am nächsten sind.
Noch mehr Aufgaben findest du im Buch: ›Captain CiaoCiao erobert Java: Das Trainingsbuch für besseres Java. 300 Java-Workshops, Aufgaben und Übungen mit kommentierten Lösungen‹