Java 8 - kurz und bündig

Java 8 - kurz und bündig

Java 8 ist seit März 2014 in der Release-Version verfügbar und bahnt sich derzeit den Weg in die ersten Projekte. Zwar findet sich auf http://java.com aktuell noch die siebte Version der Java Runtime Environment, doch die Entwicklerversion JDK 8 kann bereits seit dem Frühjahr 2014 heruntergeladen werden. Passend dazu wurde auch die IDE Eclipse 4.4 Luna fertiggestellt, die Java 8 vollständig unterstützt.

Im Projektalltag bleibt oft wenig Zeit, sich mit technischen Neuheiten zu beschäftigen; hier soll dieser Blogbeitrag Abhilfe schaffen. Er fasst die wichtigsten neuen Features von Java 8 kurz und bündig zusammen. Dabei werden alle Neuerungen anhand von möglichst einfachen Codebeispielen vorgestellt, um die Einarbeitung zu erleichtern.

[1]Functional Interfaces, Lambda Expressions und Method References

Beginnen wir mit einem Begriff, den viele Leser wahrscheinlich bereits in Zusammenhang mit Java 8 gehört haben: „Lambda Expressions“. Mithilfe von Lambda-Ausdrücken wird es ermöglicht, nicht nur Daten, sondern auch Funktionalität an eine Methode zu übergeben. Grundlage für die Verwendung von „Lambdas“ sind die neuen „FunctionalInterfaces“ in Java. Das sind Schnittstellen, die genau eine abstrakte Methode enthalten. Der Compiler erkennt sie automatisch. Um dem Entwickler die Kontrolle zu erleichtern, wurde die Annotation @FunctionalInterface zu Java 8 hinzugefügt. Sie hat bei der Laufzeit keine Funktion, dient aber ähnlich wie @Override dazu, Fehler bei der Entwicklung schnell zu erkennen. Die Entwicklungsumgebung meldet bei Verwendung dieser Annotation bereits zum Compile-Zeitpunkt, falls die betroffene Schnittstelle in Wirklichkeit gar kein „FunctionalInterface“ ist.

Wird das in Listing 1 definierte „FunctionalInterface“ als Methodenparameter verwendet, kann die Funktionalität, ähnlich wie mit anonymen „nested classes“, direkt zum Zeitpunkt des Aufrufs mithilfe eines Lambda-Ausdrucks oder einer Methodenreferenz (mehr dazu gleich) definiert werden.

Listing 1: FunctionalInterface

Listing 1: FunctionalInterface

 

Das folgende Beispiel zeigt eine Methode, die das in Listing 1 definierte „LambdaInterface“ als Parameter nutzt:

Listing 2: Methode mit FunctionalInterface-Parameter

Listing 2: Methode mit FunctionalInterface-Parameter

 

In Listing 2 wird definiert, dass der String „ein Text“ an die Methode des „FunctionalInterface“ übergeben wird, allerdings nicht, was mit dem String getan werden soll. Vergleichen wir dazu, wie wir die Verhaltensweise der Methode „oneSingleMethod“ in Java 7 und Java 8 definieren können.

Wie in Listing 3 zu sehen, kann mit Java 7 die Methode „oneSingleMethod“ nur mit einer öffentlichen oder einer anonymen inneren Klasse überschrieben werden. Es ist also in jedem Fall notwendig, das komplette Interface zu überschreiben.

Listing 3: Implementierung des Interfaces mit einer anonymen inneren Klasse (Java 7)

Listing 3: Implementierung des Interfaces mit einer anonymen inneren Klasse (Java 7)

 

In Listing 4 kann man sehen, dass in der Java-8-Variante mit Lambda Ausdruck sehr kurz und lesbar formuliert werden kann. Vor dem Pfeil „->“ werden die Übergabeparameter von „oneSingleMethod“ deklariert. Es ist nicht mehr notwendig, den Parameter mit Typ zu versehen, da der Compiler durch das Java-8-Feature „Type Inference“ automatisch erkennt, dass der Parameter „doSomething“ vom Typ „String“ ist. In der folgenden Auflistung werden alle Punkte genannt, die im Vergleich zur Java-7-Version eingespart werden können:

  • new LambdaInterface(), Klassen-/Interface-Name wird nicht mehr benötigt
  • public void oneSingleMethod, Methodenkopf, wird automatisch über das „FunctionalInterface“ erkannt.
  • Der Typ des Parameters (String) wird automatisch per „Type Inference“ ermittelt.
Listing 4: Implementierung von „oneSingleMethod“ mit einem Lambda Ausdruck

Listing 4: Implementierung von „oneSingleMethod“ mit einem Lambda Ausdruck

 

Mithilfe einer Methodenreferenz kann der Ausdruck aus Listing 4 noch kürzer formuliert werden:

Listing 5: Methodenreferenz auf die Instanzmethode „println“

Listing 5: Methodenreferenz auf die Instanzmethode „println“

 

In dieser Variante entfällt zusätzlich noch die Definition der Methodenparameter. Die Notation „Klassenname::Methodenname“ kann sowohl für statische als auch für Member-Methoden genutzt und in jedem Fall durch einen Lambda-Ausdruck ersetzt werden. Natürlich prüft der Compiler auch, ob die übergebene Referenz oder Lambda-Implementierung zum funktionalen Interface passt. Dies gilt sowohl für die übergebenen Werte als auch für den Rückgabetyp.

Damit man sich wenig ausdrucksstarke Methoden wie „lambdaMethod“ aus Listing 2 sparen kann, stellt Java 8 funktionale Interfaces für viele häufig auftretende Fälle zur Verfügung. Die wichtigsten Vertreter dieser Kategorie finden sich im Package „java.util.function“ und werden (nicht vollständig) in Tabelle 1 erläutert.

Tabelle 1: FunctionalInterfaces für häufige Anwendungsfälle

Tabelle 1: FunctionalInterfaces für häufige Anwendungsfälle

 

Die in Tabelle 1 gezeigten Interfaces werden in Java 8 bereits an vielen Stellen eingesetzt. Ein Beispiel hierfür ist das funktionale Interface Comparator“, das in der „compare“-Methode das Interface „Function“ einsetzt.

Listing 6 zeigt, wie eine Liste von Strings mithilfe des Comparators „String::compareToIgnoreCase“ sortiert wird. Alle Werte werden anschließend mithilfe von „forEach“ und der Methodenreferenz „System.out::println“ ausgegeben.

Listing 6: Sortieren einer Liste vom Typ String mit dem funktionalen Interface "Comparator"

Listing 6: Sortieren einer Liste vom Typ String mit dem funktionalen Interface "Comparator"

 

[2]Streams

Eine der häufigsten Aufgaben im Projektalltag ist die Verarbeitung von Werten in einer Collection. Selbst für einfachste Operationen ist es in Java 7 nötig, ein oder mehrere Schleifen zu definieren, die die Verarbeitung der Werte vornehmen. Eine parallele Abarbeitung ist dabei nur schwer zu realisieren. Hier kommt die neue Abstraktion „Stream“ ins Spiel. Ein Stream ist eine Sequenz von Elementen, die aus einer Datenquelle herangezogen werden. Das Interface „Stream“ unterstützt alle typischen Operationen, die in einer Listenverarbeitung genutzt werden. Dazu zählen u.a. Filterung, Aggregation, Projektion und Sortierung.

Folgendes Beispiel erläutert die Nutzung von Streams in Java 8:

Listing 7: Verwendung eines Streams zur Listenverarbeitung

Listing 7: Verwendung eines Streams zur Listenverarbeitung

 

Zuerst wird eine Liste „team“ mit drei Mitgliedern erzeugt, die jeweils ein Alter, ein Gewicht und einen Namen haben. Möchte man jetzt beispielsweise das durchschnittliche Gewicht aller Teammitglieder mit einem Alter über 30 ermitteln, bräuchte man in Java 7 mindestens eine Schleife, eine „if“-Abfrage, eine Wertzuweisung und eine Methode zur Bildung des Durchschnitts. Wie man in Listing 7 sehen kann, werden alle diese Aufgaben von einem Stream übernommen. Alle nicht-terminierenden Methoden, wie z.B. „filter“, können mit der fluent-API der Streams genutzt werden, um die Daten nahtlos in einem Folgeschritt weiterverarbeiten zu können. Im Beispiel wird gezeigt, wie Lambda-Ausdrücke und Methoden-Referenzen schwer lesbare anonyme Klassen ersetzen können.

Neben der erhöhten Lesbarkeit und der Einsparung von „Boilerplate“-Code bringen Streams noch weitere Vorteile mit sich. Spielt die Verarbeitungsreihenfolge von Streams keine Rolle, kann man mit nur einer Anpassung die parallele Verarbeitung in mehreren Threads starten.

Listing 8 verwendet „parallelStream()“ anstatt „stream()“, um die Abarbeitung von „forEach“ auf mehreren CPU-Kernen bzw. in mehreren Threads durchzuführen.

Listing 8: Erzeugung eines parallelen Streams

Listing 8: Erzeugung eines parallelen Streams

 

Ein weiterer Vorteil ist die einfache Erzeugung von Streams. Neben der direkten Instanziierung aus Listen oder Arrays gibt es noch zahlreiche weitere Methoden. In Listing 9 wird dies anhand eines IntStreams demonstriert. Dieser wird im Beispiel mit einem Builder sowie mit den Methoden generate(), iterate(), range() und rangeClosed() erzeugt.

Listing 9: Erzeugen von IntStreams

Listing 9: Erzeugen von IntStreams

 

[3]DateTime-API

Die neue DateTime-API orientiert sich stark an dem schon länger verfügbaren Framework „Joda-Time“ und ist seit Java 8 im Package „java.time“ zu finden. Es löst die bereits seit Längerem kritisierte Zeitverwaltung von Java ab und verfolgt folgende Kernziele:

  • Nicht änderbare Klassen: Jeder Wert, der einmal angelegt wurde, kann nicht mehr geändert werden. Dies verhindert fehleranfällige Datumsoperationen.
  • Domain-driven design: Das Domänenmodell ist separiert und repräsentiert konkrete Anwendungsfälle, die bei der Arbeit mit Datum und Zeit auftreten. Es gibt. z.B. eine Klasse für Zeit (ohne Datum) oder für einen bestimmten Tag des Monats.
  • Aufteilung verschiedener Kalender- und Zeitsysteme in unterschiedliche Chronologien.

Wichtige Klassen in der umfassenden „java.time“-Bibliothek sind „LocalDate“, „LocalTime“ und „LocalDateTime“. Sie repräsentieren ein Datum ohne Zeit, eine Zeit ohne Datum und eine Kombination aus beidem. Listing 10 zeigt die Instanziierung der genannten Klassen.

Listing 10: Instanziierung von LocalDate, LocalTime und LocalDateTime

Listing 10: Instanziierung von LocalDate, LocalTime und LocalDateTime

 

Anhand der weiterführenden Beispiele in Listing 11 sollen einige Teilaspekte zur Java 8 DateTime-API näher beleuchtet werden; für eine vollständige Referenz sollte der Leser auf die Java-8-Dokumentation zurückgreifen.

Listing 11: Beispiele zur DateTime-API aus Java SE 8

Listing 11: Beispiele zur DateTime-API aus Java SE 8

 

Im Beispiel-Listing wird geprüft, ob das Datum „date“ ein Freitag ist. „plusYears“ addiert anschließend fünf Jahre zu „date“. Zeile 6 instanziiert ein neues „LocalDateTime“-Objekt mit dem Datum von „date“ und der Zeit „15:30“ Uhr. Zeile 7 erzeugt zwei Objekte. „toLocalDate()“ instanziiert ein Datum, „atStartOfDay()“ ein „LocalDateTime“-Objekt mit der Uhrzeit „00:00“ Uhr. Zuletzt wird der 24. März als „MonthDay“ instanziiert. Mit „atYear“ kann der „MonthDay“ mit ein Jahr verknüpft werden, was zu einem neuen „LocalDate“-Objekt mit dem Datumswert „24.03.2015“ führt.

Durch die Nutzung von Immutable-Objects und der fluent-API können mächtige Ausdrücke in lesbarer Form geschrieben werden, wie man an den Beispielen aus Listing 11 erkennen kann.

[4]Sonstiges

Im Folgenden werden weitere wichtige Neuerungen kurz angesprochen. Eine vollständige Auflistung der neuen Features findet sich unter [Q1].

  • „Default“-Methoden in Interfaces: Interfaces können mit dem Schlüsselwort „default“ Standardimplementierungen für Methoden liefern. Im Unterschied zu abstrakten Klassen können Interfaces aber keinen Zustand haben bzw. keine Instanzvariablen deklarieren. Ein „@FunctionalInterface“ kann neben der einen abstrakten Methode viele „default“-Methoden enthalten und verstößt damit nicht gegen den Vertrag.
  • Statische Methoden in Interfaces: Interfaces können statische Methoden mit einer Implementierung zur Verfügung stellen. Auch hier gilt: Ein „@FunctionalInterface“ kann mehrere statische Methoden enthalten.
  • Mehrfachvererbung „light“: Eine Klasse kann zwei Interfaces implementieren, die die gleiche „default“-Methode zur Verfügung stellen. Die Klasse muss die Methode überschreiben und kann dann mit „InterfaceName.super.methodName()“ die zu verwendende „super“-Methode aufrufen.
  • „OptionalValues“: „Optional.of(T)“ erzeugt einen optionalen Wert. Mit dieser Vorgehensweise können Standardwerte vergeben und „NullPointerExceptions“ verhindert werden. Die Methode „.orElse(T)“ liefert einen Alternativwert, falls der Wert des Optionals nicht gesetzt ist.
  • Type Annotations: Annotationen können nicht mehr nur bei der Deklaration sondern auch bei der Verwendung von Variablen genutzt werden.
  • JavaFX 8: Im Client-Framework „JavaFX“ wurden zahlreiche Erweiterungen vorgenommen. Dazu zählen u.a. das neue Haupt-Thema „Modena“ sowie die „SwingNode“, mit der Swing-Content nahtlos in „JavaFX“-Clients eingebettet werden kann.

Mit dem größten Update seit Java 5 liefert Oracle ein umfangreiches Featureset, das in der Entwicklergemeinde großen Anklang findet. Trotzdem gibt es auch viel Stoff für Diskussionen, wie beispielsweise die Einführung der „Default-Interfaces“, die notwendig waren, um Java 8 abwärtskompatibel anpassen zu können. Ein gutes Beispiel hierfür ist die Collection-Methode „default stream()“, die man ohne Verletzung der Kompatibilität nicht einführen hätte können. Insgesamt dürften sich aber die meisten Entwickler darüber freuen, wenn sie in den Genuss der neuen Funktionen kommen, die den Aufschluss zu modernen Sprachen wie bspw. Scala bringen sollen.

Quellen:

  1. Oracle: Whats new in Java 8: http://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html
  2. Oracle: Java 8 Tutorials: http://docs.oracle.com/javase/tutorial
  3. Brian Goetz: Lambdas and Streams in Java 8 Libraries: http://www.drdobbs.com/jvm/lambdas-and-streams-in-java-8-libraries/240166818
  4. Level up lunch: IntStream example: http://www.leveluplunch.com/java/examples/java-util-stream-intstream-example/
  5. Angelika Langer, Klaus Kreft: Was sind Streams: Java Magazin 6/2014