Java SE 8: Streams und mehr

Heute mal kein Linux ­čśŐ.
Processing Data with Java SE 8 Streams, Part 1
Java 8 Stream Tutorial - Benjamin Winterberg

Sollte man nur noch Streams verwenden? Nein!: How Java 8 Lambdas and Streams Can Make Your Code 5 Times Slower | Takipi Blog

Funktionales (Streams) vs. Imperatives Programmieren:

Imperatives Programmieren ist ein Paradigma, in welchem genau festgelegt wird, wie und mit welchen Befehlen genau die Maschine/Runtime ausgef├╝hrt werden soll, um die gew├╝nschten Ergebnisse zu erzielen.

Funktionales Programmieren ist eine Form der deklarativen Programmierung, in dem festgelegt wird, was man gerne erreichen m├Âchte und die Maschine/Runtime den besten Weg findet, es zu tun.

Englische Originalquelle: Functional vs Imperative Programming. Fibonacci, Prime and Factorial in Java 8

TL;DR
Bei Collections geht es vorwiegend um Daten, bei Streams um Berechnungen. Mit der java.util.Stream API wurde eine neue Schnittstelle eingef├╝hrt, mit welcher sich SQL-├Ąhnliche Operationen (Definition des gew├╝nschten Ergebnis, keine Implementierung) auf Daten durchf├╝hren lassen - der Nutzer muss triviale Berechnungen nicht mehr selbst durchf├╝hren, stattdessen kann die Stream API genutzt werden. Des Weiteren finden auch Lambda Ausdr├╝cke Anwendung.

Zuerst einmal ein kurzer Einschub in Java NIO (non-blocking IO), dabei seit Version 7. Eine Datei mittels Java einzulesen hat mich schon immer genervt.

byte[] b = Files.readAllBytes(Paths.get("/tmp/test"));
System.out.println(new String(b));

List<String>list = Files.readAllLines(Paths.get("/tmp/test"));

Bei Bedarf auch Zeile f├╝r Zeile.
Generell w├╝rde ich nicht mit NIO Programmieren - Geschwindigkeitsvorteile sind zweifelhaft und der Code wird schwer wartbar. Lieber ein paar Threads pro Connection/User etc, aber das ist ein anderes Thema.

Die Stream API:

public static void main(String[] args) {
    List<String> argList = Stream.of(args).collect(Collectors.toList());
    varargsTest("a", "b", "c");
}

public static void varargsTest(String... strings) {
    List<String> argList = Stream.of(strings).collect(Collectors.toList());
    System.out.println("Done");
}

Mal eben Varargs und Arrays in eine Collection konvertieren - kein Problem.

Predicate:
import java.util.function.Predicate;

public enum IntegerTests {
    
    IS_EVEN(new Predicate<Integer>(){

        @Override
        public boolean test(Integer i) {
            return i%2 == 0;
        }
        
    }),
    IS_ODD( (Integer i) -> {return i%2 != 0;} ),
    IS_ZERO(i -> i == 0);
    
    private Predicate<Integer> predicate;
    
    private IntegerTests(Predicate<Integer> predicate) {
        this.predicate = predicate;
    }
    
    public boolean test(Integer t) {
        return predicate.test(t);
    }

}


Predicate geh├Ârt zu den in Java 8 eingef├╝hrten funktionalen Interfaces. Predicate ist ein Interface mit genau einer Methode, test(t: <T>): boolean. Etwas UML nebenbei kann auch nicht schaden.
Den Lambda Ausdruck hatte ich in Java: Threadkommunikation bereits verwendet, hingegen nicht erkl├Ąrt.
Es verh├Ąlt sich mit den Predicates genau wie mit einem Runnable - es ist immer eine Abstrakte Methode vorhanden, was die Kurzform ganz unten (IS_ZERO) erlaubt. Aber von Anfang an:
Ein Bild sagt mehr als tausend Worte:

  • Parameterliste
  • Methodenrumpf

Lamda Ausdr├╝cke sorgen schon f├╝r mehr ├ťbersichtlichkeit - solange man die Interfaces und dessen Funktion kennt.

Die Tests oben, ob Null, gerade oder ungerade Zahl, kann ich dann fr├Âhlich auf einem Stream anwenden und Daten nach meinen Kriterien filtern:
Doch was ist das? Die Ausgangsdaten liegen als primitives float-Array vor.

Die Stream API in Aktion:

float[] rawDataFloat = new float[] { 0f, 1.0f, 2.0f, 3.0f };
double[] rawData = new double[rawDataFloat.length];

AtomicInteger index = new AtomicInteger(-1);
rawData = Arrays.stream(rawData)
    .map(d -> new Double(rawDataFloat[index.incrementAndGet()]))
    .toArray();

int[] data = Arrays
        .stream(rawData)
        .mapToInt(d -> new Integer((int) d))
        .toArray();

List<Integer> dataCollection = Arrays
        .stream(data)
        .boxed()
        .collect(Collectors.toList());

System.out.println("Minimum: " + Collections.min(dataCollection));
System.out.println("Maximum: " + Collections.max(dataCollection));

System.out.println("Minimum: " + Arrays.stream(data).min().getAsInt());
System.out.println("Maximum: " + Arrays.stream(data).max().getAsInt());

Arrays.stream(data)
    .filter(IntegerTests.IS_ODD::test)
    .forEach(System.out::println);
// oder, falls Ergebnis behalten werden soll
List<Integer> filtered = Arrays.stream(data)
        .filter(IntegerTests.IS_ODD::test).boxed()
        .collect(Collectors.toList());
filtered.stream().forEach(System.out::println);


ergibt die folgende Ausgabe:
Minimum: 0
Maximum: 3
Minimum: 0
Maximum: 3
1
3
1
3


Schritt-f├╝r-Schritt:

float[] rawDataFloat = new float[] { 0f, 1.0f, 2.0f, 3.0f };
double[] rawData = new double[rawDataFloat.length];

AtomicInteger index = new AtomicInteger(-1);
rawData = Arrays.stream(rawData)
    .map(d -> new Double(rawDataFloat[index.incrementAndGet()]))
    .toArray();

Primitive Streams gibt es derzeit nur f├╝r Int, Long, Double. Selbstredend h├Ąlt mich das nicht von der weiteren Datenverarbeitung ab.
Der Zwischenschritt ├╝ber Double ist nicht n├Âtig, ich k├Ânnte gleich Integer nehmen.
Mit Streams kann nicht ├╝ber ein primitives Float Array iteriert werden. Ich erstelle also ein leeres Double Array in der gr├Â├če des Float Arrays, iteriere dar├╝ber mit .stream, in der map-Methode werden daraufhin die Floats nach double konvertiert, das Array wird gef├╝llt.

Iteration macht er von alleine, w├╝rde ich statt stream die Methode parallelStream nutzen; Multithreaded.
8 Bytes mehr, und mein Stream wird Multithreaded (Ordnung nicht mehr garantiert) - was will man noch mehr?


int[] data = Arrays
        .stream(rawData)
        .mapToInt(d -> new Integer((int) d))
        .toArray();

List<Integer> dataCollection = Arrays
        .stream(data)
        .boxed()
        .collect(Collectors.toList());

Konvertierung in ein primitives int Array, danach in eine Integer-List. Wollte ich nur die Liste haben, so m├╝sste ich den Umweg ├╝ber eine zus├Ątzliche tempor├Ąre Variable nicht gehen.


System.out.println("Minimum: " + Collections.min(dataCollection));
System.out.println("Maximum: " + Collections.max(dataCollection));

Collections, Objekte; alles unn├Âtiger Overhead. Andererseits besteht f├╝r den Programmierer ein erheblicher Overhead, wenn dieser st├Ąndig das Rad neu erfinden muss.
Das sch├Âne an Java ist doch gerade die umfangreiche Klassenbibliothek.
Collections ist seit Java 7 verf├╝gbar.


System.out.println("Minimum: " + Arrays.stream(data).min().getAsInt());
System.out.println("Maximum: " + Arrays.stream(data).max().getAsInt());

Filterung, Reduktion mit den Java 8 Streams. Selbes Ergebnis wie oben mit den Collections-Methoden.


Arrays.stream(data)
    .filter(IntegerTests.IS_ODD::test)
    .forEach(System.out::println);
// oder, falls Ergebnis behalten werden soll
List<Integer> filtered = Arrays.stream(data)
        .filter(IntegerTests.IS_ODD::test)
        .boxed()
        .collect(Collectors.toList());
filtered.stream().forEach(System.out::println);

Filterung mit Predicate. In der filter Methode k├Ânnen nun Predicates ├╝bergeben werden. In diesem Fall m├Âchte ich auf ungerade Zahlen filtern, was mit dem oben definiertem Predicate IS_ODD in der test Methode m├Âglich ist.
Collections k├Ânnen direkt ├╝ber die .stream() Methode gestreamt werden.

Die Key-Value Paare einer Map auszugeben war noch nie einfacher:

Map<String, Integer> map = new HashMap() {
    {
        put("a", 123);
        put("b", 456);
    }
};

map.entrySet().stream().forEach(e -> System.out.println(e.getKey() + " " + e.getValue()));

Statische Initialisierung mit { extends anonymous class { static initializer }} aka "Map-Literals" ist verp├Ânt.

Ausgabe:
a 123
b 456


Abschlie├čend bleibt zu sagen, nach diesem kleinen Einblick in die Stream API, es ist wahrlich f├╝r den konkreten Anwendungsfall abzuw├Ągen, ob es lohnenswert ist, Streams zu nutzen. Nichtsdestotrotz bietet die Stream API neue Mittel und Wege, funktionale Programmierung in Java einzusetzen und auf diese Weise Daten aufzubereiten.