I am currently working on an English translation. If you like to help to proofread please contact me: ullenboom ät g m a i l dot c o m.

Java Videotraining Werbung

1. Besondere Typen aus 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) und equals(…​) (Gleichwertigkeit) kennen

  • equals(…​) und hashCode() implementieren können

  • Comparator und Comparable für Ordnungskriterien implementieren

  • Funktion von Autoboxing verstehen

  • Aufzählungstypen deklarieren und mit Eigenschaften ausstatten

Verwendete Datentypen in diesem Kapitel:

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 die hashCode()-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 Name null sein darf oder nicht. Da wir annehmen, er könnte null 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)- sowie hashCode()-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 Methoden equals(Object) und hashCode().

  • 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 (Modul java.desktop)

  • java.lang.String (Modul java.base)

  • java.lang.StringBuilder (Modul java.base)

  • java.net.URL (Modul java.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 von Comparator.

Comparator Comparable UML
Abbildung 1. UML-Diagramm von 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]

Listing 1. com/tutego/exercise/util/Heroes.java
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

public class Heroes {

  private Heroes() { }

  public static class Hero {

    public enum Sex { MALE, FEMALE }

    public final String name;
    public final Sex    sex;
    public final int    yearFirstAppearance;

    public Hero( String name, Sex sex, int yearFirstAppearance ) {
      this.name = Objects.requireNonNull( name );
      this.sex  = Objects.requireNonNull( sex );
      this.yearFirstAppearance = yearFirstAppearance;
    }

    @Override public String toString() {
      return String.format( "Hero[name=%s, sex=%s, yearFirstAppearance=%s]",
                            name, sex, yearFirstAppearance );
    }
  }

  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/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.

Java 8 Backport

Die statischen of(…​)-Methoden der Schnittstelle List sind neu in Java 9. Für Java 8 lässt sich schreiben: Collections.unmodifiableList( Arrays.asList(e1, e2, …​). Die Methode Collectors.toUnmodifiableList() gibt es seit Java 10; für Java 8 müssen wir zum Beispiel schreiben: Collections.unmodifiableList( aStream.collect( Collectors.toList() ) ). Und statt .collect( Collectors.toUnmodifiableList() ist seit Java 16 auf dem Stream toList() möglich; das Ergebnis ist eine unmodifizierbare Liste.

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:

    1. eine lokale Klasse

    2. eine anonyme Klasse

    3. einen Lambda-Ausdruck

  • Die Schnittstelle List hat eine sort(…​)-Methode. Sortiere mit dem neuen Comparator die Liste allHeroes.

  • Erweitere den einen Comparator, sodass bei gleichem Erscheinungsjahr zusätzlich nach dem Namen verglichen wird. Bewerte den Ansatz, dass der Comparator 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-Methode thenComparing(Comparator<? super T> other).

  • Einige Helden haben das gleiche Erscheinungsjahr.

    1. Schreibe eine Comparator-Implementierung, die nur die Helden nach ihren Namen vergleicht.

    2. Schreibe einen zweiten Comparator, der die Helden nur nach ihrem Erscheinungsjahr vergleicht.

    3. Sortiere alle Helden im ersten Kriterium nach dem Erscheinungsjahr, dann nach dem Namen. Implementiere den zusammengesetzten Comparator mit thenComparing(…​).

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. Ein Comparator »extrahiert« in aller Regel Kernelemente und vergleicht diese. Das ist das Ziel der statischen comparingXXX(…​)-Methoden der Schnittstelle Comparator. Denn diesen Methoden wird ein lediglich ein Key-Extraktor übergeben, und den Vergleich der extrahierten Werte übernehmen die comparingXXX(…​) Methoden selbst.

Schauen wir uns drei Implementierungen an, und beginnen wir mit der Implementierung der Methode comparing(Function):

Listing 2. OpenJDK-Implementierung aus 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:

Listing 3. OpenJDK-Implementierung aus 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.

Listing 4. OpenJDK-Implementierung aus 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:

  1. Erzeuge mit der statischen Methode Comparator.comparingInt(ToIntFunction<? super T> keyExtractor) und einem Lambda-Ausdruck einen Comparator für Helden-Erscheinungsjahre, und sortiere damit die Liste.

  2. Verwende für den Namensvergleich ebenfalls eine Comparator-Methode, die einen Key-Extractor benutzt.

  3. Sortiere nach Namen und anschließend nach Alter, wieder mit thenComparing(…​). Ändere dann die Verkettungsmethode, und verwende thenComparingInt(…​) anstelle von thenComparing(…​).

  4. Schreibe einen Comparator<Hero>, der sich auf CASE_INSENSITIVE_ORDER aus String stützt, um den Heldennamen unabhängig von der Groß-/Kleinschreibung. Greife auf die Comparator-Methode comparing(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ür Point-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 der Arrays-Methode sort(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 java.awt.Point bietet diverse Klassen- und Objektmethoden zur Berechnung des Abstandes. Schaue dafür in die API-Dokumenation.

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:

  1. Lege eine neue Klasse oder Record Store an.

  2. Gib dem Store zwei Objektvariablen Point location und String name.

  3. Sammle diverse Store-Objekte in einer Liste.

  4. Schreibe eine Methode List<Store> findStoresAround(Collection<Store> stores, Point center), die eine Liste zurückliefert, nach Abständen zum center sortiert; vorne in der Liste stehen diejenigen, die der Destillerie am nächsten sind.


1. Quelle: https://github.com/fivethirtyeight/data/tree/master/comic-characters