Java 8: Erweiterung von java.util.Map
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.