Was Entwickler mit Java 8 erwartet

Seite 3: Aufzählungen und Datumsprobleme

Inhaltsverzeichnis

Lambda-Ausdrücke eröffnen generell ganz neue Wege im Umgang mit Aufzählungen. Dabei geht es weniger darum, Code eleganter zu formulieren, vielmehr sollen in Java 8 einzelne Methoden zur Verarbeitung von Aufzählungen besser parallelisiert und erst bei Bedarf vollständig ausgewertet werden (durch so genannte Lazy Operations). Möglich wird das durch Erweiterungen in den Collection-Klassen der Java-Standardbibliothek.

forEach ist für die Schnittstellen Iterable und Iterator als Alternative zu herkömmlichen for-each- Schleifen gedacht. Die sogenannte Internal Iteration die die Methode verwendet, bietet im Vergleich zur traditionellen External Iteration zwei wesentliche Vorteile: Zum einen können Lambda-Ausdrücke komfortabel auf alle Aufzählungselemente angewendet werden, zum anderen lässt sich die forEach-Methode so implementieren, dass die Verarbeitung der Elemente parallelisiert erfolgt.

Neben forEach gibt es andere neue Methoden, die verschiedene Formen von Lambda-Ausdrücken als Parameter erwarten. Zu deren Deklaration sieht das neue Paket java.util.functions diverse Schnittstellen vor (siehe Tabelle 'Lambda-Schnittstellen'). Sie kommen zum Beispiel in den Parametern verschiedener Methoden der Collection-Schnittstellen zum Einsatz:

  • Collection.forEach(Consumer)
  • Collection.removeIf(Predicate)
  • List.replaceAll(Function)
  • Stream.anyMatch(Predicate)
Predicate<T> überprüft, ob ein Objekt vom Typ T ein Kriterium erfüllt.
Supplier<T> liefert Objekte vom Typ T (z.B. für get-Methoden).
Consumer<T> verarbeitet ein Objekt vom Typ T, gibt kein Ergebnis zurück.
Consumer<T, U> verarbeitet zwei Objekte vom Typ T und U, gibt kein Ergebnis zurück.
Function<T, R> verarbeitet ein Objekt vom Typ T und liefert als Ergebnis ein Objekt vom Typ R zurück.
BiFunction<T, U, R> verarbeitet zwei Objekte vom Typ T und U und liefert als Ergebnis ein Objekt vom Typ R zurück.
UnaryOperator<T> entspricht Function<T, T>, d.h., die zu verarbeitenden Daten und die Ergebnisse weisen denselben Typ auf.
BinaryOperator<T> entspricht BiFunction<T, T, T>, d.h., die zu verarbeitenden Daten und die Ergebnisse weisen denselben Typ auf.

Das folgende Beispiel zeigt den Einsatz der neuen Methode removeIf. Sie entfernt alle Elemente einer Aufzählung, die ein bestimmtes Kriterium erfüllen. Hierfür erwartet removeIf ein Predicate-Objekt, das sich am einfachsten als Lambda-Ausdruck formulieren lässt.

import java.util.*;

List<Integer> lst = new ArrayList<>();
for(int i=1; i<10; i++)
lst.add(i);
// alle ungeraden Zahlen entfernen
lst.removeIf(i -> i % 2 == 1);

Die spannendste Neuerung ist aber im Paket java.util.stream versteckt. Die dort enthaltenen Schnittstelle Stream<T> erweitert die grundlegenden Aufzählungsklassen um ein vollkommen neues Konzept zur funktionalen Programmierung. Die Elemente werden dabei nicht gespeichert, sondern von einer Methode an die nächste weitergereicht (von der Idee her ähnlich wie Pipes unter Unix/Linux) und erst bei Bedarf tatsächlich verarbeitet (Lazy Operation). Wichtige Methoden sind filter, map, reduce, fold, limit und skip.

import java.util.*;

String lorem = "Lorem ipsum dolor sit amet, ...";
List<String> lst = Arrays.asList(lorem.split(" "));

// alle Wörter mit mehr als sechs Zeichen ausgeben
// Ausgabe: consetetur sadipscing invidunt aliquyam
// voluptua. accusam dolores gubergren, ...
lst.stream()
.filter(s -> s.length()>6)
.forEach(s -> System.out.println(s));

// alle Wörter mit mehr als sechs Zeichen zählen (ohne Doppelgänger)
long n = lst.parallelStream()
.filter(s -> s.length()>6)
.distinct()
.count();
System.out.println(n);

// durchschnittliche Wortlänge
OptionalDouble avg = lst.parallelStream()
.mapToInt(s -> s.length())
.average();
System.out.println(avg.getAsDouble());

Der obige Code zeigt, wie eine Liste von Zeichenketten in einen Stream umgewandelt und dann auf unterschiedliche Weise verarbeitet wird. Bemerkenswert ist die Methode parallelStream: Sie liefert einen zur Parallelverarbeitung optimierten Stream.

Der verunglückten Date-Klasse in der Java-Standardbibliothek wurde bereits in Java-Version 1.1 die Calendar-Klasse zur Seite gestellt, doch auch sie ist eher eine Notlösung. Als gegenwärtig beste Möglichkeit, diffizile Datumsfragen zu behandeln, gilt die unter der Apache-2-Lizenz verfügbare Bibliothek Joda Time.

In Java 8 wagt Oracle mit dem sogenannten ThreeTen-Projekt nun einen weiteren Anlauf, um Daten, Zeiten und ihre unzähligen Sonderfälle innerhalb der Standardbibliothek in den Griff zu bekommen – und versucht gleichzeitig, Designschwächen der Joda-Time-Bibliothek zu beheben. Damit ist schon klar: Java-Entwickler müssen sich auf eine weitere Bibliothek einlassen, die inkompatibel zu allen bisherigen Varianten und mit über 60 Klassen und Schnittstellen in mehreren java.time-Paketen alles andere als übersichtlich ist.

Eine wichtige neue Idee des Projekts ist die Unterscheidung zwischen einer Machine Time Line und einer Human Time Line:

  • Die Machine Time Line beschreibt Mechanismen, um einen Zeitpunkt (Klasse Instant) beziehungsweise eine Zeitspanne (Duration) durch den Computer auf Nanosekunden genau abzubilden. Die Instant-Klasse entspricht am ehesten der Date-Klasse.
  • In der Human Time Line geht es darum, Datum und Zeit so darzustellen, dass sie sich von Menschen verarbeiten lassen – also zum Beispiel mit einem Monatsnamen. ThreeTen unterscheidet dabei zwischen lokalen Daten/Zeiten ohne Bezug zu einer Zeitzone (Klassen LocalDate, LocalTime, LocalDateTime), Daten/Zeiten mit einem fixen Offset (zum Beispiel +01:00; OffsetDate, OffsetTime und OffsetDateTime) sowie Zeitangaben mit voller Zeitzonenunterstützung samt Sommer-/Winterzeitumstellung (ZonedDateTime).
// Anwendung der Zeit- und Datum-Klassen
import java.time.*;
import java.util.*;
import java.time.format.*;

LocalDateTime dt = LocalDateTime.now();
System.out.format("Jahr: %d\n", dt.getYear());
System.out.format("Monat: %d\n", dt.getMonthValue()); // 1-12
System.out.format("Formatiert: %s\n", // z.B. 2013-06-06T13:29:50.252
dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

// der Monatsname
Month m = dt.getMonth();
String s = m.getDisplayName(TextStyle.FULL, new Locale("de"));
System.out.format("Monat: %s\n", s); // z.B. Juni

// das Datum in einem Monat
LocalDateTime dt2 = dt.plusMonths(1);
System.out.format("Formatiert: %s\n", // z.B. 2013-07-06
dt2.format(DateTimeFormatter.ISO_LOCAL_DATE));

Standardmäßig verwenden die ThreeTen-Klassen den in Europa üblichen ISO-Kalender. Zum Arbeiten in anderen Kalendersystemen stellen die Pakete java.time.chrono und java.time.calendars einige Zusatzklassen zur Verfügung (z.B. MinguoDate und MinguoChronology für den in Taiwan geltenden Minguo-Kalender).