Objekt-Arrays

Objekt-Arrays haben einige Aspekte, die zu berücksichtigen sind:

  • Immer dann, wenn null ein zulässiger Objekt-Inhalt ist, muss vor Nutzung eine null-Prüfung erfolgen

  • Bei Verwendung als Werte-Container mit LEER_WERT ist dieser fast immer der Wert null – damit ist dies automatisch mit-erledigt, da ohnehin nur Zellen gültig sind, wenn sie nicht den LEER_WERT enthalten.

  • Zugriff auf Methoden und Attribute eines enthaltenen Objekts erfolgt wie bei "normalen" Objekten – mit arrayname[index] ist die Objektreferenz verfügbar, damit auch der Zugriff auf Methoden und Attribute: array[idx].setSomething("abc"), array[idx].attribut, array[idx].printInfo() etc. möglich.

1. Deklaration, Instanzierung, Initialisierung

Namensgebung wie bei allen Arrays entweder als Mehrzahlwort wie 'personen', 'dokumente', etc. oder durch Anhängen eines Hinweiswortes wie 'Liste', 'Array', 'Pool' etc.

Deklaration erfolgt mit ElemKl[] arrayname; // ElemKl für 'ElementKlasse'

Instanzierung kann auf 2 Arten erfolgen:

  • arrayname = new ElemKl[kapazitaet]; Damit wird ein mit Zellwerten null gefülltes Array der gewünsten Kapazität angelegt.

  • arrayname = new ElemKl[] { new ElemKl(par1a, par2a, …​), null, null, new ElemKl(par1z, par2z, …​) };
    Damit wird ein Array mit Kapazität 4 angelegt und die Zellen 0, 3 mit gültigen Objekten, die Zellen 1, 2 mit null initialisiert.

Initialisierung im ersten obigen Fall erfolgt mit null-Werten, im zweiten Fall können explizit gültige Objekte oder null-Werte festgelegt werden, wobe mit der Anzahl auch die Kapazität definiert wird.

2. Überprüfung auf Gleichheit/Ungleichheit

Beim Kontrollieren, ob ein bestimmtes, übergebenes Objekt im Array enthalten ist, stellt sich die Frage, wie Übereinstimmung definiert wird.

2.1. Hintergrund

Die triviale Variante ist Übereinstimmung nur bei Identität. Dies kann problematisch sein: Nehmen wir ein String-Objekt, das als Literal definiert ist: String word1 = "hallo"; Nun erzeugen wir den selben Text auf andere, "dynamische" Weise:
String word2 = new String(new char[] {'h', 'a', 'l', 'l', 'o'});

Es zeigt sich, dass word1 == word2 den Wert false liefert, da bei Objekten ja mit Objekt-Referenzen hantiert wird, Es werden also eigentlich Objekt-Adressen im Hauptspeicher verglichen, die nur bei Identität gleich sind.
(Bei Strings ist die Sache unübersichtlicher, da Java inhaltlich gleiche String-Literale automatisch auf die SELBE Adresse zusammenfasst, d. h. nur dynamisch erzeugte inhaltsgleiche Strings können separate Objekte sein)

Für den Benutzer ist aber klar, dass bei Strings Gleichwertigkeit vorliegt, wenn die Zeichenfolge übereinstimmt - die Adresse ist egal.

Ähnliches gilt für fast alle Klassen, daher gilt die Regel, dass Objekte IMMER mit obj1.equals(obj2) verglichen werden, es sei denn, man möchte tatsächlich auf Idenität prüfen. (obj1 und obj2 müssen austauschbar sein, ohne das Ergebnis zu ändern)

2.2. Null-Prüfung

Ein wichtiger Spezialfall ist Prüfung auf null:
String txt = null; if (!txt.equals(null)) {…​…​} liefert eine NullPointerException, da das Objekt, an dem Methode equals(..) aufgerufen wird, ja null ist.

Hier MUSS daher immer geschrieben werden: if (txt != null && txt.length() …​) {…​…​}.
Wichtig: die null-Prüfung muss zuerst erfolgen!

(Das funktioniert übrigens nur deshalb, weil Java logische Ausdrücke nur so weit abarbeitet, bis das boolsche Ergebnis feststeht und &&-Verknüpfungen ab dem ersten false sicher als Ganzes false sind)

2.3. Implementieren von 'equals(..)'

Die hier besprochene Methode equals(..) ist in trivialer Implementation (liefert true nur bei Identität) automatisch für jede Klasse definiert (basierend auf der noch nicht behandelten Vererbung und der "Stammvater"-Klasse Object)

Diese Trivial-Implementation kann man jedoch überschreiben/übersteuern (Englisch: "override"), man implementiert sie einfach mit der selben Signatur neu. Optional (aber zur Fehleraufdeckung durch den Compiler sehr sinnvoll) kann die Annotation @Override vor die Deklaration geschrieben werden.

Beispielhaft sieht das für eine (unvollständig implementierte) Klasse Person so aus:

public class Person {
    private String name;
    private LocalDate gebDatum = null;
    private int ranking;
    private String info;

    // ...

    @Override                                                        (1)
    public boolean equals(Object obj) {                              (2)
        if (obj == this) return true;  // identity!                  (3)
        if (obj == null || !(obj instanceof Person)) return false;   (4)
        Person other = (Person) obj;                                 (5)
        if (this.name.equals(other.name)
                && this.gebDatum.equals(other.gebDatum)              (6)
                && this.ranking == other.ranking) {                  (7)
            return true;                                             (8)
        }
        return false;                                                (9)
    }
}
1 Java "Annotation" für Uberschreiben - optional, aber empfehlenswert
2 zwingend festgelegte Methodensignatur
3 übergebenes Objekt obj identisch mit demjenigen, an dem Methode aufgerufen wurde (this) → true
4 übergebenes Objekt ist null oder nicht vom selben Typ wie "this" → false
5 wenn bisher kein return wirksam, hat obj tatsächlich Typ Person, somit kann gefahrlos "casting" erfolgen.
Bis hierher ist das Schema immer gleich (bis auf Verwendung des passenden Typs)
6 this ist das Objekt, an dem die Methode aufgerufen wurde, other das gecastete Parameter-Objekt. Die Klassen String und LocalDate haben sinnvolle equals(..)-Implementationen, die genutzt werden.
7 Beim int-Attribut ranking kann direkt verglichen werden.
8 Die 3 Attribute name, gebDatum und ranking müssen für true alle übereinstimmen (Damit wird "willkürlich" festgelegt, was als gleichwertig angesehen wird)
9 Wenn bisher noch kein return wirksam wurde

Wir "schummeln" vorerst und verwenden fälschlicherweise eine vereinfachte Variante, die als Parameter nicht den Typ Object, sondern den typ der eigenen Klasse verwendet, da wir die Typ-Prüfung mit instanceof noch nicht gelernt haben.
In unseren Beispielen gibt das keinen Unterschied, da unsere Methode "spezialisierter" ist und vom Compiler ausgewählt wird, wenn wir die richtigen Objekte als Vergleichs-Partner verwenden.

Klassen, die auch zur Erstellung von Objekt-Arrays dienen sollen, müssen für korrekte Funktion UNBEDINGT die Methode equals(..) implementieren, da diese benötigt wird, um das Vorhandensein eines bestimmten oder dazu gleichwertigen Objekts im Array zu prüfen.
Benötigt wird dies z.B. beim Entfernen eines Objekts (remove-Methode), Sicherstellung von Doublettenfreiheit, …​ Die Signatur dieser equals-Methode ist vom JDK fix vorgegeben, da sie an vielen Stellen benötigt wird: boolean equals(Objekt obj): boolean. (trotzdem funktioniert unser "Schummeln")

Der Entwickler entscheidet, wie Gleichwertigkeit (Austauschbarkeit) festgelegt sein soll: Bei Personen könnte schon Übereinstimmung der Sozialversicherungsnummer (falls als Attribut vorhanden und immer definiert) ausreichen, bei Online-Accounts die E-Mail-Adresse, bei Personen ginge auch die Kombination aus Nachname, Vorname, Geburtsdatum und Geburtsort, etc., in unserem obigen Beispiel reicht die Übereinstimmung von name, gebDatum und ranking, die info wird dazu NICHT verwendet.

3. Vergleichen für Sortieren mit 'compareTo(…​)'

Für Sortieralgorithmen genügt die Definition einer einzigen spezifischen Vergeleichsmethode, die bei Übergabe zweier Objekte der zu sortierenden Sammlung die Entscheidung kleiner/gleich/größer (bezogen auf das erste Objekt) liefert, um durch oftmaligen Aufruf dieser Methode mit unterschiedlichen Objekt-Paaren für ALLE Objekt-Arten und Sortier-Definitionen eine Sortierung durchzuführen (jedes Objekt muss mindestens einmal dabei sein).

Da sehr oft eine "Standard-Sortierung" für Listen benötigt wird (Text-Einträge: alphabetische Sortierung, Datum: zeitliche Ordnung, Zahlen: numerischer Wert, …​), wird häufig eine dazu passende Vergleichsmethode direkt in die jeweilige Klasse implementiert. Diese hat eine standardisierte Signatur, womit sie von jedem standardkonformen Sortieralgorithmus verwendet werden kann: int compareTo(SelbeKlasse other) (später werden wir lernen, dass die allgemeine Nutzbarkeit erfordert, dass die Klasse das Interface java.lang.Comparable<T> implementiert).

Beim Rückgabewert werden nur 3 Zustände unterschieden:

  • 0 für sortiermäßige Gleichheit (oft, aber nicht zwingend: wenn equals(..) true ergibt)

  • negativ (Wert egal !!), wenn erstes Objekt KLEINER

  • positiv (Wert egal !!), wenn erstes Objekt GRÖSSER

Merkregel: Selbes Vorzeichen, das bei Zahlendifferenz entsteht: 5 < 75 - 7 = -2 oder 6 > 36 - 3 = +3

Hier ein Beispiel, wieder für obige einfache Personenklasse:

    public int compareTo(Person other) {
        int cmp = this.getName().compareTo(other.getName()); // Getter
        if (cmp == 0) {  // falls beide Namen gleich:       // oder
            cmp = this.gebDatum.compareTo(other.gebDatum); // direkt
        }
        if (cmp == 0) {  // falls immer noch kein Unterschied:
            cmp = this.ranking - other.ranking;  // 3. Attribut checken
        }
        return cmp;  // numerisch 3 Zustände: <0, =0, >0
    }

4. Sortierverfahren 'SelectionSort', 'BubbleSort'

Nun das oben verwendete Beispiel komplett. In Klasse Person findet sich equals(..) und compareTo(..), in PersonenContainer die genannten Sortierverfahren und einige weitere sinnvolle Methoden.

4.1. Klasse 'Person'

import java.time.LocalDate;

/**
 * Einfache Person-Klasse mit equals, compareTo, LocalDate, etc..
 *
 * @author renkin@spengergasse.at
 * @version 2022-06-06
 */
public class Person
{
    private String name;
    private LocalDate gebDatum = null;
    private int ranking;
    private String info;

    public Person(String name, LocalDate gebDatum) {
        this.setName(name);
        setGebDatum(gebDatum);
    }

    public Person(String name, int jahr, int monat, int tag) {
        setName(name);
        LocalDate gebDatum = LocalDate.of(jahr, monat, tag);
        //or: gebDatum = LocalDate.parse("2002-12-31");  //11.12.2002
        // for calc of age: LocalDate now = LocalDate.now();
        // int alter = now.getYear() - gebDatum.getYear();
        setGebDatum(gebDatum);
    }

    public void setName(String name) {
        this.name = name;
    }

    // Aufruf z.B. mit: ...setGebDatum(new LocalDate.of(2002, 12, 31))
    public void setGebDatum(LocalDate gebDatum) {
        if (gebDatum.isAfter(LocalDate.now())) {
            System.out.format("liegt in Zukunft!");
        } else {
            this.gebDatum = gebDatum;
        }
    }

    public void setInfo(String info) {
        this.info = info;
    }

    public String getName() {
        return this.name;
    }

    public LocalDate getGebDatum() {
        return this.gebDatum;
    }

    public String getInfo() {
        return this.info;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;  // identity!
        if (obj == null || !(obj instanceof Person)) return false;
        Person other = (Person) obj;
        // usually makes sense to have consistent equals, compareTo, so we could use:
        // return this.compareTo(other) == 0;
        // or separate implementation:
        if (this.name.equals(other.name)
                && this.gebDatum.equals(other.gebDatum)
                && this.ranking == other.ranking) {
                    return true;
                }
        return false;
    }

    public int compareTo(Person other) {
        int cmp = this.getName().compareTo(other.getName());
        if (cmp == 0) {
            cmp = this.getGebDatum().compareTo(other.getGebDatum());
        }
        if (cmp == 0) {
            cmp = this.ranking - other.ranking;
        }
        return cmp;
    }

    @Override
    public String toString() {
        String infoTxt = "---";
        if (info != null) {
            infoTxt = "'" + info + "'";
        }
        return "Person name=" + name + ", gebDatum=" + gebDatum + ", ranking=" + ranking
        + ", info: " + infoTxt;
    }

    public void printInfo() {
        System.out.println(this);
        //System.out.println(this.toString());
    }

    public static void testCompareTo() {
        Person ada = new Person("Ada", 1987, 06, 05);
        Person evi = new Person("Evi", LocalDate.parse("1999-12-24"));
        Person udo = new Person("Udo", LocalDate.of(2002, 12, 31));
        System.out.format("ada: %s%n", ada);
        System.out.format("evi: %s%n", evi);
        System.out.format("udo: %s%n", udo);
        System.out.format("ada.compareTo(evi)=%s%n", ada.compareTo(evi));
        System.out.format("evi.compareTo(ada)=%s%n", evi.compareTo(ada));
        System.out.format("evi.compareTo(evi)=%s%n", evi.compareTo(evi));
        System.out.format("evi.compareTo(udo)=%s%n", evi.compareTo(udo));
    }
}

4.2. Klasse PersonenContainer

import java.time.LocalDate;
import java.util.Random;

/**
 * Array als lückenlos gefüllter Werte-Container, dazu Sortieren,
 * wieder "durcheinanderbringen (unsort)", etc..
 *
 * @author renkin@spengergasse.at
 * @version 2022-06-06
 */
public class PersonenContainer
{
    private Person[] personen = { new Person("Evi", 2001, 11, 30), null };
    private int freeIdx = 1;

    public PersonenContainer() {
        Person p1 = new Person("Evi", 2002, 12, 31);
        personen = new Person[]{   //personen = new Person[8];
            p1,
            new Person("Nina", 2000, 11, 11),
            new Person("Evi", 1985, 02, 12),
            new Person("Udo", 1999, 01, 01),
            new Person("Ede", LocalDate.of(1987, 06, 05)),
            new Person("BabyTom", LocalDate.now()),
            null, null, null};
        freeIdx = 6;
    }

    public PersonenContainer(int kapazitaet) {
        personen = new Person[kapazitaet];
        freeIdx = 0;
    }

    public PersonenContainer(Person[] personen, int freeIdx) {
        this.personen = personen;
        this.freeIdx = freeIdx;
    }

    public boolean isFull() {
        return freeIdx == personen.length;
    }

    public boolean isEmpty() {
        return freeIdx == 0;
    }

    public int usedSize() {
        return freeIdx;
    }

    public int freeSpace() {
        return personen.length - usedSize();
    }

    public boolean add(Person person) {
        if (person == null) {
            System.out.format("person to add is null");
            return false;
        }
        if (isFull()) {
            System.out.format("Container full (capacity=%d)", personen.length);
            return false;
        }
        personen[freeIdx] = person;
        freeIdx++;
        return true;
    }

    public boolean contains(Person person) {
        for (int i = 0; i < freeIdx; i++) {
            if (personen[i].equals(person)) {   // use of equals !!!
                return true;
            }
        }
        return false;
    }

    public int indexOf(Person person) {
        for (int i = 0; i < freeIdx; i++) {
            if (personen[i].equals(person)) {   // use of equals !!!
                return i;
            }
        }
        return -1;
    }

    public boolean addUnique(Person person) {
        if (contains(person)) {
            System.out.format("already in container: %s", person);
            return false;
        }
        return add(person);
    }

    public boolean remove(int idx) {
        if (idx < 0 || idx >= freeIdx) {
            System.out.format("ERR remove: idx=%d not in 0..%d-1 (%<d items)", idx, freeIdx);
            return false;
        }
        freeIdx--;
        for (int i = idx; i < freeIdx; i++) {
            personen[i] = personen[i+1];
        }
        return true;
    }

    public boolean remove(Person person) {
        int idx = indexOf(person);  // use indexOf(..) from above
        if (idx < 0) {
            System.out.format("remove Person: not in container: %s", person);
            return false;
        }
        return remove(idx);         // use remove(..) from above
    }

    public void exchange(int i, int k) {
        if (i != k) {
            Person tmp = personen[i];
            personen[i] = personen[k];
            personen[k] = tmp;
        }
    }

    public void unsort() {
        Random randGen = new Random();
        for (int i = 0; i < freeIdx; i++) {
            exchange(i, randGen.nextInt(freeIdx));
        }
    }

    public void reverse() {
        for (int i = 0; i < freeIdx/2; i++) {
            exchange(i, freeIdx - i - 1);
        }
    }

    public void bubbleSort() {
        boolean more = true;
        while (more) {
            more = oneBubble();
        }
    }

    public boolean oneBubble() {
        boolean changed = false;
        for (int i = 1; i < freeIdx; i++) {
            if (personen[i-1].compareTo(personen[i]) > 0) {
                exchange(i-1, i);
                changed = true;
            }
        }
        return changed;
    }

    public void selectionSort() {
        for (int i = 0; i < freeIdx; i++) {
            exchange(i, findMinValIdx(i, freeIdx));
        }
    }

    public int findMinValIdx(int startIdx, int endIdxExcl) {
        if (startIdx < 0 || startIdx >= endIdxExcl) {
            return -1;  // will cause ArrayIndexOutOfBoundsException above if occuring
        }
        int currMinValIdx = startIdx;
        for (int i = startIdx+1; i < endIdxExcl; i++) {
            if (personen[i].compareTo(personen[currMinValIdx]) < 0) { // <0 .. 1st person "smaller"
                currMinValIdx = i;
            }
        }
        return currMinValIdx;
    }

    public void printList(String title) {
        System.out.format("==== %s ====%n", title);
        for (int i = 0; i < freeIdx; i++) {
            System.out.format("idx=%2d: %s%n", i, personen[i]);
        }
    }

}