Maps

in Arbeit …​

1. Überblick

Eine Map in Java-Diktion ist so etwas wie eine Liste mit nicht-numerischem "Index". Dieser Index wird meist als Key (Schlüssel) bezeichnet und definiert die "Zelle" eindeutig, in denen der zugeordnete Value (Wert) liegt.

Durch Angabe des Key kann also der Value erhalten werden.

In Java kann der Key-Typ im Prinzip jede Klasse sein. Wichtig ist, dass die als Keys verwendeten Objekte während dieser Verwendung nicht "an heiklen Stellen" von außen verändert werden, da sonst die Zugriffslogik und verschiedene andere Aspekte inkonsistent werden können.

Idealerweise sollten "immutable" oder faktisch im Betrieb nicht geänderte Objekte als Key dienen - das sind z.B. Strings, viele Klassen aus dem Zeit-Framework (Packages java.time…​.).

Falls die Keys nicht sicher unverändert bleiben, ist ein Benachrichtigungskonzept zu verwenden, das bei "heiklen" Änderungen das Neu-Einlesen, Neu-Sortieren o.ä. auslösen kann.

Später werden wir dazu das Konzept der "Property Change Events" kennenlernen.

2. Wichtige Operationen

Einige der wichtigsten Operationen sind:

  • size() …​ liefert die aktuell enthaltene Anzahl von Elementen

  • get(key) …​ liefert das Element (den "Value") zurück, falls der Key schon verwendet war – sonst null (wird auch geliefert, wenn der Value null war!)

  • containsKey() …​ liefert true, wenn der Key bereits verwendet ist (sicherer Weg, um Verwendung zu überprüfen im Gegensatz zu get(..) != null, falls null-Values möglich sind)

  • put(key, value) …​ fügt das Schlüssel-Wert-Paar ein. Falls der Schlüssel schon vorhanden war, wird der Value ersetzt. Daher ist je nach Aufgabenstellung eine Prüfung nötig. Eine Möglichkeit ist Prüfung mit get(key) auf ungleich null, sicherer ist jedoch containsKey(..). Zurückgeliefert wird der zuvor enthaltene Value oder null, wenn der Key davor nicht in Verwendung war.

  • remove(key) …​ entfernt das Element mit dem angegebenen Key und liefert das entfernte Element für Abschlussarbeiten zurück. Wenn Key nicht in Verwendung war, wird null zurückgeliefert

  • keySet() …​ liefert ein (ungeordnetes) Set mit allen verwendeten Keys zurück. Bei erforderlicher Ordnung entweder eine der oben erwähnten Map-Implementation verwenden oder mit dem KeySet eine List generieren, die sortiert werden kann)

  • Iteration mit for (KeyType key : theMap.keySet()) { ElemType el = theMap.get(key); el.someMethod(); …​ }

  • Entfernen von Elementen während Iteration erfordert spezielle Herangehensweise, sonst wird meistens eine ConcurrentModificationException geworfen - siehe später!

Weitere Operationen siehe JavaDoc Map (Java SE 17 & JDK 17) : Oracle bzw. IDE - Vorschlags-Funktionalität

3. Wichtige implementierende Klassen

Die am häufigsten verwendete Implementation ist die HashMap. Sie verwendet analog zu Klasse HashSet die HashCodes der Keys (des KeySets) für eine effiziente Implementation.
Daher ist das Overriding der Methode hashCode() der für die Keys verwendeten Klasse konsistent mit deren equals(..) EXTREM wichtig! Hashset ist die schnellste verfügbare Variante!

Weitere wichtige Implementationen:

TreeMap

(siehe Javadoc TreeMap). Liefert ein sortiertes KeySet (nach in der Key-Klasse implementierter Sortier-Ordnung oder per Konstruktor-Parameter vom Typ Comparator<..>, implementiert das Interface SortedMap)

LinkedHashMap

(siehe Javadoc LinkedHashMap) Liefert ein nach Einfüge-Reihenfolge sortiertes KeySet.

4. Beispiel zu Map

Nun ein konkretes Beispiel: Ein Personencontainer in Form einer Map könnte mit (eindeutiger) E-Mail-Adresse als Key aufgebaut werden. Dann kann durch Angabe der passenden E-Mail-Adresse jede enthaltene Person erreicht werden.

Durch Nutzung des KeySets (alle als Keys genutzten E-Mail-Adressen der enthaltenen Personen) kann die Map vollständig durchiteriert werden.

Hier eine einfache, vollständige Demonstration - Klassen EmailAddr, Person, FriendsBook (mit inkludiertem main(..) und darin aufgerufenen Test-Methoden):

package my.pkg;
import java.util.Objects;

public class EmailAddr {
    private String email;

    private EmailAddr(String email) {  // private - from outside only factory!
        this.email = email;  // no checks here - done in factory!
    }
    public static EmailAddr create(String email) {  // static factory method
        checkValid(email);
        return new EmailAddr(email);
    }
    public static void checkValid(String email) { // only effect is exception if needed
        if (email == null) {
            throw new java.lang.IllegalArgumentException("email is null");  // unchecked
        }
        if (email.isBlank()) {
            throw new java.lang.IllegalArgumentException("email has no content");
        }
        // more checks, see https://en.wikipedia.org/wiki/Email_address
    }
    @Override
    public int hashCode() {   // created by IDE
        return Objects.hash(email);
    }
    @Override
    public boolean equals(Object obj) {   // created by IDE
        if (this == obj)  return true;
        if (!(obj instanceof EmailAddr))  return false;
        EmailAddr other = (EmailAddr) obj;
        return Objects.equals(email, other.email);
    }
    @Override
    public String toString() {
        return email;
    }
    // There is NO SETTER for email, so it is IMMUTABLE. Otherwise the email address
    // could be changed while used as a key!
    // For such cases we would need a "property change event" implementation!
}
package my.pkg;
import java.time.LocalDate;

public class Person {
    private String name;
    private EmailAddr emailAddr;
    private LocalDate birthDate;
    private String info;

    public Person(String name, EmailAddr emailAddr) {
        setName(name);
        setEmailAddr(emailAddr);
    }
    public String getName() { return name; }
    public EmailAddr getEmailAddr() { return emailAddr; }
    public LocalDate getBirthDate() { return birthDate; }
    public String getInfo() { return info; }

    public void setName(String name) { this.name = name; }
    public void setEmailAddr(EmailAddr emailAddr) { this.emailAddr = emailAddr; }
    public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
    public void setInfo(String info) { this.info = info; }
    @Override
    public String toString() {
        return "Person [name=%s, email=%s, birthDate=%s, info=%s]" //
                .formatted(name, emailAddr, birthDate, info);
    }
package my.pkg;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;

public class FriendsBook {
    private Map<EmailAddr, Person> friendsMap;

    public FriendsBook() {
        friendsMap = new HashMap<>(); // accessing Person by EmailAddr hashCode+equals
    }
    public static void main(String[] args) {
        FriendsBook myFriendsBook = new FriendsBook();
        myFriendsBook.populate();
        myFriendsBook.show("after populate(): Ede, Ida");
        myFriendsBook.change();
        myFriendsBook.show("after change(): name Ede -> Edi");
        myFriendsBook.remove();
        myFriendsBook.show("after remove(): ida");
        myFriendsBook.badChange();
        myFriendsBook.show("after badChange(): email ede@.. -> eduard@.. (email != key)");
        System.out.println("Now try to create invalid EmailAddr '   ' :");
        myFriendsBook.produceException();
    }
    public void populate() {
        EmailAddr ea;
        Person p;
        friendsMap.put(ea = EmailAddr.create("ede@nixnet.at"), //
                p = new Person("Ede", ea));
        p.setBirthDate(LocalDate.of(1999, 12, 31));
        p.setInfo("Some Details");
        friendsMap.put(ea = EmailAddr.create("ida@mailth.at"), //
                p = new Person("Ida", ea));
        p.setInfo("Important facts");
    }
    public void change() {
        Person p = friendsMap.get(EmailAddr.create("ede@nixnet.at"));
        p.setName("Edi");
    }
    public void remove() {
        EmailAddr ea;
        Person p = friendsMap.remove(ea = EmailAddr.create("ida@mailth.at"));
        System.out.format("removed key=%s -> %s%n", ea, p);
    }
    public void badChange() {
        // change emailAddr of person while in map -> emailAddr not equals key any more!
        Person p = friendsMap.get(EmailAddr.create("ede@nixnet.at"));
        p.setEmailAddr(EmailAddr.create("eduard@nixnet.at"));
    }
    public void show(String title) {
        System.out.format("FriendsMap (%d items) %s:%n", friendsMap.size(), title);
        for (EmailAddr ea : friendsMap.keySet()) {
            System.out.format("  key=%s -> %s%n", ea, friendsMap.get(ea));
        }
        System.out.println();
    }
    public void produceException() {
        EmailAddr ea = EmailAddr.create("   ");
    }
}

Damit wird folgender Output generiert:

FriendsMap (2 items) After populate with Ede, Ida:
  key=ede@nixnet.at -> Person [name=Ede, email=ede@nixnet.at, birthDate=1999-12-31, info=Some Details]
  key=ida@mailth.at -> Person [name=Ida, email=ida@mailth.at, birthDate=null, info=Important facts]

FriendsMap (2 items) After change name Ede -> Edi:
  key=ede@nixnet.at -> Person [name=Edi, email=ede@nixnet.at, birthDate=1999-12-31, info=Some Details]
  key=ida@mailth.at -> Person [name=Ida, email=ida@mailth.at, birthDate=null, info=Important facts]

removed key=ida@mailth.at -> Person [name=Ida, email=ida@mailth.at, birthDate=null, info=Important facts]
FriendsMap (1 items) After remove ida:
  key=ede@nixnet.at -> Person [name=Edi, email=ede@nixnet.at, birthDate=1999-12-31, info=Some Details]

FriendsMap (1 items) After badChange email ede@.. -> eduard@.. (email != key):
  key=ede@nixnet.at -> Person [name=Edi, email=eduard@nixnet.at, birthDate=1999-12-31, info=Some Details]

Now try to create invalid EmailAddr '   ':
Exception in thread "main" java.lang.IllegalArgumentException: email has no content
	at my.scribble2021q4a.mapdemo.EmailAddr.checkValid(EmailAddr.java:21)
	at my.scribble2021q4a.mapdemo.EmailAddr.create(EmailAddr.java:13)
	at my.scribble2021q4a.mapdemo.FriendsBook.produceException(FriendsBook.java:67)
	at my.scribble2021q4a.mapdemo.FriendsBook.main(FriendsBook.java:26)

5. Erläuterungen zum Beispiel

Wichtige Aspekte EmailAddr:

  • In der Key-Klasse (EmailAddr) muss hashMap() und equals(…​) definiert sein.

  • Die statische Factory-Methode EmailAddr.create("…​") kann VOR Instanzierung des Objekts nötige Aktivitäten durchführen (hier Aufruf von checkValid("…​"))

  • Da Checks in der Factory-Methode erfolgen, sollte der Konstruktor private sein – sonst können ungeprüfte EmailAddr-Objekte erzeugt werden.

  • Die statische Methode checkValid("…​") hat keinen Effekt außer das Werfen einer Exception, wenn ungültige email übergeben wird. Hier wird der Einfachheit halber eine der vielen vom JDK bereitgestellten RuntimeExceptions verwendet: java.lang.IllegalArgumentException (Alle Klassen aus Package java.lang.* werden bei Bedarf automatisch importiert)

  • Methode toString() der EmailAddr liefert einfach die Email-Adresse als String zurück.

Klasse Person enthält keine Besonderheiten.

Wichtige Aspekte FriendsBook:

  • Die Deklaration von friendsMap hat das Interface Map als Typ, damit können Objekte aller implementierenden Klassen bei der Instanzierung verwendet werden – z.B. HashMap, TreeMap (implementiert SortedMap – deren keySet() liefert Keys sortiert), LinkedHashMap (liefert Keys in Einfüge-Reihenfolge).

  • Um von einer statischen Methode (hier main(..)) auf Instanzmethoden zugreifen zu können, muss zuvor eine Instanz der Klasse erzeugt und hinterher verwendet werden:
    FriendsBook myFriendsBook = new FriendsBook();
    myFriendsBook.populate();

  • Die Zuweisung '=' liefert nach Durchführung den zugewiesenen Wert zurück, daher kann bei Parameterübergabe geschrieben werden:
    (nach vorheriger Deklaration EmailAddr ea;):
    friendsMap.remove(ea = EmailAddr.create("ida@mailth.at"));