Kompakt-Einstieg Java Collections mit Generics, For-Each, Iteratoren

1. Überblick

Java Collections sind Sammlungen von Objekten mit gemeinsamer Charakteristik (gemeinsame Klasse / gemeinsames Interface). Passen normalerweise Kapazität dynamisch an, Daten primitiver Typen müssen Wrapper-Klasse verwenden.

Sie sind als Interface java.util.Collection<E> definiert.
Die 2 mit weitem Abstand gebräuchlichsten Sub-Interfaces sind java.util.List<E> und java.util.Set<E>

Die wichtigsten Implementationen sind:

Deklarationstyp List<E>:

Deklarationstyp Set<E>:

Deklarationstypen java.util.SortedSet<E> (Sub-Interface von Set) und
sein Sub-Interface java.util.NavigableSet<E> (zusätzlich nächst-kleinerer/-größerer Nachbar etc.):

2. Operationen / Methoden im Interface Collection

Folgende grundlegende Operationen sind für alle Sub-Interfaces normalerweise vorhanden (und "exotischere" weitere):

  • Elementanzahl erfragen: int size(), boolean isEmpty()

  • Enthalten-Sein von Elementen prüfen: boolean contains(Object o), boolean containsAll(Collection<?> c)

  • Hinzufügen von Elementen (optional): boolean add(E e), boolean addAll(Collection<? extends E> c)

  • Entfernen von Elementen (optional): boolean remove(Object o), boolean removeAll(Collection<?> c), void clear() (leert komplett)

  • Inhaltsgleiches Array erzeugen: <T> T[] toArray(T[] a) (a ist "Template"-Array-Objekt, sinnvoll mit Kapazität 0 oder aber zur direkten Nutzung mit benötigter Kapazität)

  • Iterator-Objekt für das Collection-Objekt: Iterator<E> iterator()

  • Per Konstruktor oder Utility-Klassen java.util.Collections (Endung s) gibt es Möglichkeiten, neue Collection-Instanzen mit Inhalt einer übergebenen Collection zu erzeugen (wichtig, wenn unveränderbar)

  • Utility-Klasse java.util.Arrays hat Methode static <T> List<T> asList(T…​ a).
    Selbst Set-Implementationen haben oft Konstruktoren, die alle Collection-Arten als Parameter erlauben.
    T…​ bedeuet, dass sowohl Arrays als auch explizite Wert-Aufzählungen erlaubt sind. Durch Autoboxing ist auch folgendes möglich:
    List<Double> l = Arrays.asList(1.0, 2.0, 3.0);

Manche Implementationen verbieten z.B. Änderungs-Operationen (werfen UnsupportedOperationException) – Kennzeichnung optional

Für Details siehe: zu Collection sowie zu List und zu Set.

3. Generics

Generics erlauben die Angabe von Typ-Parametern – in Collections kann damit der Element-Typ festgelegt werden.

  • Deklaration als List<E> erfordert Verwendung in Form von z.B. List<Person>. Dies erzwingt ausschließlich Personen-Objekte als Inhalt.

  • Es können mehrere Typen beistrich-getrennt nötig sein: Deklaration Map<K, V> kann als Map<SozVersNr, Person> verwendet werden

  • Üblicherweise werden bei Deklaration Einzel-Großbuchstaben wie T (type), E (element), K (key), V (value) verwendet.

  • Deklaration <?> bedeutet: beliebiger Typ

  • Deklaration <T extends Person> bedeutet: T muss Person oder davon abgeleitete Klasse sein.

  • Deklaration <T super Tier> bedeutet: T muss Tier oder eine Superklasse sein.

  • Deklarationen können verschachtelt sein: <T extends Object & Comparable<? super T>>

4. equals(..) und hashCode()

Diese dienen zur Objekt-Identifikation. (equals(..)) bzw. zur effizienten Auffindung in Objekt-Sammlungen (hashCode()) und sind schon innerhalb der Klasse Object erstmalig definiert.

Die Methoden-Signatur sind: boolean equals(Object obj) und int hashCode().

Die Identifikation soll häufig die Objekt-Idee betreffen, nicht die "physische" Repräsentation im Computer. Beispielsweise geht es bei String-Identifikation um die Zeichenabfolge und nicht um die Repräsentation im Hauptspeicher.

HashCodes sind int-Werte, die aus einer Auswahl von Objekt-Attributen einfach berechnet werden und normalerweise innerhalb der Collection NICHT eindeutig sind. Sie sollen vielmehr die Elemente einer Collection in viele kleine Gruppen mit selbem HashCode gliedern.

Die Elemente werden nach HashCode-Wert sortiert abgelegt und bei Prüfung, ob ein gegebenes Objekt in der Collection enthalten ist, wird dessen HashCode berechnet und dann die Position der Elemente mit diesem HashCode gesucht. Da die Ablage nach HashCodes geordnet ist, kann die Position extrem effizient gefunden werden. An dieser Stelle müssen im Normalfall nur sehr wenige Elemente mit equals(..) geprüft werden, bis das tatsächlich gesuchte Element gefunden ist.

Hier zeigt sich, dass equals(..) und hashCode() zueinander konsistent definiert sein müssen, da sonst das gesuchte Objekt u.U. gar nicht im erwarteten Bereich liegt und die Auffindung fehlschlägt.

Beide Methoden lassen sich gut automatisiert und konsistent erstellen – praktisch alle IDEs bieten eine solche Funktionalität an.

Hier finden sich Details zu equals(..) und Details zu hashCode()
sowie [Beispiel-Code]

5. For-Each

Auch "enhanced for loop" genannt, dient zu Iteration über ("Wiederholen", Durcharbeiten von) Arrays und Collections und generell über alle Klassen, die Interface java.lang.Iterable (siehe unten) implementieren.

----->
String[] someColors = {"black", "white", "red", "green", "blue", "yellow", "brown"};
System.out.format("List of %d colors:%n", someColors.length);
String separator = "[";
for (String color : someColors) {  // color erhält in jedem Durchlauf anderen Wert
    System.out.format("%s%s", separator, color);
    separator = ", ";
}
System.out.println("]");
----

Die im Kopf des Statements deklarierte Variable color erhält in jeder Runde einen weiteren Wert des Arrays, bis alle durchlaufen sind. Wichtig ist, dass z.B. bei Sets keine Ordnung definiert ist, es werden also zwar alle Elemente des Set bereitgestellt, die Reihenfolge ist jedoch nicht definiert!

6. Iteratoren

Alle Klassen, die das Interface java.lang.Iterable implementieren, müssen Methode Iterator<T> iterator() implementieren. Interface java.util.Iterator<E> (E ist der generic Element Typ) gehört zum Collection Framework.

Folgende Methoden sind essentiell:

  • boolean hasNext(): "schnuppert, ob weiteres Element verfügbar"

  • E next(): liefert nächstes (zu Beginn erstes) Element

  • default void remove(): erlaubt es im Gegensatz zum Durchlaufen einer for-each-Schleife SICHER, während der Iteration beliebig viele Elemente zu entfernen.

Beispiel:

public class IteratorDemo
{
    public static void iteratorRemoveDemo() {
        Set<String> msgSet = new HashSet<>(Arrays.asList("Hi", "  ", "i", "", "am", "somewhere"));
        printStringColl("nach Erzeugung:", msgSet);
        Iterator<String> msgSetIterator = msgSet.iterator();
        while (msgSetIterator.hasNext()) {
            String msg = msgSetIterator.next();
            if (msg.isBlank()) {
                msgSetIterator.remove();  // no Problem to remove several
                System.out.format("Removed (iterator) from Set: '%s'%n", msg);
            }
        }
        printStringColl("nach Bereinigung:", msgSet);
    }
    public static void printStringColl(String title, Collection<String> coll) {
        System.out.print(title);
        for (String el : coll) {
            System.out.format(" '%s'", el);
        }
        System.out.println();
    }
}

7. Veraltete Alternativen

in Arbeit ...
  • java.util.Enumeration *