Java 8: Erweiterung von java.util.Map

von Hubert Schmid vom 2013-11-24

Lambda-Expressions und Default-Methods machen es möglich: Die Schnittstelle java.util.Map erfährt mit Java 8 zahlreiche Erweiterungen, um die Verwendung für Entwickler deutlich zu vereinfachen. Im Folgenden gebe ich einen kurzen Überblick über die wichtigsten Änderungen.

getOrDefault

Die Methode getOrDefault wurde schon lange vermisst. Sie vervollständigt die existierende get-Methode, und besitzt im Vergleich zu Letzteren einen zusätzlichen Parameter, der zurückgegeben wird, wenn kein Eintrag für den angegebenen Key existiert. In vielen Fällen kann man sich damit eine Sonderbehandlung für nicht existierende Einträge ersparen.

Map<EmployeeType, BigDecimal> salaries = ...; BigDecimal salariesForInterns = salaries.getOrDefault(EmployeeType.INTERN, BigDecimal.ZERO); if (salariesForInterns.compareTo(...)) { ...

Die Methode getOrDefault ermöglicht auch die effiziente Unterscheidung zwischen einem Eintrag mit null und einem nicht existierenden Eintrag. Die Klasse im folgenden Listing nutzt diese Möglichkeit für einen Cache, der auch null-Werte unterstützt.

public class CachedFunction<T, R> implements Function<T, R> { private static final Object PLACEHOLDER = new Object(); private final Map<T, R> map = new HashMap<>(); private final Function<T, R> delegate; public CachedFunction(Function<T, R> delegate) { this.delegate = delegate; } public R apply(T arg) { R result = map.getOrDefault(arg, (R) PLACEHOLDER); if (result == PLACEHOLDER) { result = delegate.apply(arg); map.put(arg, result); } return result; } }

putIfAbsent & replace

Bisher enthielt die Map-Schnittstelle nur die put-Methode zum Einfügen und Überschreiben. Mit Java 8 kommen zwei spezialisierte Methoden mit den gleichen Parametern hinzu: putIfAbsent fügt Einträge nur ein, falls noch kein entsprechender Key existiert. replace hingegen ersetzt nur existierende Einträge. Im folgenden Listing werden beide Methoden verwendet, um Gehälter nach Typ zu summieren.

Map<EmployeeType, BigDecimal> salaries = new HashMap<>(); for (Employee e : employees) { BigDecimal oldValue = salaries.putIfAbsent(e.getType(), e.getSalary()); if (oldValue != null) { salaries.replace(e.getType(), oldValue.add(e.getSalary())); } }

Ein wenig irreführend ist die Verwendung der Begriffe absent und present bezüglich null-Werten. present ist gleichbedeutend zu get(key) != null und absent zu get(key) == null. Damit ist die Bedeutung inkonsistent mit containsKey(key).

computeIfAbsent

Die Methode computeIfAbsent ist vergleichbar zu putIfAbsent. Sie fügt einene Eintrag ein, falls er noch nicht existiert. Im Unterschied zu Letzteren wird der Wert jedoch lazy übergeben, und die Methode gibt stets den aktuellen Eintrag zurück. Damit eignet sich die Methode bestens zum Gruppieren von Elementen. So werden beispielsweise im folgenden Listing Mitarbeiter nach Typ gruppiert.

Map<EmployeeType, List<Employees>> groups = new HashMap<>(); for (Employee e : employees) { groups.computeIfAbsent(e.getType(), t -> new ArrayList<>()).add(e); }

Neben computeIfAbsent gibt es auch die beiden Methoden computeIfPresent und compute. Erstere ist jedoch am Interessantesten, da die Sonderbehandlung häufig beim ersten Ereignis pro Key erforderlich ist.

merge

Die Methode merge ist ein wichtiger Spezialfall von compute. Ihr werden sowohl ein Key, ein Value als auch eine binäre Operation übergeben. Falls dem Key noch kein Value zugeordnet ist (d.h. absent ist), wird das übergebene Paar eingefügt. Andernfalls wird der neue Value aus dem Alten und dem Übergebenen mittels der binären Operation berechnet.

Das hört sich kompliziert an, ist aber relativ einfach. Im folgenden Listing werden nochmals Gehälter nach Typ summiert. Weiter oben wurden dafür putIfAbsent und replace verwendet. Doch wie man sieht, lässt sich der gleiche Code mit merge wesentlich einfacher schreiben.

Map<EmployeeType, BigDecimal> salaries = new HashMap<>(); for (Employee e : employees) { salaries.merge(e.getType(), e.getSalary(), BigDecimal::add); }

forEach & replaceAll

Zu guter Letzt sollte man noch die beiden Bulk-Operationen forEach und replaceAll erwähnen. Dabei handelt es sich jedoch lediglich um Convenience-Methoden, da die gleiche Funktionalität auch über entrySet zur Verfügung steht.

Map<EmployeeType, BigDecimal> salaries = ...; salaries.replaceAll((t, v) -> v.setScale(0, BigDecimal.ROUND_HALF_UP)); salaries.forEach((t, v) -> System.out.printf("%-9s %7s €\n", t + ":", v));

Insgesamt handelt es sich um ein gelungenes Erweiterungspaket, das wieder mehr Lust auf die Map der Standardbibliothek macht, und mit der Java wieder zu anderen Bibliotheken und Sprachen aufschließt. Ein längst überfälliger Schritt.