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 – sonstnull
(wird auch geliefert, wenn der Valuenull
war!) -
containsKey()
… lieferttrue
, wenn der Key bereits verwendet ist (sicherer Weg, um Verwendung zu überprüfen im Gegensatz zuget(..) != null
, fallsnull
-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 mitget(key)
auf ungleichnull
, sicherer ist jedochcontainsKey(..)
. Zurückgeliefert wird der zuvor enthaltene Value odernull
, 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, wirdnull
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 eineList
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 InterfaceSortedMap
) - 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
) musshashMap()
undequals(…)
definiert sein. -
Die statische Factory-Methode
EmailAddr.create("…")
kann VOR Instanzierung des Objekts nötige Aktivitäten durchführen (hier Aufruf voncheckValid("…")
) -
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ültigeemail
übergeben wird. Hier wird der Einfachheit halber eine der vielen vom JDK bereitgestellten RuntimeExceptions verwendet:java.lang.IllegalArgumentException
(Alle Klassen aus Packagejava.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 InterfaceMap
als Typ, damit können Objekte aller implementierenden Klassen bei der Instanzierung verwendet werden – z.B.HashMap
,TreeMap
(implementiertSortedMap
– derenkeySet()
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 DeklarationEmailAddr ea;
):
friendsMap.remove(ea = EmailAddr.create("ida@mailth.at"));