Stream-API in Java SE 8

Stream-API in Java SE 8

Im ersten Teil dieser Serie über Neuheiten in Java SE 8 wurden Lambda Ausdrücke sowie Default-Methoden in Interfaces vorgestellt. Im zweiten Teil wird der ehemalige Neofoniker und jetzige Senior Softwareentwickler bei idealo, Martin Gerlach, eine weitere Neuerung in Java 8 vorstellen: die Stream API. Mit ihr sollen Zugriffe auf Daten vereinfacht werden.

Mit Hilfe der Lambda Ausdrücken und den Default-Methoden ist es leicht möglich Operationen auf (potentiell unendlich langen) Datenströmen (Streams) im funktionalen Stil auszuführen. Ein Beispiel dafür sind z.B. Map/Reduce-Operationen auf Collections. Auch andere, in der Analyse großer Datenmengen übliche Funktionen, lassen sich damit durchführen. Zudem besteht die Möglichkeit, bestimmte Operationen auf einfache Weise automatisch parallel durch mehrere Threads auszuführen zu lassen.

Streams im Überblick und erste Beispiele

Die Package-Dokumentation von java.util.stream gibt einen ausführlichen Überblick über die Stream-API. Darüber hinaus wurden mittles Default-Methoden diverse Interfaces des Collection-Frameworks sowie vereinzelte weitere Interfaces und Klassen um Streaming-Features erweitert, ein Beispiel ist die Methode java.io.BufferedReader.lines(). Einen Überblick über die Erweiterungen des JDK hinsichtlich Streams und Lambdas findet man hier. Eine vollständige Behandlung des Themas würde hier den Rahmen sprengen. Dieser Artikel gibt daher lediglich einige Beispiele.

Ausgangspunkt sind die neuen Default-Methoden java.util.Collection.stream() sowie die Variante für parallele Verarbeitung, .parallelStream(), mittels der man von jeder Implementierung von Collection<E> einen Stream<E> über die Elemente der Collection erhält. Durch Aufruf dieser Methoden findet zunächst noch kein Verarbeitungsschritt statt, sondern es werden mittels verschiedener “Fluent API Calls” die durchzuführenden Operationen als Sequenz von intermediären (“intermediate”) Operationen und maximal einer abschließenden (“terminal”) Operation definiert. z.B.:

List<Point> points;                 // java.awt.Point(int x, int y)

// Map-Reduce

int sumYForPositiveX = points.stream()

.filter(p -> p.x > 0)                    // intermediate

.mapToInt(p -> p.y)                    // intermediate

.reduce(0, Integer::sum); // = .sum()        // terminal

// GroupBy-(Map)-Reduce

Map<Integer, Integer> sumYbySignumX = points.stream()

.collect(Collectors.groupingBy(            // terminal

p -> Integer.signum(p.x),                    

Collectors.summingInt(p -> p.y)));

// Map-Collect

List<Double> distancesFromOrigin = points.stream()  

        .mapToDouble(p -> p.distance(0.0, 0.0))            // i.

        .collect(Collectors.toCollection(ArrayList::new));    // t.

International Iteration bei Java 8

Im Unterschied zum Interface Iterator<E>, mittels dem der Programmierer die Iteration über Iterable<E>-Implementierungen (z.B. auch Implementierungen von Collection<E>) von außen (“External Iteration”) steuern kann (bzw. muss!), liegt der Fokus von Stream<E> auf der Durchführung bestimmter Operationen auf der zugrundeliegenden Datenstruktur. Implementierungen von Stream<E> sowie der Varianten für primitive Typen (IntStreamLongStreamDoubleStream und diverse auf primitive Typen zugeschnittene Methoden) stellen für die zahlreichen Methoden dieses Interfaces Implementierungen bereit, die sich intern um die Ausführung dieser Operationen kümmern und u.a. auch das Iterieren über die Elemente der zu analysierenden Datenstruktur übernehmen. Das nennt man “Internal Iteration”. So wird es dem Entwickler ermöglicht, durch Angabe der verschiedenen Operationen mittels der Streaming-API auszudrücken, was mit seiner Datenstruktur gemacht werden soll und es der im JDK enthaltenen Implementierung der API zu überlassen, wie dies geschieht.

Brückenfunktionalitäten zwischen den beiden Welten bieten BaseStream.iterator() sowie Iterator().forEachRemaining() und Iterable.forEach(). Einen Iterator als Stream-Quelle zu benutzen ist etwas komplizierter (siehe Stackoverflow zum erstenzweiten und dritten).

Eigenschaften von Streams

Streams sind:

Persistenzlos – Streams speichern keine Daten, sondern dienen dazu, Daten von einer Quelle (Datenstruktur, I/O-Ressource, …) durch eine Abfolge (“Pipeline”) von Operationen zu verarbeiten und abschließend zu aggregieren, einer Datensenke zuzuführen, o.ä.

Funktional – Streams verändern ihre Datenquelle nicht, Stream-Operationen erzeugen vielmehr neue Streams mit veränderten Eigenschaften. filter() löscht z.B. keine Elemente aus einer zugrundeliegenden Collection, sondern erzeugt einen neuen Stream, der bestimmte Elemente der Collection auslässt.

Lazy – Intermediäre Operationen sind immer lazy, d.h. sie werden nicht bei ihrem Aufruf, sondern erst bei Aufruf einer terminalen Operation ausgeführt. Dies hat den Vorteil, dass Abfolgen von Operationen, wie z.B. im Falle von “Finde den ersten String mit drei aufeinanderfolgenden Vokalen” (.stream().filter(s -> contains3ConsecutiveVowels(s)).findFirst()), optimiert werden können. In diesem Beispiel muss eben nicht (immer) jeder String untersucht werden.

Möglicherweise unendlich – Streams über Collections sind endlich, aber Streams müssen nicht endlich sein. Durch “abkürzende” (short-circuiting) Operationen (s.u.) wie limit()oder findFirst() sind endliche Berechnungen über unendliche Streams möglich. Es lässt sich auch eine langlaufende terminale Operation vorstellen, die durch zustandslose intermediäre Operationen wie etwa map() und filter() verarbeitete Elemente eines unendlich langen (Input)Streams fortlaufend per Seiteneffekt (s.u.) in eine Datensenke schreibt.

Consumable – Wie im Falle eines Iterators können die Elemente eines Streams nur einmal durchlaufen werden d.h., es kann nur max. eine terminale Operation ausgeführt werden, die dazu führt, dass vorgelagerte intermediäre Operationen (in optimierter Art und Weise) ablaufen. Streams können also vor Aufruf der ersten terminalen Operation nicht zwischengespeichert werden, um weitere terminale Operationen auszuführen:

Stream<Integer> xStream  = points.stream().map(p -> p.x);    // ok    

List<Integer> x1 = xStream.collect(Collectors.toList());    // ok

List<Integer> x2 = xStream.collect(Collectors.toList());    // Boom!

// IllegalStateException – … stream has already been operated upon …

Erlaubt ist hingegen das Speichern der Streamdefinition als Lambda, welches dann mehrfach ausgeführt wird:

Supplier<Stream<Integer>> s = () -> points.stream().map(p -> p.x);   

List<Integer> x1 = s.get().collect(Collectors.toList());    // ok

List<Integer> x2 = s.get().collect(Collectors.toList());    // ok!

Sequentiell oder Parallel – Streams können “parallel sein”, was dazu führt, dass viele (aber nicht alle) Operationen parallel ablaufen. Zum Einsatz kommt dabei der common ForkJoinPool der JVM. Außer über die in der JavaDoc beschriebenen System-Properties kann man leider keinen Einfluss auf den verwendeten Pool nehmen. Weiterhin muss man damit rechnen, dass bestimmte Operationen die Ausführung durch Synchronisationsmechanismen sogar verlangsamen, so wird z.B. sorted() immer nur single-threaded ausgeführt und vorher wie hinterher mittels Barriers synchronisiert. Ist das Sortieren die einzige oder die mit Abstand umfassendste Aufgabe des Streams vor der terminalen Operation, so macht die Verwendung eines parallelen Streams hier keinen Sinn.

Java-8-Stream-APIs erzeugen mit wenigen Ausnahmen wie Collection.parallelStream() per default sequentielle Streams, welche mittels BaseStream.parallel() in parallele umgewandelt werden können. BaseStream.sequential() erklärt umgekehrt einen Stream für sequentiell.

Geordnet oder Ungeordnet – Streams über geordnete Collections wie etwa Listen oder sortierte Sets sind geordnet, Streams über HashSets sind es z.B. nicht. Operationen wie sorted() wandeln einen nicht geordneten Stream in einen geordneten um, und die Operation unordered() ermöglichst es den Stream für nachfolgende Operationen als nicht geordnet zu betrachten, was die parallele Ausführung von Operationen wie distinct() oder gruppierte Reduktion (grouped reduction, “group by”) i.d.R. etwas effizienter macht.

Für identisch geordnete Streams muss die Ausführung einer Kette von Operationen für jeden Stream dasselbe Ergebnis liefern, für ungeordnete Streams muss dies nicht der Fall sein, z.B. muss IntStream.range(0, 3).unordered().map(x -> x*2).collect(Collectors.toList()) bei wiederholter Ausführung nicht unbedingt immer [0, 2, 4] erzeugen, sondern es wäre für jede Ausführung jede Permutation der resultierenden Listenelemente erlaubt. Diese Ordnung zu garantieren, ist für sequentielle Streams fast immer viel einfacher als für parallele!

Eigenschaften von Stream-Operationen

Intermediär oder Terminal – Auf einem Stream können beliebig viele intermediäre Operationen zum Einsatz kommen und maximal eine terminale Operation. Intermediäre Operationen wie map(), filter(), distinct(), sorted() usw. geben daher wieder einen Stream zurück, auf dem dann durch Aufruf der Fluent-API-Methoden weitere Operationen ausgeführt werden können. Terminale Operationen wie reduce()collect(), findFirst(), findAny() usw. konsumieren ihren Stream indem sie alle intermediären Operationen ausführen und dann ein endgültiges Ergebnis (keinen Stream) liefern.

Short-circuiting– Stream-Operationen sind abkürzend, wenn sie einen Stream “verkürzen”. Sie können intermediär sein wie skip()und limit() oder terminal wie findFirst() und findAny().

Stateless oder Stateful – Manche intermediären Stream-Operationen müssen einen internen Zustand halten, um ihr Ergebnis zu berechnen. Beispiele dafür sind limit(), skip(), distinct(), sorted(). Manche dieser Operationen, z.B. distinct() und sorted(), die den ganzen Stream sehen müssen, um ein korrektes Ergebnis zu liefern, funktionieren daher generell nicht besonders gut auf parallelen Streams (s.o.). limit() und skip() sind nur dann problematisch, wenn ein geordneter Stream parallel bearbeitet werden soll, da es dann wichtig ist, welche Elemente einbezogen bzw. übersprungen werden.

Seiteneffekte – Prinizipiell kann jede Stream-Operation Seiteneffekte erzeugen, indem sie außerhalb ihrer Lambda-Scopes liegende Objekte verändert, was allerdings generell nicht zu empfehlen ist. z.B.:

List<String> stringList = new ArrayList<>();
points.stream().forEach(p -> results.add(p.toString())); // ARGH!

Dies ist ein vermeidbarer Seiteneffekt. Anstelle von forEach(Consumer) (was im übrigen auch für geordnete Streams die Reihenfolge im Gegensatz zu forEachOrdered(Consumer) nicht einhalten muss!) sollte hier besser collect(Collector) verwendet werden:

List<String> stringList = points.stream()

        .map(Object::toString)         // or: p -> p.toString()

        .collect(Collectors.toList());

Ein nicht vermeidbarer Seiteneffekt ist z.B. die Ausgabe auf der Console, bzw. generell das Schreiben in Datensenken. Außerdem gibt es in der Kategorie noch die intermediäre Operation peek(Consumer):

List<Point> positiveX = points.stream()

        .peek(System.out::println) // all

.filter(p -> p.x > 0)

.peek(p -> System.out.println(„** “ + p)) // only filtered

.collect(Collectors.toList());

Das peek()-Beispiel zeigt übrigens auch sehr schön, in welcher Reihenfolge die Operationen tatsächlich ausgeführt werden, man achte dazu auf die Position der mit “**” beginnenden Ausgaben, der aus der Liste [ Point(1,1), Point(1,-1), Point(-1,-1), Point(-1,1) ] herausgefilterten Punkte mit positiver X-Koordinate:

java.awt.Point[x=1,y=1]

** java.awt.Point[x=1,y=1]

java.awt.Point[x=1,y=-1]

** java.awt.Point[x=1,y=-1]

java.awt.Point[x=-1,y=-1]

java.awt.Point[x=-1,y=1]

Wie man sieht, werden in einem sequentiellen Stream zunächst für jedes Listen-Element alle relevanten Stream-Operationen ausgeführt bevor mit dem nächsten Listen-Element fortgefahren wird. Weiterhin finden die Ausgaben nur statt, wenn eine terminale Operation wie hier collect() in der Pipeline vorhanden ist.

Reduce vs. Collect oder Immutability vs. Mutability

Die Operation collect() im vorigen Beispiel ist eigentlich wenig “funktional”, da sie eine durch Collectors.toList() intern) initial erzeugte Liste durch die Operation verändert, nämlich mit Werten befüllt.

Im Gegensatz dazu geht reduce() von nicht veränderlichen (immutable) Objekten aus, die schrittweise von der eigentlichen Reduktionsfunktion (z.B. Aufsummierung, Konkatenierung) zu neuen nicht veränderlichen Objekten und schließlich zum Ergebnis zusammengefügt werden.

Beispiele von String-Repräsentationen

Folgende Beispiele zum Zusammenfügen von String-Repräsentationen (Strings sind in Java unveränderliche Objekte) der Elemente der Punkteliste aus den vorangegangen Beispielen verdeutlichen den Unterschied:

1. String-Konkatenation

String pointsStr = points.stream().reduce(„“,     // Initial value

(String s, Point p) -> s + “ “ + p,         // Accumulator

(String s1, String s2) -> s1 + s2));        // Combiner

Die Kombinationsfunktion (Combiner) benötigt man nur für parallele Ausführung, da hier die Reduktion zunächst auf Teillisten erfolgt und die Teilergebnisse anschließend kombiniert werden müssen.

Natürlich weiß schon ein Java-Anfänger, dass String-Konkatenation keine besonders performante Operation ist. Der Compiler optimiert bei langen “+”-Ketten zwar durch interne Benutzung von StringBuilder, aber in diesem Fall ist das nur innerhalb der Lambdas für die Einzelteile möglich, so dass hier im Endeffekt anstelle vieler String-Objekte viele StringBuilder-Objekte erzeugt und wieder verworfen werden. Besser wäre vielleicht dieser Ansatz:

2. Reduce mit StringBuilder

String pointsStr = points.stream().reduce(new StringBuilder(),

StringBuilder::append,     // (sb, point) -> sb.append(point)

StringBuilder::append)    // (sb, sb) -> sb.append(sb)

.toString();                // make String from StringBuilder

Nun werden dem initialen Wert, einem neuen StringBuilder-Objekt mittels append() die String-Repräsentationen der Listen-Elemente der Reihe nach hinzugefügt. Das funktioniert allerdings nur, weil append() eine “Funktion” ist, sie gibt nämlich als Ergebnis den (veränderten) StringBuilder wieder zurück. Außerdem funktioniert dieser Ansatz nur sequentiell! Denn wenn derselbe(!) initiale Wert in parallel ausgeführten Verarbeitungsschritten (points.parallelStream().reduce(…)) durch append() verändert wird, führt das mindestens dazu, dass höchstwahrscheinlich die Ordnung der Liste nicht eingehalten wird, aber in der Regel endet dieser Versuch mit einer Exception, da StringBuilder-Objekte nicht threadsafe sind.

Für solche Fälle gibt es in Java 8 daher collect() – auch “mutable reduction” genannt:

3. Collect mit StringBuilder

String pointsStr = points.parallelStream()

.collect(StringBuilder::new,

StringBuilder::append, StringBuilder::append)

.toString();

Der Unterschied zu reduce() ist, dass hier als Startwert gar kein Datenwert sondern eine Funktion (Lambda Ausdruck oder Methodenreferenz) erwartet wird, die für jeden parallelen Verarbeitungsteil das eigentliche (veränderliche) Startobjekt erzeugt (Supplier). Hier startet also jeder parallele Teil mit einem eigenen StringBuilder. Des Weiteren sind Akkumulator und Combiner hier keine BiFunctions, sondern BiConsumers, die keine Rückgabewerte erwarten bzw. Rückgabewerte, wie etwa von append(), verwerfen.

Die Klasse Collectors stellt viele nützliche Collector-Implementierungen zur Benutzung mit der Stream-Methode collect(Collector) bereit. Collector-Objekte fassen die o.g. Akkumulations- und Kombinationslogik zusammen.

Als einführende und weiterführende Literatur zu Streams seien als zwei von vielen diese Quellen genannt:

  • Kurz und bündig: Die Package-Beschreibung von java.util.stream.
  • Ausführlich: Richard Warburton, “Java 8 Lambdas – Functional Programming for the Masses”, O’Reilly

Des Weiteren danke ich Angelika Langer und Klaus Kreft für einen inspirierenden Lambda- und Streams-Hackathon am 6.11.2014 in Berlin, aus dem das eine oder andere Beispiel in veränderter Form für diesen Artikel verwendet wurde.

Die neue Date/Time-API in Java 8

Entwicklern, die zuvor bereits mit der Bibliothek Joda-Time gearbeitet haben, werden die neuen APIs im Package java.time und dessen Subpackages bekannt vorkommen. In der Tat ist der Hauptentwickler von Joda-Time, Stephen Colebourne, einer der Specification Leads des JSR-310.

Wie Oracle selbst beschreibt, bestehen die wesentlichen Schwachpunkte der bisherigen API, hauptsächlich DateCalendar und SimpleDateFormat, insbesondere darin, dass Objekte dieser Klassen nicht threadsafe sind (Objekte von Date und Calendar sind mutable aber nicht synchronisiert und SimpleDateFormat ist intern unnötig zustandsbehaftet), sowie in einigen nicht-intuitiven Design-Entscheidungen, die noch auf frühe Java-Tage zurück gehen. Zum Beispiel repräsentiert der Wert 0 für das Jahresfeld in der Date-Klasse das Jahr 1900 und die Monate sind nicht von 1 bis 12 nummeriert, sondern von 0 bis 11. Zwar wurde mit der Calendar-Klasse in Java 1.1 eine etwas flexiblere, für Internationalisierung geeignetere Lösung (inkl. Zeitzonen und nicht-gregorianischen Kalendern) eingeführt und die Date-Klasse im Prinzip zum Wrapper um die Unix-Epoche (Anzahl der Millisekunden seit dem 1.1.1970 0:00 UTC) degradiert, aber auch Calendar ist nicht threadsafe und mit unter nicht sehr intuitiv zu benutzen.

Die Klassen des Packages java.time folgen dagegen den Prinzipien Immutability (“Value Classes”) und Domain Driven Design, insb. der Trennung von ortsabhängigen Datumswerten und ortsunabhängigen Zeitbegriffen, also genau von “Date” und “Time”.

Weiterhin wurde auf eine lesbare, intuitiv verständliche API geachtet, wie folgendes Beispiel von Datumsarithmetik über den Beginn der mitteleuropäischen Sommerzeit hinweg zeigt:

LocalDateTime savingTest =

LocalDateTime.of(2014, Month.MARCH, 28, 23, 30);

savingTest.plusHours(48); // „2014-03-30T23:30“

savingTest.plusDays(2);   // „2014-03-30T23:30“

 

ZonedDateTime atZone = savingTest.atZone(ZoneId.of(„Europe/Berlin“));

atZone.plusHours(48);    // „2014-03-31T00:30+02:00[Europe/Berlin]“

atZone.plusDays(2);     // „2014-03-30T23:30+02:00[Europe/Berlin]“

Abgesehen davon, dass die “plus”-Operationen hier jeweils neue Objekte erzeugen (im Kommentar jeweils die durch .toString() generierte String-Repräsentation der neuen Objekte), ist deren Semantik auch sehr verständlich:

Liegt keine Information zur Zeitzone vor (LocalDateTime), so entsprechen 48 Stunden immer zwei Tagen.

In Zeitzonen mit Sommerzeit (ZonedDateTime) ist dies um die Umstellungszeitpunkte herum aber nicht immer der Fall. Hier geht man von der intuitiven Interpretation der addierten Zeiträume aus:

  • Für “48 Stunden” zählt man die Uhr 48 Stunden vorwärts und dabei wird zur Umstellung Ende März für die MESZ dann eine Stunde übersprungen, so dass man bis zum 31.3. 0:30 zählt.
  • Addiert man zu einem beliebigen Zeitpunkt jedoch “2 Tage”, so geht man von der selben Uhrzeit “übermorgen” aus, also 30.3. 23:30. Dies sind im angegebenen Beispiel jedoch nur 47 Stunden vom Ausganszeitpunkt (“atZone”) gesehen.

Analog:

LOG.info(atZone);        // „2014-03-28T23:30+01:00[Europe/Berlin]“

Period twoDays = Period.ofDays(2);                    // „P2D“

Period.between(                                    // „P2D“

atZone.toLocalDate(), atZone.plusDays(2).toLocalDate());

Duration fortyEightHours = Duration.ofDays(2);            // „PT48H“

Duration.between(atZone, atZone.plusDays(2));             // „PT47H“

Period (“P”, Datumsunterschied in Tagen) und Duration (“Time Period” bzw. “PT”, Zeitunterschied mit Nanosekunden-Präzision) sind dabei die beiden zentralen Klassen für Zeiträume, mit gemeinsamem Interface TemporalAmount.

Zusätzlich werden auch andere “Chronologies” als das ISO-8601-System wie “Japanese” oder “ThaiBuddhist” unterstützt. Die entsprechenden Klassen befinden sich im Package java.util.chrono.

Den alten APIs wurden Konvertierungsfunktionen hinzugefügt, z.B.:

static GregorianCalendar GregorianCalendar.from(ZonedDateTime)

static Date Date.from(Instant)

ZonedDateTime GregorianCalendar.toZonedDateTime()

Instant Calendar.toInstant()

Instant Date.toInstant()

Instant-Objekte repräsentieren dabei – ähnlich wie Date, nur eben immutable – Zeitpunkte der Unix-Epoche mit Nanosekunden-Präzision. Intern werden diese Zeitpunkte durch je einen long-Wert für die Sekunden der Epoche sowie einen int-Wert für den Nanosekundenbruchteil (0 … 999.999.999) repräsentiert.

Weitere Java 8-Neuerungen

Alle Neuerungen des JDK 8 sind unter What’s New in JDK 8 nachzulesen, hier eine kleine Auswahl:

Hinsichtlich Concurrency wurde neben der Unterstützung paralleler Streams (siehe Teil 2), das Collections-Framework massiv um Interfaces, Klassen und Methoden erweitert. Siehe Package java.util.concurrent (mit Subpackages) sowie z.B. ConcurrentHashMap als Beispiel für eine so erweiterte Klasse (über 30 neue Methoden). Außerdem gibt es mit java.util.concurrent.atomic.LongAccumulatorLongAdderDoubleAccumulator, sowie DoubleAdder durchsatzoptimierte neue Werkzeuge zum parallelen aktualisieren (speziell addieren bzw. aggregieren) von Zahlenwerten durch zahlreiche Threads.

In der HotSpot JVM wurde der PermGen Space durch den sog. “Metaspace” (native memory) ersetzt, das Compilerflag -XX:PermGenSpace ist damit unnötig und wirkungslos.

Es ist nun möglich vom Compiler Informationen zur Reflection für Parameternamen (javac -parameters) im Bytecode hinterlegen zu lassen. Einige Frameworks, wie z.B. Spring, werden davon profitieren, denn bisher war hierfür das Kompilieren im Debug-Modus nötig.

Repeatable Annotations können nun ohne ihre Container-Annotation geschrieben werden (z.B. mehrere @Schedule Annotationen in Spring Batch oder mehrere @CompoundIndex Annotationen in Spring Data), allerdings handelt es sich hier nur um vereinfachte Syntax und keine Änderung im Bytecode, d.h. die Reflection erfolgt nach wie vor über die weiterhin benötigte Container-Annotation.

Einige Funktionen für vorzeichenlose Ganzzahlarithmetik und Parsing für Long und Integer wurden hinzugefügt. Hierbei handelt es sich nicht um neue Typen! Zum Beispiel:

// This raised and still raises a NumberFormatException

long s = Long.parseLong(„ffffffffffffffff“, 16); // Exception !!!

// New in Java 8

long u = Long.parseUnsignedLong(„ffffffffffffffff“, 16); // Now OK!

LOG.info(Long.toString(u)); // „-1“ … oops?! See below!

LOG.info(Long.toUnsignedString(u, 16)); // „ffffffffffffffff“ … ah!

Hier ist zu beachten, dass die Variable u intern tatsächlich den Wert -1 hat. Dies ist die vorzeichenbehaftete Interpretion des Bitmusters aus 64 Einsen bzw. 16 mal “f” im Hexadezimalsystem im Wertebereich [ -263, 263 – 1 ] (64 Bit), also genau im Wertebereich von Java long Werten. Im Wertebereich [ 0, 264 – 1 ] (vorzeichenlose 64 Bit), würde das Bitmuster tatsächlich als 264 – 1 interpretiert werden.

Im JDK8 gibt es nun (endlich!) einen Base64 Encoder und Decoder. JavaFX und die Security-Packages wurden erweitert und an diversen Stellen wurden Performance-Verbesserungen erzielt. Es gibt außerdem 3 “Compact Profiles” (Java-Untermengen) für die Unterstützung entsprechender Devices und Appliances, sowie eine auf der JVM basierende und dadurch mit Java interaktionsfähige Javascript Engine namens Nashorn (“jjs”).

Die Version 8 der Sprache Java und des JDK bringt umfassende Neuerungen. Zentrale Punkte sind die Unterstützung von funktionalen Elementen wie Lambda-Ausdrücken und Streams. Um die bestehenden APIs, insb. das Collection-Framework, hiermit “aufzurüsten”, wurden Default- und Static-Methoden auf Interfaces eingeführt. Die neue Date/Time-API, inspiriert von der Bibliothek Joda-Time, folgt ebenfalls wichtigen in der funktionalen Programmierung üblichen Design-Prinzipien wie z.B. Immutability (Value Classes). Multicore-Computing wird durch parallele Streams und Concurrency-Erweiterungen unterstützt.

Alles in allem werden dem Entwickler damit mächtige und einigermaßen moderne Werkzeuge in die Hand gegeben.

Schwachpuntke Java 8

Trotzdem gibt es – gerade hinsichtlich Lambdas und Streams – noch einige Schwachstellen:

Zum einen führt Javas Typsicherheit dazu, dass die Zuweisung eines Lambda-Ausdrucks an Variablen und Parameter nur über funktionale Interfaces möglich ist, die u.U. inkompatibel zu anderen funktionalen Interfaces sind, für die der Lambda-Ausdruck aber ebenso eingesetzt werden kann!

Zum anderen muss Code, welcher so typisierte Lambdas (etwa als Function<T, R> oder IntFunction<T>) entgegen nimmt, beim Aufruf der Funktion den Methodennamen des funktionalen Interfaces benutzen, da die Java-Syntax (noch) keine reinen Funktionsaufrufe kennt. Z.B.:

IntFunction<String> f = String::length; // or: f = s -> s.length();

int len = f(myString); // That would be nice but does not compile

int len = f.apply(myString); // Need to name the method :/

Es kann nicht f(myString) aufgerufen werden, sondern es muss f.apply(myString) verwendet werden, da die abstrakte Methode von IntFunction<T> eben int apply(T) ist.

Zu Guter Letzt sollte man einmal darauf achten, wie viele (nämlich die meisten) der neuen Interfaces, Klassen und Methoden zur Verwendung von Lambdas und Streams nur der Unterstützung der primitiven Java-Typen int, long und double dient (boolean, byte, short und float wurden berechtigterweise gleich außen vor gelassen). Siehe auch das vorige Beispiel, hier wurde IntFunction<String> statt Function<String, Integer> verwendet, da Java Generic Type Parameter keine primitiven Typen unterstützen und man hier aber eben auf jeden Fall int (und nicht Integer) erhalten möchte.

Performance ist hierbei mittlerweile kaum noch ein Argument, da Autoboxing-/-unboxing auf modernen Rechnerarchitekturen kaum noch einen Unterschied zur reinen Verwendung primitiver Typen machen dürfte.

Nur über “null”-Werte müsste man sich noch etwas Gedanken machen, dann stünde einer Abschaffung von primitiven Typen nicht mehr viel im Wege (s. z.B. folgenden etwas älteren Artikel: “For the Java Development Kit (JDK) 10 or after, a fundamental change is being discussed: making the Java language Object Oriented. This might see the introduction of a unified type system that turns everything into objects and means no more primitives.”)

Vorteile von Java 8 für Unternehmen

Die o.g. Neuerungen und die sich daraus ergebenden Perspektiven überwiegen die genannten Schwachstellen bei Weitem.

Für Unternehmen mit eigener Softwareentwicklung lohnt sich ein schrittweiser Umstieg auf Java 8 aus mehreren Gründen:

Die neuen Sprach-Features schließen eine Lücke zu den sich insbesondere in der Verarbeitung großer Datenmengen und der Datenanalyse (“Big Data”) immer weiter verbreitenden JVM-Sprachen mit funktionalen Features, die nicht an einen abwärtskompatiblen Standard gebunden sind (Clojure, Scala, usw.) sowie zu JavaScript, welches sich auch serverseitig immer stärkerer Beliebtheit erfreut.

Für erfahrene Java-Entwickler und Anwendungs-Architekten sind die Neuerungen leicht zu erlernen und mit etwas Grundwissen über funktionale Prinzipien ebenso leicht im Alltag anzuwenden. Dabei geht es nicht nur um lokale Optimierungen von Code-Stellen zwecks Lesbarkeit, sondern, mit zunehmender Routine, nach und nach auch um die Weiterentwicklung ganzer Anwendungsarchitekturen im Hinblick auf Multicore- und Cluster-Computing, Performance, Skalierbarkeit und Elastizität. Funktionale Prinzipien wie Immutability, die daraus unmittelbar folgende Threadsafety, sowie Streaming usw., erleichtern zusammen mit Techniken der reaktiven oder ereignisorientierten Programmierung sowie neuartigen Virtualisierungslösungen (Infrastructure as Code, as a Service, …) die Erstellung effizienter, skalierbarer, und wartbarer Systeme.

Überblick über alle Neuerungen im JDK 8: What’s New in JDK 8.

Veröffentlicht am 24. November 2014, aktualisiert am 18. Oktober 2020

Teilen auf

Martin Gerlach

Martin Gerlach arbeitet seit 2013 als Senior Softwareentwickler im Bereich Angebotsimport bei idealo, Deutschlands größtem Preisvergleichsportal. Dort befasst er sich mit der Entwicklung perfomanter, skalierbarer Datenimport-, Transformations- und Analysetools. Sein besonderes Interesse gilt dabei verteilten Frameworks für Streaming und Analyse großer Datenmengen sowie damit einhergehenden „funktionalen“ Ansätzen in der Programmierung. Vorher war Martin über 8 Jahre lang für IBM und 5 Jahre für Neofonie sowohl in Forschung und Entwicklung als auch in Kundenprojekten tätig. Er ist Master of Science Absolvent der HAW Hamburg in Informatik mit Schwerpunkt „Verteilte Systeme“.

Digitales in guten Händen – Markenwerte und Branding bei Neofonie

Was bedeutet der Slogan „Digitales in guten Händen“? Anja Unterberger-Schneck, Teamlead Marketing bei Neofonie, gibt Einblicke ins Branding.

axel-ionone

Neofonie beim BVDVA-Kongress 2022

Axel Hillebrand, UX Consultant stellt im Vortrag Methoden zur Gewinn- und Imageoptimierung mittels User Experience beim BVDVA-Kongress 2022 online vor.

QuratorConference_News

ontolux auf der Qurator-Konferenz in Berlin

Auf der Qurator-Konferenz in Berlin versammeln sich KI-Experten, um über digitale Kuratierungsmethoden im Einsatz für Unternehmen zu diskutieren. Dr. Till Plumbaum von ontolux moderiert das Q&A Panel.

newsbox-shopwareLizenzen

Neue Lizenzmodelle bei Shopware

Shopware ändert seine Lizenzmodelle. Mit Rise, Evolve und Beyond werden die bisherigen Professional- und Enterprise-Pakete abgelöst. Mehr… 

ION ONE bei der CX1 World Conference

Grant McGillivray und Björn Andresen von ION ONE richten einen Roundtable zum Thema Nutzerzentrierung für KMUs bei der CX1 World Conference 2022 aus, einem der größten Online-Branchenevents.

Newsbox_dmexco22

Neofonie auf der DMEXCO

Die größte Digitalmesse Deutschlands öffnet in wenigen Tagen die Tore. Neofonie ist vor Ort und berät unabhängig zu Content Management- und E-Commerce-Systemen und zeigt Praxislösungen für den Mittelstand auf.

Füchse_App_Header_Case

Füchse Berlin stellen neue Fan-App vor

Die Füchse Berlin präsentieren ihre neue Fan-App, die durch eine hohe Performance, neue Features und ein ganz neues Nutzererlebnis überzeugt.

Header_KI_Forschung_ontolux_christopher-burns-Kj2SaNHG-hg-unsplash_08_22 (1)

KI & Forschung 08-2022

Die neue Ausgabe der „KI & Forschung“ unserer KI-Agentur ontolux ist da: Dieses Mal hinterfragen sie die Gefahr von KI und thematisieren Deepfakes und Dall-E 2.

Digitale Technologien im Wandel bei Neofonie

Digitale Technologien haben ein enormes Wachstumspotenzial. Im Interview mit Ender Özgür, Head of Software Factories bei Neofonie, beleuchten wir den Wandel.

gartner_quadrant_DigitalCommerce

Neuer Gartner Magic Quadrant Digital Commerce 2022

Gartner veröffentlich jährlich den Magic Quadrant für Digital Commerce. Mit dabei in 2022 sind alle Commerce Partner von Neofonie.

KI & Forschung 07-2022

Die Juli-Ausgabe von KI und Forschung von ontolux gibt Tipps, wie die Sommerzeit genutzt werden kann, um sich näher mit KI und NLP zu beschäftigen.

Neofonie – Aufräumen mit Vorannahmen über den Agenturalltag

Wir räumen mit den Vorurteilen gegenüber dem Agenturleben auf. Interview zum Arbeitsleben in der Digitalagentur Neofonie mit Tim Priwe und André Hirsinger.

ontolux - Qi Wu - hält Vortrag bei der Berlin Buzzwords 2022

ontolux bei der M3 und Berlin Buzzwords – Rückblick

ontolux stellte Methoden für ressourcensparende KI-Modelle bei der Minds Mastering Machines und Berlin Buzzwords im Juni 2022 vor. Ein Rückblick

TextMining_Illustration

Neues Forschungsprojekt Text2Tech

Ziel ist die Erforschung und Entwicklung automatisierter Methoden zur Technologiebeobachtung am Beispiel der Automobilbranche.

Work-Life-Balance-Maßnahme Sabbatical bei Neofonie

Sabbatical als ein Work-Life-Balance-Instrument kann eine Auszeit vom Job ermöglichen und Träume erfüllen. Markus von Neofonie schildert seine ganz persönlichen Erfahrungen zum Thema.

Header_Fuechse

Neofonie Mobile ist offizieller Partner der Füchse Berlin 

Der Handball-Bundesligist Füchse Berlin erneuert sein digitales Angebot und holt sich Neofonie Mobil als offizieller App-Partner der Füchse Berlin an Board.

Google Analytics 4 kommt – Was jetzt zu tun ist

Das Webanalyse-Tool Google Analytics, Universal Analytics (GA3) wird am 01. Juli 2023 komplett auf Google Analytics 4 (GA4) umgestellt. Ivo Stechow – SEO-Experte bei Neofonie sagt wie die Umstellung gelingt.

KI und Forschung 6-2022

KI & Forschung 06-2022

Die Diskussion, ob Sprachmodelle gefährlich werden und KI den Menschen überflügeln und selbständig werden, ist wieder entfacht. In unserer monatlichen „KI und Forschung“-Serien gibt Dr. Till Plumbaum Einblicke.

Header_KI_Forschung_ontolux_christopher-burns-Kj2SaNHG-hg-unsplash_05_22

KI & Forschung 05-2022

Wie kann künstliche Intelligenz in der Justiz unterstützen? Diese und weitere Themen stellt Dr. Till Plumbaum in der aktuellen „KI und Forschung“- Reihe vor.

Sofia Lisiza im Interview bei Neofonie_betriebliche Weiterbildung

Betriebliche Weiterbildung bei Neofonie – ein Erfahrungsbericht

Lebenslanges Lernen ist der Schlüssel zum beruflichen Erfolg und dem persönlichen Karriereweg. Sofia Lisiza berichtet im Interview von ihren ganz persönlichen Erfahrungen mit dem Thema betriebliche Weiterbildung bei Neofonie.

Unser Newsletter „Neo Report“ vermittelt konkretes Praxiswissen, Trends und Know-how für Ihr digitales Business – quartalsweise und kompakt. Jetzt anmelden.