Demo zu equals(…​), hashCode(), compareTo(…​), Collections und Weiterem

Es wird eine einfache Personenklasse zun Demonstrieren der im Titel erwähnten Themen definiert. Sie hat Attribute, die viele Konzepte für Methode compareTo(..) zeigen lassen.
Wichtige Aspekte:

  • Es werden zu Demonstrationszwecken zwei Konstante deklariert (MIN_BIRTH_YEAR, MAX_BIRTH_YEAR).

  • Es wird eine Enumeration (enum) verwendet: Gender.

  • Es wird das Interface Comparable<T> mit seiner einzigen Methode int compareTo(T other) "eingebaut". Damit lässt sich eine "natürliche" Ordnung festlegen, nach der standardmäßig sortiert werden soll
    (Wir wählen als stärkstes Kriterium smoker, dann lastname, firstname, birthYear, zuletzt gender).

  • In der main(..)-Methode wird eine Liste als Attribut mittels Interface List<T> deklariert und mit Klasse ArrayList<T> instanziert. Darin wird die statische Methode printPersons(..) aufgerufen und die Liste als Parameter übergeben.

  • Methode private static printPersons(Collection<Person> persons, String title) kann die übergebene Liste als Parameter verwenden, da Interface List<T> eine Spezialisierung von Interface Collection<T> ist (analog auch Interface Set<T>).

  • Die Liste wird sortiert mittels persons.sort(null);
    Die in Interface List definierte Methode sort(Comparator<? super E> c) kann als Parameter null erhalten, wenn die Elementklasse das Interface Comparable<T> implmentiert. Dann wird das mittels Methode compareTo(..) festgelegte natural ordering umgesetzt.

  • Oben erwähnte Methode printPersons(..) verwendet eine foreach-Schleife. Markant an dieser ist, dass sie sehr kompakte Iteration ermöglicht, aber keine Laufvariable für den Index bereitstellt. Das macht sie aber auch zur Iteration über Collections ohne definierte Ordnung (Sets) fähig.

  • In Methode equals(..) wird Methode java.util.Objects.equals(obj1, obj2)
    (mit Signatur public static boolean equals(Object a, Object b)) verwendet. Diese funktioniert auch, wenn z.B. this.firstname Wert null hat.

1. Basis-Info zur Demo-Klasse Person

public class Person implements Comparable<Person> {

    public static final int MIN_BIRTH_YEAR = 1900; // durch 'final' konstant -->
    public static final int MAX_BIRTH_YEAR = 2022; //.. MÜSSEN in Deklar. Wert erhalten

    public enum Gender {
        FEMALE, MALE, OTHER, UNKNOWN;
    }

    private String lastname;
    private String firstname;
    private Gender gender;
    private int birthYear;
    private boolean smoker;
    private String email;

    // ...

    // @Override public int hashCode() { ... }
    // @Override public boolean equals(Object obj) { ... }
    // @Override public int compareTo(Person o) { ... }   // definiert im Interface Comparable<Person>
}

2. Implementation hashCode() und equals(…​)

Ein erster Anwendungsfall ist die Annahme, dass die 4 Attribute lastname, firstname, gender und birthYear Eindeutigkeit einer Person sicherstellen (stimmt nicht - dazu müsste das vollständige Geburtsdatum und der Geburtsort gegeben sein, dann wird im jeweiligen Standesamt dafür gesorgt, dass es keine 2 Franz Meier am selben Tag im selben Ort/Gemeindebezirk gibt).

Dies wird für die Re-Definition (Override) der beiden wichtigen, schon in der Root-Klasse Object definierten Methoden equals(…​) und hashCode() benötigt.

Unter dieser Annahme und durch Auswahl dieser Attribute im Generator-Dialog kann man in der IDE die folgenden Methoden generieren.
Hier wurden in Eclipse die Checkboxen "Use 'instanceof' to compare types" und "Use Objects.hash and Objects.equals methods (1.7 or higher)" angekreuzt – dies macht die Methoden wesentlich kompakter.
Zusätzlich wurde in equals(…​) händisch das this. vor die eigenen Instanzvariablen gesetzt, da damit die Symmetrie this ←→ other besser sichtbar wird, weiters wurden erläuternde Kommentare eingefügt:

    @Override
    public int hashCode() {
        // in der JDK-Methode Objects.hash(...) wird schon die ganze Arbeit erledigt:
        return Objects.hash(this.birthYear, this.firstname, this.gender, this.lastname);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)              // identisches Objekt übergeben (sich selbst)
            return true;
        if (!(obj instanceof Person)) // <--  falls 'obj' keine Person, kann es nicht gleich sein
            return false;             //   |
        Person other = (Person) obj;  // obiges 'instanceof' sichert die Gültigkeit des type cast
        return this.birthYear == other.birthYear && Objects.equals(this.firstname, other.firstname)
                && this.gender == other.gender && Objects.equals(this.lastname, other.lastname);
    }

Bei Annahme, dass die E-Mail-Adresse Eindeutigkeit sichert (typisch für Online-Accounts) ist die Instanzvariable email als Einziges auszuwählen und es entsteht bei gleicher Vorgehensweise:

    @Override
    public int hashCode() {
        return Objects.hash(this.email);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof Person))
            return false;
        Person other = (Person) obj;
        return Objects.equals(this.email, other.email);
    }

3. Implementation compareTo(…​)

Die wegen Implementierens des Interface Comparable<T> zu implementierende Methode public int compareTo(Person o) { …​ } erfordert implizit die Festlegung, welche "natürliche" Ordnung man für Personen verwenden möchte.
Hier wählen wir als stärkstes Kriterium lastname, dann firstname, dann birthYear, zuletzt gender:

    @Override
    public int compareTo(Person other) {
        int cmp;
        if ((cmp = this.lastname.compareTo(other.lastname)) != 0) {   // bei Gleichheit gilt cmp == 0
            return cmp; // das erste Kriterium ergibt Ungleichheit und hat damit Klarheit geschaffen
        }
        if ((cmp = this.firstname.compareTo(other.firstname)) != 0) {
            return cmp;
        }
        if ((cmp = this.birthYear - other.birthYear) != 0) {
            return cmp;
        }
        if ((cmp = this.gender.compareTo(other.gender)) != 0) {
            return cmp;
        }
        return 0; // alle Kriterien sind gleich, daher ist Gleichheit gegeben --> 0
        // return this.email.compareTo(other.email); // falls nur email verwendet würde ...
    }

Nun wollen wir einige Collection-Typen ansehen