Variationen der get-Methode von java.util.Map

von Hubert Schmid vom 2013-03-03

Die Klasse java.util.Map existiert seit annähernd 15 Jahre in der Standardbibliothek und wird von unzähligen Java-Entwicklern tagtäglich eingesetzt. Doch die Methode ist nicht unumstritten. Denn die typischen Use-Cases werden durch die Schnittstelle nur unzureichend unterstützt.

Einfach ist die Situation in dem Fall, wo ein Eintrag zu einem gesuchten Schlüssel existiert. Interessanter sind dagegen die Abläufe in allen anderen Fällen, die sich grob wie folgt beschreiben lassen:

  1. Der Aufrufer verwendet null statt dem gesuchten Objekt.
  2. Der Aufrufer benötigt ein anderes statt dem gesuchten Objekt.
  3. Der Aufrufer behandelt den Fall spezifisch.
  4. Es handelt sich um eine Fehlersituation, die gemeinsam mit anderen Fehlern behandelt wird.

null-basierte Schnittstelle

Die vorhandene Map-Schnittstelle gibt einfach null statt dem gesuchten Objekt zurück, wenn der Schlüssel nicht gefunden wird.

public interface Map<K,V> { boolean containsKey(K key); // returns null if key is not found V get(K key); }

Sie unterstützt den ersten Ablauf direkt, der zweite Ablauf lässt sich ein wenig umständlich mit Hilfe von containsKey und eines Conditional-Operators realisieren, und für den dritten Ablauf eignet sich eine vorangestellte if-Bedingung mit containsKey. Lediglich der vierte Ablauf bereitet große Schmerzen, da er häufig vorkommt und aufgrund der schlechten Unterstützung regelmäßig zu unangenehmen NullPointerExceptions an ganz anderen Stellen führt.

Exception-basierte Schnittstelle

Der vierte Ablauf ließe sich einfach mit einer get-Methode realisieren, die eine Exception wirft, falls der gesuchte Schlüssel nicht gefunden wird.

public interface Map<K,V> { boolean containsKey(K key); // throws exception if key is not found V get(K key) throws KeyNotFoundException; }

Allerdings hat diese Schnittstelle das umgekehrte Problem: Die ersten drei Abläufe lassen sich nur sehr umständlich realisieren. Denn Exception-Handling ist in Java nicht dafür geeignet, catch-Blöcke spezifisch den auslösenden Operationen zuzuordnen.

Defaultwert-basierte Schnittstelle

In vielen anderen Programmiersprachen kann der get-Methode der entsprechenden Datenstruktur ein Defaultwert übergeben werden. Dieser wird zurückgeliefert, wenn der Schlüssel nicht gefunden wird.

public interface Map<K,V> { boolean containsKey(K key); // returns defaultValue if key is not found V get(K key, V defaultValue); }

Diese Funktion vereinfacht den zweiten Ablauf, falls das alternative Objekt bereits vor dem Aufruf der get-Methode bekannt ist. Es unterstützt allerdings auch den vierten Ablauf geringfügig, da das Verhalten bei nicht gefundenem Schlüssel offensichtlicher und damit weniger fehleranfällig ist.

Funktionsobjekt-basierte Schnittstelle

Die Variante mit dem Defaultwert funktioniert gut, wenn das Objekt bereits vor dem Aufruf der get-Methode bekannt ist. Ungeeignet ist sie, falls die Bestimmung des Defaultwerts teuer ist. Eine weitere Alternative besteht also darin, den Defaultwert nur bei Bedarf mit Hilfe eines Funktionsobjekts zu bestimmen.

public interface Map<K,V> { boolean containsKey(K key); // returns alternate.apply(key) if key is not found V get(K key, Function<K, V> alternate); }

Die Lambda-Erweiterungen aus Java 8 vereinfachen die Verwendung stark. So erzeugt beispielsweise der folgende Code ein neues File-Objekt, falls zu dem Pfadnamen noch kein Objekt in der Map existiert.

// Map<String, File> fileCache = ...; File file = fileCache.get(pathname, File::new);

Dieser Ansatz unterstützt interessanterweise auch den vierten Ablauf. Im einfachsten Fall reicht der Aufruf map.get(key, null) aus, der offensichtlich eine NullPointerException wirft, falls der gesuchte Schlüssel nicht existiert. Ein wenig expliziter lässt es sich mit Lambda-Ausdrücken umsetzen:

// Map<String, File> fileCache = ...; File file = fileCache.get(pathname, RuntimeException::throw);

Das sieht zwar ganz gut aus, doch die Lambda-Ausdrücke haben zwei Haken: Erstens gibt es sie noch nicht, und zweitens wird es Jahre dauern, bis sie von allen Java-Entwicklern akzeptiert sein werden.

Kombinierte Schnittstelle

Wenn ich aktuell eine zur Map vergleichbare Schnittstelle entwerfen müsste, dann würde ich aus den genannten Gründen zu einer Kombination aus einer Defaultwert- und Exception-basierten Schnittstelle tendieren. Das erfordert einerseits zwar eine weitere Methode, anderseits lassen sich aber alle vier Abläufe einfach realisieren.

public interface Map<K,V> { boolean containsKey(K key); V get(Object key, V defaultValue); V lookup(Object key) throws KeyNotFoundException; }

Wichtig bei diesem Ansatz ist, dass die beiden Methoden sich im Namen unterscheiden, um deutlich zu machen, dass sie sich bei nicht gefundenen Schlüsseln ganz unterschiedlich verhalten. Ich gehe allerdings nicht davon aus, dass wir eine solche Änderung der Map-Schnittstelle in absehbarer Zukunft in Java sehen werden.