Extremes aus dem Stream

„Wir gelangen nur selten anders als durch Extreme zur Wahrheit – wir müssen den Irrtum – und oft den Unsinn – zuvor erschöpfen, ehe wir uns zu dem schönen Ziele der ruhigen Weisheit hinaufarbeiten.“

Friedrich von Schiller

Die Java Stream API vereinfacht das Arbeiten mit Collections ungemein. Wo früher kompliziert anmutende Schleifenkonstrukte dem Labyrinth-begeisterten Authisten Freude bereiteten, finden sich heute meist elegante Stream Ausdrücke.

Repository.findAll()
  .filter(Person::hasChildren())
  .max(Comparator.comparing(Person::getBirthday))
  .orElseThrow();

Der obige Stream-Ausdruck liefert die jüngste Person in der Datenbank, die Kinder hat. Wird keine Person gefunden, dann wird eine NoSuchElementException geworfen.

Manchmal interessiert man sich für das früheste Geburtstagsdatum und verwendet die Methode min statt max. Aber hin und wieder interessieren der früheste und das späteste Datum.

LocalDate firstHoliday = holidays.stream().min(Comparator.naturalOrder());
LocalDate lastHoliday = holidays.stream().max(Comparator.naturalOrder());

In diesem Fall enthält die Liste holidays nicht die Feiertage eines ganzen Jahres. Sonst wären die Ergebnisse für Deutschland trivial, nämlich Neujahr und der 2. Weihnachtsfeiertag.

Die Liste zweimal zu durchsuchen wirkt nicht sehr elegant. Schöner wäre ein Collector, der beide Werte gleichzeitig einsammelt.

Extrema<LocalDate> extrema = holidays.stream()
  .collect(ExtremaCollector.extrema(Comparator.<LocalDate>naturalOrder()));

Das Beispiel verwendet einen selbstgeschriebenen Collector, der eine Instanz vom Typ Extrema zurückliefert. Die Instanz enthält das kleine und größte Objekt aus dem Stream bzgl. des übergebenen Comparator. In diesem Fall, den Standardvergleich für LocalDate Instanzen.

Die Extrema Klasse ist ein generisches Ergebnisklasse für Minimum und Maximum. Da in leeren Streams keine Extrema existieren, liefern die getMin und getMax Methoden Optional Instanzen zurück.

public final class Extrema<T> {
  private final T min;
  private final T max;

  Extrema(T min, T max) {
    this.min = min;
    this.max = max;
  }

  public Optional<T> getMin() {
    return Optional.ofNullable(min);
  }

  public Optional<T> getMax() {
    return Optional.ofNullable(max);
  }
}

Der dazugehörige ExtremaCollector wird, wie die Collector Instanzen aus dem Beitrag Einsammeln und Portionieren mit Stream Collector, mit der Convenience Methode Collector.of erzeugt.

public static <T> Collector<T, ?, Extrema<T>> extrema(Comparator<T> comparator) {
  return Collector.of(
    () -> new Accu<>(comparator),
    Accu::add, 
    Accu::merge, 
    Accu::value, 
    CONCURRENT, UNORDERED);
}

Der erste Parameter ist ein Supplier um den Akkumulator Accu zum Einsammeln von Minimum und Maximumwerten. Der zweite Parameter dient zum Hinzufügen eines neuen Wertes. Ist der neue Wert kleiner als min, dann ergibt er ein neues Minimum, ist er größer als max ein neues Maximum. Das Einsammeln von Minimum und Maximum ist auch sehr effizient, weil nur ein oder zwei Vergleiche pro Element im Stream durchgeführt werden müssen.

void add(T value) {
  if (min == null) {
    max = min = value;
    return;
  }
  if (comparator.compare(min, value) > 0) {
    min = value;
  } else if (comparator.compare(max, value) < 0) {
    max = value;
  }
}

Der dritte Parameter dient zum Zusammenführen von verschieden Accu Instanzen. Bei paralleler Verarbeitung eines Streams können mehrere Accu Instanzen erzeugt werden, deren Ergebnisse aggregiert werden müssen.

ExtremaCollect<T> merge(ExtremaCollect<T> collector) {
  if (comparator.compare(min, collector.min) > 0) {
    min = collector.min;
  } 
  if (comparator.compare(max, collector.max) < 0) {
    max = collector.max;
  }
  return this;
}

Auch hier ist die Implementierung recht einfach. Der kleinere der beiden min Werte wird das neue Minimum und der größere der beiden max Werte, das neue Maximum.

Der vierte Parameter erzeugt das Ergebnis des ExtremaCollector, indem eine Extrema Instanz mit den entgültigen Werten für min und max erzeugt wird.

Extrema<T> value() {
  return new Extrema<>(min, max);
}

Die Java Stream API ist nicht nur elegant zu verwenden, auch eigene Collector Ergänzungen sind einfach zu erstellen. Wer sich den ExtremaCollector anschauen oder vielleicht sogar verwenden möchte, finden ihn als neues Mitglied der Stream Collector Utilities Familie.