Optionales einsammeln

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ückliefert, 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 Person generieren. Dafür gibt es verschiedene Ansätze, die aber alle nicht schön ausschauen.

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 (list, optional) -> optional.ifPresent(t -> list.add(t)), der das Element o nur dann in die Liste list einfügt, wenn er nicht leer ist. Mit einem abgewandelten Akkumulator (list, o) -> list.add(o.orElse(null), erhalten wir einen 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<String> 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, R> 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