Die Verwendung der Optional Klasse für API Rückgabewerte, macht viele Dinge bei der Verwendung einer API sehr viel einfacher. Beispiele dafür gab es ja schon in dem Beitrag Befreit die APIs von Nullen.
Manches mal sorgt der Optional als Rückgabewert für Verdruss. Im folgenden Beispiel sollen alle Väter der Personen aus der Liste eingesammelt werden.
List<Optional<Person>> fathers = persons.stream().map(Person::getFather).collect(toList());
Da getFather
ein Optional<Person>
zurück liefert, ist das Ergebnis eine Liste von Optional<Person>
. Um eine Liste von Person
zu erhalten, können wir im Stream, aus dem Optional<Person>
eine
generieren. Dafür gibt es verschiedene Ansätze, die aber alle nicht schön ausschauen.Person
persons.stream().map(Person::getFather).filter(Optional::isPresent).map(Optional::get).collect(toList()); persons.stream().map(Person::getFather).filter(p -> p.orElse(null)).map(Objects::noneNull).collect(toList());
Als wortwörtlich letzte Lösung kann aber ein eigener Collector verwendet werden, der die Aufgabe erhält aus einem Stream von Optional<Person> eine Liste von Person zu erzeugen. Das eigene Collectoren nicht besonders schwer zu erstellen sind, zeigt der Beitrag Einsammeln und portionieren mit Stream Collector.
Unser selbstgemachter Collector
ist vom Typ Collector<Optional<T>, ? List<T>>
. Was nichts anderes bedeutet, wir sammeln Elemente vom Typ Optional<T>
ein und speichern die Inhalte dieser Elemente in eine Liste mit dem generischen Typ T.
Eine erste Version ist im folgenden Beispiel zu sehen.
static <T> Collector<Optional<T>, ?, List<T>> toList() { return Collector.of( ArrayList::new, (list, optional) -> optional.ifPresent(t -> list.add(t)), (list1, list2) -> { list1.addAll(list2); return list1; }); }
Der Kern der Lösung ist der Akkumulator
, der das Element o nur dann in die Liste (list, optional) -> optional.ifPresent(t -> list.add(t))
list
einfügt, wenn er nicht leer ist. Mit einem abgewandelten Akkumulator
, erhalten wir einen (list, o) -> list.add(o.orElse(null)
Collector
, der für leere Optional
Instanzen einen NULL Werte in die Liste einfügt.
Mit dem folgenden Test prüfen wir, ob die Implementierung hält, was sie verspricht.
@Test void collect() { List<Optional<String>> list = Arrays.asList(Optional.of("test1"), Optional.of("test2"), Optional.empty(), Optional.empty()); List<Strin> values = list.stream().collect(OptionalsCollectors.toList()); Assertions.assertEquals(Arrays.asList("test1", "test2"), values); List<String> valuesWithEmpty = list.stream().collect(OptionalsCollectors.toListWithEmpty()); Assertions.assertEquals(Arrays.asList("test1", "test2", null, null), values); }
Da sieht ja schon sehr gut aus, aber was ist mit der Wiederverwendung bestehender Kollektoren? Versuchen wir einen Wrapper für bestehende Collector
zu schreiben. Wir übernehmen alle Komponenten des verwendeten Collector
und fügen nur für den Akkumulator die zusätzliche Behandlung der Optional
ein.
static <T, U> Collector<Optional<T>, U, R> withEmpty(Collector<T, U, > wrapped) { return Collector.of( wrapped.supplier(), (x, o) -> wrapped.accumulator().accept(x, o.orElse(null)), wrapped.combiner(), wrapped.finisher(), wrapped.characteristics().toArray(new Characteristics[0])); } static <T, U, R> Collector<Optional<T>, U, R> withoutEmpty(Collector<T, U, R> wrapped) { return Collector.of( wrapped.supplier(), (x, o) -> o.ifPresent(t -> wrapped.accumulator().accept(x, t)), wrapped.combiner(), wrapped.finisher(), wrapped.characteristics().toArray(new Characteristics[0])); }
Durch die zusätzliche Verwendung des generischen Typs R in unserer Methode, sind wir jetzt nicht mehr auf Kollektoren angewiesen, die Listen verwenden. Deshalb erweitern wir unseren Test und prüfen das Sammeln in ein Set
.
@Test void collectToSetWithoutEmpty() { List<Optional<String>> list = asList(Optional.of("test1"), Optional.of("test2"), Optional.empty(), Optional.of("test2"), Optional.empty()); Set<String> valueSet = list.stream().collect(OptionalsCollectors.withoutEmpty(toSet()); Assertions.assertEquals(new HashSet<>(asList("test1", "test2")), valueSet); }
Für alle, die sich die Lösung im Detail anschauen möchten oder vielleicht selber verwenden wollen, wurden die Sourcen dem Gitlab Projekt Stream Collector Utilities hinzugefügt
Nachtrag
Im Zuge der Ankündigung von JFrog, ihr Repository JCenter zu schließen, ist die Bibliothek als Java 11 Binary nach Maven Central migriert.
<dependency> <groupId>de.schegge</groupId> <artifactId>stream-collector-utilities</artifactId> <version>0.1.3</version> </dependency>