Freitag, Juni 16, 2017

Java 8 Collection Beispiele | Stream#reduce()

TL;DR: #reduce() nicht anwenden! Zu kompliziert.

Thema heute: Die unterschiedlichen #reduce(...) Methoden der Klasse java.util.Stream. Betrachtet werden die Methoden:

  1. Optional<T> java.util.Stream#reduce(BinaryOperator<T> accumulator)
    
  2. T java.util.Stream#reduce(T identity,
                              BinaryOperator<T> accumulator)
    
  3. <U> U java.util.Stream#reduce(U identity,
                                  BiFunction<U,? super T, U> accumulator,
                                  BinaryOperator<U> combiner)

Der zentrale Parameter accumulator repräsentiert eine Funktion java.util.function.BinaryOperator zur Berechnung der Reduktion. Allen drei Methoden ist dieser Parameter gemein. Was passiert hier? Mathematisch ausgedrückt ist es vielleicht am anschaulichsten. Es sei f() eine Funktion. f() wird auf alle Elemente des Stream rekursiv angewendet.
f(f(f(a0, a1), a2), a3)
Ein andere Darstellung ergibt sich, wenn statt einer Funktion ein (Bi-)Operator op verwendet wird.
a0 op a1 op a2 ...
bzw. ganz allgemein
ai op ai + 1

Die binäre Funktion/der binäre Operator sollte folgenden funktionalen Anforderungen genügen:

  • assoziativ: a op (b op c) = (a op b) op c. Darunter fallen solche Operationen wie die Addition, Multiplikation, Min, Max. Die Subtraktion ist z.B. nicht assoziativ: (20-5)-4 ≠ 20-(5-4). Das gleiche gilt für die Division. Eine Stolperfalle für schnelle Codeänderungen.
  • Nicht störend (non-interfering). Wie kann man das verstehen? Auf "NOSID-Java 8 stream reduce vs collect" findet sich eine gute Erklärung: Die reduce-Methode eignet sich ausschließlich für Reduktionen mit reinen Funktionen. Damit sind Funktionen gemeint, die beim Aufruf weder ihren eigenen Zustand, den Zustand der Übergabeparameter noch irgendwelchen global sichtbaren Zustand ändern. Sie geben lediglich eine Referenz auf ein Objekt zurück, das üblicherweise entweder neu erzeugt wird oder zumindest unveränderlich ist – in jedem Fall aber keine Auswirkungen auf den sonstigen Programmzustand hat.
  • zustandslos. Der accumulator darf keinen inneren, äußeren oder globalen Zustand besitzen. Passt irgendwie gut zu dem letzten Punkt.
Folgt man diesen Anforderungen erhält man eine im mathematischen Sinne echte Funktion.

Fall 1

Optional<T> java.util.Stream#reduce(BinaryOperator<T> accumulator)

Für die #reduce(...) aus 1 ergeben sich folgende Regeln für die Ergebnisberechnung:

  • Leere Streams erzeugen ein leeres Ergebnis. Deswegen wird ein Optional als Rückgabeparameter verwendet.
  • Ein 1-elementiger Stream liefert das erste und einzige Element zurück.
  • Auf mehrelementige Streams wird die BinaryOperator Funktion, wie beschrieben, angewendet.

Fall 2

T java.util.Stream#reduce(T identity, BinaryOperator<T> accumulator)

Das #reduce(...) aus 2 bekommt einen weiteren Parameter identity. Mathematisch erfüllt es den folgenden Zweck: a op id = a. Für die Addition wäre das die 0 oder für die Multiplikation die 1.

  • Leere Streams liefern die identity zurück. Im Gegensatz zu 1 kann das Optional als Rückgabeparameter entfallen.
  • Ein 1-elementiger Stream liefert das erste und einzige Element zurück.
  • Auf mehrelementige Streams wird die BinaryOperator Funktion, wie beschrieben, angewendet.

Fall 3


<U> U java.util.Stream#reduce(U identity,
                              BiFunction<U,? super T, U> accumulator,
                              BinaryOperator<U> combiner)

Dieser Fall wird benötigt, wenn der accumulator in der einfachen Form

BinaryOperator<T>
nicht verwendet werden kann. In der einfachen Form sind Eingangs- und Ausgangsparameter vom selben Typ. In dieser Variante wird ein accumulator mit folgender Signatur erwartet:
BiFunction<U,? super T, U>
Der Rückgabetyp U entspricht dem Typen des ersten Eingabeparameters. Der zweite Parametertyp T entspricht den Datentypen aus dem Stream. Damit sind Akkumulatoren möglich, die z.B. die Längen der Strings eines Streams (Typ String) ermitteln und diese Längen (Typ Integer) komulieren. Dazu gesellt sich ein weiterer Parameter: Der combiner.

Ein accumulator, der die Längen aller Strings aus einem Stream addiert, hat die Eingangsparameter Integer (die Länge aller bisher gelesenen Strings) und String (der aktuell zu verarbeitende String). Der Return-/Ausgangsparameter ist ein Integer. In diesem Fall würde der accumulator der Signatur BiFunction<String, Integer, String> entsprechen. Bei der parallelen Akkumulation des Streams ergibt sich jetzt ein Problem. Im einfachsten Fall wird der Stream in zwei Streams aufgeteilt. Nach Abarbeitung beider Teil-Streams ergeben sich zwei Ergebnisse (jeweils vom Typ Integer). Diese beiden Teilergebnisse müssen nun zusammen geführt werden. Über einen Akkumulator wie in Fall 1 oder Fall 2 ist das nicht möglich, da Ein- und Ausgabeparameter unterschiedliche Typen repräsentieren. Damit ist geklärt, warum hier ein weiterer Parameter aufgeführt wird: Der combiner. Dieser kombiniert die Ergebnisse der Akkumulatoren, wenn der Stream parallel verarbeitet wird. Laut Javadoc wird folgendes für den combiner gefordert:

combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
Passend zu dem Beispiel ein wenig Code:
import org.assertj.core.api.Assertions.*;
import org.junit.*;

List strings = Arrays.asList("Abc", "Def", "Ghi", "Jkl", "Mno");

assertThat(strings.stream().reduce(0,
        (Integer sum, String string) -> sum + string.length(),
        (Integer sum1, Integer sum2) -> sum1 + sum2).intValue()).isEqualTo(15);

Nach der ganzen Theorie nun einige Beispiele. In diesem Fall werden die Elemente einer Liste addiert. Ich empfehle, den Code per Copy und Paste in die bevorzugte IDE zu übernehmen und den Test einmal auszuführen. Das assertThat() stammt aus dem Package org.assertj.core.api.Assertions.*. Der Rest ist wohlbekannt.

assertThat(
    Arrays.asList(10, 20, 30)
          .stream()
          .reduce((result, element) -> result + element)
          .get())
          .isEqualTo(60);

assertThat(
    Arrays.asList(10)
          .stream()
          .reduce((result, element) -> result + element)
          .get())
          .isEqualTo(10);

// Bei der Addition zweier Elemente soll das Ergebnis um 1 erhöht
// werden.
assertThat(
    Arrays.asList(10, 20, 30)
          .stream()
          .reduce((result, element) -> result + element + 1)
          .get())
          .isEqualTo(62);
// Auf das erste Element wird die übergebene Funktion nicht angewendet!

assertThat(
    Arrays.asList(10)
          .stream()
          .reduce((result, element) -> result + element + 1)
          .get())
         .isEqualTo(10);
// Auf das erste Element wird die übergebene Funktion nicht angewendet!

// Nun zusätzlich mit dem Parameter identity
assertThat(
    Arrays.asList(10, 20, 30)
          .stream()
          .reduce(0, (result, element) -> result + element + 1))
          .isEqualTo(63);
// Die Funktion wird auf das erste Element angewendet. In diesem Fall
// wird der result Parameter mit dem identity Parameter vorbelegt.

assertThat(
    Arrays.asList(10)
         .stream()
         .reduce(0, (result, element) -> result + element + 1))
         .isEqualTo(11);

// Ein Beispiel mit Combiner:
assertThat(Arrays.asList("Abc", "Def", "Ghi", "Jkl", "Mno")
         .stream()
         .reduce(0, (Integer sum, String string) -> sum + string.length(),
                    (Integer sum1, Integer sum2) -> sum1 + sum2)
         .intValue())
         .isEqualTo(15);

// Tückisch ...
List strings = Arrays.asList("abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz");

// ... Sequentiell ausgefuehrt ware das noch in Ordnung ...
assertThat(
    strings.stream().reduce(
        new StringBuilder(),
        StringBuilder::append,
        StringBuilder::append).toString()).isEqualTo("abcdefghijklmnopqrstuvwxyz");

// ... die parallele Streamverarbeitung verursacht aber einen Fehler ...
assertThat(
    strings.parallelStream().reduce(
        new StringBuilder(),
        StringBuilder::append,
        StringBuilder::append).toString()).isNotEqualTo("abcdefghijklmnopqrstuvwxyz");
Gerade letztes Beispiel zeigt, dass die naive Verwendung von reduce interessante Fehler verursachen kann. Problematisch in diesem Fall ist die Verwendung von StringBuilder::append, welches ein Element aus dem Stream selbst verändert. Das fällt in die Kategorie interfering. Die Codebeispiele finden sich auch auf GitHub unter Java 8 Examples

Weitere Hinweise und Beispiele:

Donnerstag, Juni 15, 2017

Visual Code | Typescript

Typescript ist die Pflichtsprache für das neue Angular2! Anfänglich war ich skeptisch, ob es sich wirklich lohnt für ein GUI Framework eine neue Sprache zu lernen. Nach einigem ausprobieren, hat mich das Konzept überzeugt. Besonders angetan hat es mir die Typsicherheit. Wer schon mal in einem mittelgroßen Projekt mit unzähligen Klassen und (externen) Schnittstellen mit Javascript programmiert hat, kann das vermutlich verstehen. Als Editor verwende ich VisualCode. Ein Produkt aus dem Hause Microsoft. Kostenlos runterzuladen. Mit einem schicken, schlanken Frontend. Sozusagen die zweite Überraschung. Atom war bisher mein Favorit. Im Zusammenspiel mit Typescript finde ich VisualCode etwas eleganter. Diese Feststellung beruht nicht auf Fakten und wurde auf der Gefühlsebene entschieden.

Hier meine ersten Änderungen an den Default-Einstellungen von VisualCode. Die Einstellungen sind übrigens in JSON Dateien hinterlegt. Im Menü gelangt man über File > Preferences > Settings auf die gewünschte Datei.

Falls für das Web-Projekt nur die *.ts Dateien relevant sind, können die aus den Typescript generierten Javascript Dateien stören. In VisualCode kann eine Einstellung vorgenommen werden, dass der Editor alle Javascript Dateien ignoriert, die mit dem gleichen Prefix beginnen. Beispiel: start.ts und start.js. In der Explorer-Ansicht wird die start.js Datei nicht angezeigt. Die Einstellung kann systemweit oder nur für einen Arbeitsbereich gelten.

{
    "files.exclude": {
        // include the defaults from VS Code
        "**/.git": true,
        "**/.DS_Store": true,

        // exclude .js and .js.map files, when in a TypeScript project
        "**/*.js": { "when": "$(basename).ts"},
        "**/*.js.map": true
    }
}

Mit dem Mai Update verfügt der Editor über eine Minimap. Diese nimmt mir allerdings zuviel Platz weg. Mit der folgenden Einstellung kann man sie deaktivieren:

"editor.minimap.enabled": false

Freitag, April 21, 2017

Links der Woche (16. Woche 2017)

  • Eine DSL zur Formulierung von Datenbankabfragen mit Java: Query DSL
  • Ein Tool zur Überwachung von Servern: Prometheus
  • Falls ihr ab und an mal eine SD Karten bespielt. Hier gibt es ein hübsches, visuelles Tool dazu: Etcher.io
  • Google bietet eine Bibliothek für das Abfragen von Captchas im Web an: reCaptcha Der Algorithmus hinter reCaptcha versucht zu erkennen, dass das Gegenüber ein Mensch ist. In diesem Fall wird kein Captcha eingeblendet.
  • Ein Tool zur Verwaltung von Geheimnissen und Schlüsseln: Vaultproject.io
  • Für eure Mindstorm Roboter gibt es eine neue Firmware: ev3dev.org
  • Momentan bin ich auf der Suche nach einer neuen Blogger Software. Positiv aufgefallen ist mir jekyll. Das Tool findet ebenfalls auf Github Verwendung und generiert die Webseiten zu den Projekten. Siehe auch pages.github.com
  • Auf Heise gibt es ein Video mit ein paar Tipps für den Umgang mit Docker: 5 Docker Tipp
  • Von Mockito gibt es mittlerweile einen 2er Release Branch: Mockito 2.x.x In der 2er Linie wird kein mockito-all mehr angeboten. Also mockito-all durch mockito-core ersetzen und dann mal sehen was fehlt.
  • PhantomJS steht auf dem Abstellgleis. Siehe PhantomJS. Der Maintainer zieht sich zurück. Ursache sind die angekündigten, sogenannten Headless-Versionen, von Chrome und Mozilla, die PhantomJS somit obsolet erscheinen lassen. Nähere Informationen finden sich unter dem Link oben.
  • Zu guter letzt einen Blog: blog.Schauderhaft.de

Donnerstag, April 20, 2017

Oracle SQL | Group by Date

Das folgende Oracle-SQL gruppiert die fehlgeschlagenen Datensätze aus der Tabelle 'tabelle_mit_datum' nach Jahr, Monat und Tag:
SELECT
    COUNT(*) AnzahlFehler,
    TO_CHAR(cs.datum, 'YYYY') Jahr,
    TO_CHAR(cs.datum, 'MM') Monat,
    TO_CHAR(cs.datum, 'DD') Tag,
FROM
    tabelle_mit_datum tmd
WHERE
    tmd.status = 'Fehler'
    AND tmd.datum BETWEEN
        TO_DATE('01.01.2017', 'DD.MM.YYYY')
        AND TO_DATE('30.04.2017', 'DD.MM.YYYY')
GROUP BY
    TO_CHAR(cs.datum, 'YYYY') Jahr,
    TO_CHAR(cs.datum, 'MM') Monat,
    TO_CHAR(cs.datum, 'DD') Tag,
ORDER BY
    TO_CHAR(cs.datum, 'YYYY') Jahr DESC,
    TO_CHAR(cs.datum, 'MM') Monat DESC,
    TO_CHAR(cs.datum, 'DD') Tag DESC
;
Oracles TO_CHAR(date, dateformat) Funktion kann einige tolle Sachen. Der folgende Ausdruck liefert die Woche des Jahres zurück:
SELECT TO_CHAR(sysdate, 'IW') FROM dual;
Folgende Formate stehen als Parameter zur Verfügung:
YEARYear, spelled out
YYYY4-digit year
YYYLast 3 digits of year.
YYLast 2 digits of year.
YLast digit of year.
IYYLast 3 digits of ISO year.
IYLast 2 digits of ISO year.
ILast 1 digit of ISO year.
IYYY4-digit year based on the ISO standard
QQuarter of year (1, 2, 3, 4; JAN-MAR = 1).
MMMonth (01-12; JAN = 01).
MONAbbreviated name of month.
MONTHName of month, padded with blanks to length of 9 characters.
RMRoman numeral month (I-XII; JAN = I).
WWWeek of year (1-53) where week 1 starts on the first day of the year and continues to the seventh day of the year.
WWeek of month (1-5) where week 1 starts on the first day of the month and ends on the seventh.
IWWeek of year (1-52 or 1-53) based on the ISO standard.
DDay of week (1-7).
DAYName of day.
DDDay of month (1-31).
DDDDay of year (1-366).
DYAbbreviated name of day.
JJulian day; the number of days since January 1, 4712 BC.
HHHour of day (1-12).
HH12Hour of day (1-12).
HH24Hour of day (0-23).
MIMinute (0-59).
SSSecond (0-59).
SSSSSSeconds past midnight (0-86399).
FFFractional seconds.

Mittwoch, Februar 22, 2017

GIT Aliase für das Stashing

Git´s stash Befehl ist immer dann hilfreich, wenn die aktuellen Arbeiten am Sourcecode kurzfristig unterbrochen werden müssen. Oder wenn die eingeschlagene Codeänderung (wieder?) auf einen Holzweg führt und man die Änderungen verwerfen möchte (wo ein wegwerfen vielleicht angebrachter wäre?). In der aktuellen GIT Version (2.11.1) gibt es einige neue Parameter für das Stashen.

git config --global alias.stsh 'stash --keep-index'
git stsh
Stash'd nur die geänderten, nicht ge-stage'ten und dem Repository bekannten Dateien. D.h. geänderte und mit 'git add' der Staging-Area hinzugefügte Dateien, werden nicht auf den Stash geschoben.
git stash
Das Default-Stash. Wie gehabt. (Siehe GIT Doku).
git config --global alias.staash 'stash --include-untracked'
git staash
Stash'd alle dem Repository bekannten und nicht bekannten Dateien. Inklusive der Dateien in der Staging-Area.
git config --global alias.staaash 'stash --all'
git staaash
Stash'd alle dem Repository bekannten und nicht bekannten Dateien. Inklusive der Dateien in der Staging-Area. Zusätzlich werden alle Dateien aus der Ignore-List ebenfalls in den Stash mit aufgenommen.

Zusammenfassung

git config --global alias.stsh 'stash --keep-index'
git config --global alias.staash 'stash --include-untracked'
git config --global alias.staaash 'stash --all'
git config --global alias.shorty 'status --short --branch'

Gefunden auf dzone- lesser known git commands.

Mittwoch, Januar 18, 2017

Eclipse Shortcuts

Meine Eclipse Shortcuts. Eine kurze Aufzählung. Und an und ab gibt es mal eine Ergänzung.
F12               Wechselt in den Editor
Ctrl F6           Next View (Ich verlege den Key auf Alt-Q)
Ctrl F7           Schnellauswahl für den Wechsel der View
Ctrl Q            Wechselt zu der letzten editierten Stelle
Ctrl Alt H        Anzeige der Aufruf-Hierarchie einer Methode
Ctrl T            Zeigt die Vererbungshierarchie
Ctrl L            Springt zur Zeilennummer n
Ctrl E            Öffnet ein Popup für die Schnellauswahl eines Editors
Ctrl O            Öffnet ein Popup für die Schnellauswahl einer Methode
Ctrl 1            Quick Fix
Ctrl 3            Quick Access (Suche für Eclipse Aktionen)
Ctrl <Space>      Code-Vorschläge/-Assistent
Ctrl Shift F      Formatiert den Source Code
Ctrl Shift O      Organisiert die Import Statements
Ctrl Shift G      Findet alle Referenzen zu einer Klasse

Alt <crsr up>     Verschiebt die aktuelle Code-Zeile nach oben
Alt <crsr down>   Verschiebt die aktuelle Code-Zeile nach unten
Alt <crsr left>   Wechselt das Tab innerhalb des Editors nach links
Alt <crsr right>  Wechselt das Tab innerhalb des Editors nach rechts
Ctrl <page up>    Wechselt in den zuvor geöffneten Editor
Ctrl <page down>  Wechselt in den nächsten Editor

Alt Shift X       Ausführen einer Aufgabe
Alt Shift X T     Ausführen der JUnit Tests der aktuell ausgewaehlten Klasse
Alt Shift Q       Auswählen einer View
Alt Shift Q S     Öffnet das Suchen Fenster

Samstag, Oktober 08, 2016

git remove unused remote branch

Alte, nicht mehr benötigten GIT Branche entfernen? Dann probier das mal hier:
git branch -r -d origin/devel
git remote prune origin
git fetch origin --prune
Copy and paste solution from stackoverflow

Montag, August 01, 2016

Spring Source Tool 3.8.0, Maven und die Bash

STS 3.8.0, m2Eclipse und das Konfigurationsproblem

Pünktlich zu dem neu erschienen Eclipse NEON hat Pivotal seine Spring-Tool-Suite auf die Version 3.8.0 aktualisiert. Falls ihr Maven für eure Projekte verwendet, stoßt ihr schnell auf den folgenden Fehler:
Description Resource Path Location Type
org.codehaus.plexus.archiver.jar.Manifest.write(java.io.PrintWriter)
pom.xml /rpc-server line 1 Maven Configuration Problem
Einen Fix für dieses Problem gibt es bereits. Die folgenden Software müsst ihr euch per 'Install new software' installieren.
https://otto.takari.io/content/sites/m2e.extras/m2eclipse-mavenarchiver/0.17.2/N/LATEST/

Kommandozeile aus Eclipse heraus aufrufen

Wer Maven verwendet, wechselt vermutlich oft auf die Kommandozeile. Damit das aus Eclipse heraus bequem funktioniert, gibt es das Plugin StartExplorer. Im Eclipse Marketplace sucht ihr nach dem 'StartExplorer'. Um z.B. eine cygwin Shell zu öffnen, verwende ich die folgende Konfiguration.
cmd.exe /c start E:\devtools\cygwin\eclipseshell.bat ${resource_path}
Und die eclipseshell.bat hat den folgenden Inhalt (Der Pfad zur bash.exe muss entsprechend angepasst werden):
@echo off
set current_path=%1
set current_path=%current_path:\=/%
E:\devtools\cygwin\bin\bash.exe --login -c \\"cd '%current_path%' ;
    exec /bin/bash -rcfile ~/.bashrc\\

UPDATE

Das m2Eclipse Problem ist mit der Version 3.8.3 nicht reproduzierbar.

Samstag, Mai 28, 2016

Git und Windows

Bei Neueinrichtung einer GIT Umgebung unter Windows empfehlen sich die zwei folgenden Einstellungen:
git config core.autocrlf true
Konvertiert automatisch das Unix/Linux Zeilenende nach Windows und umgekehrt.
git config core.fileMode false
Ignoriert alle Änderungen an den Sicherheitseinstellungen einer Datei. Und jetzt kann es losgehen.

Sonntag, März 15, 2015

Links der Woche

Das Internet war diese Woche wieder voll. Hier mein Merkzettel:

Java | Template | Thymeleaft

TemplateEngine für Java: Thymeleaft. Die Alternative für das alte Velocity? Mit dem Eclipse-Plugin für Velocity habe ich so meine Probleme. In den aktuellen Eclipse Versionen (z.B. Luna oder das STS von Springsource) habe ich es nicht zum Fliegen bekommen. Was mir besonders gut gefällt, ist die eingebaute Vorschaufunktion von Thymeleaft. Hier ein Beispiel:
<p th:text="#{msg.welcome}">Welcome everyone!</p>
Die Anweisung th:text enfernt den Body aus dem Tag und ersetzt diesen durch das Ergebnis des Ausdrucks msg.welcome. D.h. der HTML Entwickler kann Pseudodaten im HTML Dokument hinterlegen. Die Ersetzungsanweisungen finden sich in den frei definierten Attributen der HTML Elemente wieder. Großer Vorteil: Das HTML Dokument kann jederzeit im Browser betrachtet werden.

Javascript | Seitenübergänge

Javascript, Seitenübergänge oder -fluß nett dargestellt: SmoothState

Javascript | Tabellen in Bootstrap

Javascript, Tabellen für Bootstrap: Bootgrid

Javascript | Swipe Gesten

Javascript, Swipe-Gesten: Brutaldesign

Javascript | Graphics

Javascript Bibliothek für das Zeichnen von Diagrammen (Punktdiagramm, Linendiagramm, Säulendiagramm,...): Metricsgraphic

Javascript | NodeJS

Understanding NodeJS und NodeJS für Anfänger.

Javascript | Build-Tools

NPM als Build-Tool und Grunt ist Mist. Letztere These kann ich unterstützen. Grunts-Konfiguration ist aus meiner Sicht nicht sehr intuitiv. Vielleicht bin ich aber zu sehr ANT-infiziert.

JSF Bashing

Einen interessanten Beitrag zu JSF: Why you should avoid JSF. In vielen Punkten gebe ich dem Autor Recht. Allerdings kann ich mit JSF trotzdem wunderbare Webseiten erstellen, ohne z.B. tief in Javascript einsteigen zu müssen oder zu wissen wie eine Single-Page Applikation funktioniert. Im Gegenzug muss ich verstehen, wie JSF funktioniert. Ebenfalls nicht trivial. Ein Vorteil von JSF gegenüber einer S(ingle)P(age)A(pplication) ist die Behäbigkeit der JSF Spezifikation gegenüber den ziemlich umtriebigen Javascript Frameworks. Was gleichfalls als Nachteil aufgefasst werden kann. Am Ende zählen dann die verschiedenen äußeren Faktoren wie Lebenszyklus, Entwicklungszyklus, Entwicklerteam, Sofware-Plattform, Kunde, etc.

Online | Browser Editor

Artikel verfassen: Hackpad

Java | Versionen...

Eine Übersicht über die verschiedenen Java Versionen: Baeldung Java 8.

Java 8 Collection Beispiele | Stream#reduce()

TL;DR : #reduce() nicht anwenden! Zu kompliziert. Thema heute: Die unterschiedlichen #reduce(...) Methoden der Klasse java.util.Stre...