Advertisement
  1. Code
  2. Android SDK

Java 8 für Android-Entwicklung: Stream-API und Datums- und Uhrzeitbibliotheken

by
Read Time:14 minsLanguages:
This post is part of a series called Java 8 for Android Development.
Java 8 for Android Development: Default and Static Methods

German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)

In dieser dreiteiligen Serie haben wir alle wichtigen Java 8-Funktionen untersucht, die Sie heute in Ihren Android-Projekten verwenden können.

In Cleaner Code With Lambda Expressions haben wir uns darauf konzentriert, mithilfe von Lambda-Ausdrücken Boilerplate aus Ihren Projekten zu entfernen, und dann in Standard- und statischen Methoden gesehen, wie diese Lambda-Ausdrücke durch Kombination mit Methodenreferenzen prägnanter gestaltet werden können. Wir haben auch das Wiederholen von Anmerkungen und das Deklarieren nicht abstrakter Methoden in Ihren Schnittstellen mit Standard- und statischen Schnittstellenmethoden behandelt.

In diesem letzten Beitrag werden wir uns Typannotationen, funktionale Schnittstellen und einen funktionaleren Ansatz für die Datenverarbeitung mit der neuen Stream-API von Java 8 ansehen.

Ich zeige Ihnen auch, wie Sie mit den Bibliotheken Joda-Time und ThreeTenABP auf einige zusätzliche Java 8-Funktionen zugreifen können, die derzeit nicht von der Android-Plattform unterstützt werden.

Anmerkungen eingeben

Anmerkungen helfen Ihnen beim Schreiben von Code, der robuster und weniger fehleranfällig ist, indem sie Codeinspektionstools wie Lint über die Fehler informieren, auf die sie achten sollten. Diese Inspektionstools warnen Sie dann, wenn ein Codeabschnitt nicht den in diesen Anmerkungen festgelegten Regeln entspricht.

Annotationen sind keine neue Funktion (sie stammen tatsächlich aus Java 5.0), aber in früheren Java-Versionen war es nur möglich, Annotationen auf Deklarationen anzuwenden.

Mit der Veröffentlichung von Java 8 können Sie Annotationen jetzt überall verwenden, wo Sie einen Typ verwendet haben, einschließlich Methodenempfänger; Ausdrücke zum Erstellen von Klasseninstanzen; die Implementierung von Schnittstellen; Generika und Arrays; die Spezifikation von throws und implements-Klauseln; und Typguss.

Frustrierenderweise bietet Java 8 zwar die Möglichkeit, Annotationen an mehr Stellen als je zuvor zu verwenden, bietet jedoch keine typspezifischen Annotationen.

Die Annotations Support Library von Android bietet Zugriff auf einige zusätzliche Anmerkungen wie @Nullable, @NonNull und Anmerkungen zum Überprüfen von Ressourcentypen wie @DrawableRes, @DimenRes, @ColorRes und @StringRes. Sie können jedoch auch ein statisches Analysetool eines Drittanbieters verwenden, z. B. das Checker Framework, das zusammen mit der JSR 308-Spezifikation (der Annotations on Java Types-Spezifikation) entwickelt wurde. Dieses Framework bietet einen eigenen Satz von Annotationen, die auf Typen angewendet werden können, sowie eine Reihe von "Checkern" (Annotationsprozessoren), die sich in den Kompilierungsprozess einklinken und spezifische "Checks" für jede im Checker-Framework enthaltene Typannotation durchführen.

Da Typanmerkungen keinen Einfluss auf den Laufzeitbetrieb haben, können Sie die Typanmerkungen von Java 8 in Ihren Projekten verwenden und gleichzeitig mit früheren Java-Versionen abwärtskompatibel bleiben.

Stream-API

Die Stream-API bietet einen alternativen "Pipes-and-Filter" -Ansatz für die Sammlungsverarbeitung.

Vor Java 8 haben Sie Sammlungen manuell bearbeitet, indem Sie in der Regel die Sammlung durchlaufen und nacheinander die einzelnen Elemente bearbeitet haben. Dieses explizite Looping erforderte viel Boilerplate und es ist schwierig, die for-Loop-Struktur zu erfassen, bis Sie den Körper der Schleife erreichen.

Die Stream-API bietet Ihnen die Möglichkeit, Daten effizienter zu verarbeiten, indem Sie diese Daten in einem einzigen Durchlauf ausführen – unabhängig von der Datenmenge, die Sie verarbeiten oder ob Sie mehrere Berechnungen durchführen.

In Java 8 verfügt jede Klasse, die java.util.Collection implementiert, über eine stream-Methode, die ihre Instanzen in Stream-Objekte konvertieren kann. Wenn Sie beispielsweise ein Array haben:

Dann können Sie es mit folgendem in einen Stream umwandeln:

Die Stream-API verarbeitet Daten, indem sie Werte aus einer Quelle durch eine Reihe von Rechenschritten überträgt, die als Stream-Pipeline bekannt sind. Eine Streampipeline besteht aus folgenden Elementen:

  • Eine Quelle, z. B. eine Collection-, Array- oder Generatorfunktion.
  • Null oder mehr "faule" Zwischenoperationen. Zwischenoperationen beginnen erst mit der Verarbeitung von Elementen, bis Sie eine Terminaloperation aufrufen – deshalb gelten sie als faul. Wenn Sie beispielsweise Stream.filter() für eine Datenquelle aufrufen, wird lediglich die Streampipeline eingerichtet. Es erfolgt keine Filterung, bis Sie die Terminaloperation aufrufen. Dadurch ist es möglich, mehrere Operationen aneinanderzureihen und dann alle diese Berechnungen in einem einzigen Datendurchlauf durchzuführen. Zwischenvorgänge erzeugen einen neuen Stream (z. B. erzeugt filter einen Stream, der die gefilterten Elemente enthält), ohne die Datenquelle zu ändern, sodass Sie die Originaldaten an anderer Stelle in Ihrem Projekt verwenden oder mehrere Streams aus derselben Quelle erstellen können.
  • Ein Terminalvorgang wie Stream.forEach. Wenn Sie die Terminaloperation aufrufen, werden alle Ihre Zwischenoperationen ausgeführt und erzeugen einen neuen Stream. Ein Stream kann keine Elemente speichern. Sobald Sie eine Terminaloperation aufrufen, wird dieser Stream als "verbraucht" betrachtet und kann nicht mehr verwendet werden. Wenn Sie die Elemente eines Streams erneut aufrufen möchten, müssen Sie aus der ursprünglichen Datenquelle einen neuen Stream generieren.

Einen Stream erstellen

Es gibt verschiedene Möglichkeiten, einen Stream aus einer Datenquelle abzurufen, darunter:

  • Stream.of() Erstellt einen Stream aus einzelnen Werten:

  • IntStream.range() Erstellt einen Stream aus einem Zahlenbereich:

  • Stream.iterate() Erstellt einen Stream durch wiederholtes Anwenden eines Operators auf jedes Element. Hier erstellen wir beispielsweise einen Stream, bei dem jedes Element um eins an Wert gewinnt:

Transformieren eines Streams mit Operationen

Es gibt eine Vielzahl von Operationen, die Sie verwenden können, um Berechnungen im funktionalen Stil für Ihre Streams durchzuführen. In diesem Abschnitt werde ich nur einige der am häufigsten verwendeten Stream-Operationen behandeln.

Karte

Die Operation map() verwendet einen Lambda-Ausdruck als einziges Argument und verwendet diesen Ausdruck, um den Wert oder den Typ jedes Elements im Stream zu transformieren. Folgendes gibt uns beispielsweise einen neuen Stream, in dem jeder String in Großbuchstaben umgewandelt wurde:

Grenze

Diese Operation legt eine Begrenzung der Größe eines Streams fest. Wenn Sie beispielsweise einen neuen Stream mit maximal fünf Werten erstellen möchten, verwenden Sie Folgendes:

Filter

Mit der Operation filter(Predicate<T>) können Sie Filterkriterien mithilfe eines Lambda-Ausdrucks definieren. Dieser Lambda-Ausdruck muss einen booleschen Wert zurückgeben, der bestimmt, ob jedes Element in den resultierenden Stream aufgenommen werden soll. Wenn Sie beispielsweise über ein Array von Zeichenfolgen verfügen und alle Zeichenfolgen herausfiltern möchten, die weniger als drei Zeichen enthalten, verwenden Sie Folgendes:

Sortiert

Diese Operation sortiert die Elemente eines Streams. Das Folgende gibt beispielsweise eine Reihe von Zahlen zurück, die in aufsteigender Reihenfolge angeordnet sind:

Parallelverarbeitung

Alle Stream-Vorgänge können seriell oder parallel ausgeführt werden, obwohl Streams sequentiell sind, sofern Sie nicht ausdrücklich etwas anderes angeben. Im Folgenden wird beispielsweise jedes Element nacheinander verarbeitet:

Um einen Stream parallel auszuführen, müssen Sie diesen Stream mithilfe der parallel()-Methode explizit als parallel markieren:

Unter der Haube verwenden parallele Streams das Fork / Join-Framework, sodass die Anzahl der verfügbaren Threads immer der Anzahl der verfügbaren Kerne in der CPU entspricht.

Der Nachteil paralleler Streams besteht darin, dass bei jeder Ausführung des Codes unterschiedliche Kerne beteiligt sein können, sodass Sie normalerweise bei jeder Ausführung eine andere Ausgabe erhalten. Daher sollten Sie parallele Streams nur verwenden, wenn die Verarbeitungsreihenfolge unwichtig ist, und parallele Streams vermeiden, wenn Sie reihenfolgebasierte Operationen wie findFirst() ausführen.

Terminalbetrieb

Sie sammeln die Ergebnisse eines Streams mithilfe einer Terminaloperation, die immer das letzte Element in einer Kette von Streammethoden ist und immer etwas anderes als einen Stream zurückgibt.

Es gibt einige verschiedene Arten von Terminaloperationen, die verschiedene Datentypen zurückgeben, aber in diesem Abschnitt werden wir uns zwei der am häufigsten verwendeten Terminaloperationen ansehen.

Sammeln

Der Collect-Vorgang sammelt alle verarbeiteten Elemente in einem Container, z. B. einer List oder einer Set. Java 8 bietet eine Collectors-Hilfsklasse, sodass Sie sich keine Gedanken über die Implementierung der Collectors-Schnittstelle machen müssen, plus Factories für viele gängige Collectors, einschließlich toList(), toSet() und toCollection().

Der folgende Code erzeugt eine List, die nur rote Formen enthält:

Alternativ können Sie diese gefilterten Elemente in einem Set zusammenfassen:

forEach

Die forEach()-Operation führt eine Aktion für jedes Element des Streams aus, wodurch sie zum Äquivalent einer for-each-Anweisung der Stream-API wird.

Wenn Sie eine items liste haben, können Sie forEach verwenden, um alle Artikel zu drucken, die in dieser List enthalten sind:

Im obigen Beispiel verwenden wir einen Lambda-Ausdruck, sodass es möglich ist, die gleiche Arbeit mit weniger Code mithilfe einer Methodenreferenz auszuführen:

Funktionale Schnittstellen

Eine funktionale Schnittstelle ist eine Schnittstelle, die genau eine abstrakte Methode enthält, die als funktionale Methode bezeichnet wird.

Das Konzept von Schnittstellen mit einer Methode ist nicht neu - Runnable, Comparator, Callable und OnClickListener sind Beispiele für diese Art von Schnittstelle, obwohl sie in früheren Versionen von Java als Single Abstract Method Interfaces (SAM-Schnittstellen) bezeichnet wurden.

Dies ist mehr als eine einfache Namensänderung, da es einige bemerkenswerte Unterschiede bei der Arbeit mit funktionalen (oder SAM-) Schnittstellen in Java 8 im Vergleich zu früheren Versionen gibt.

Vor Java 8 haben Sie normalerweise eine funktionale Schnittstelle mithilfe einer umfangreichen anonymen Klassenimplementierung instanziiert. Hier erstellen wir beispielsweise eine Instanz von Runnable mit einer anonymen Klasse:

Wie wir bereits in Teil 1 gesehen haben, können Sie diese Schnittstelle bei Verwendung einer Schnittstelle mit nur einer Methode mithilfe eines Lambda-Ausdrucks anstelle einer anonymen Klasse instanziieren. Jetzt können wir diese Regel aktualisieren: Sie können funktionale Schnittstellen mithilfe eines Lambda-Ausdrucks instanziieren. Beispielsweise:

Java 8 führt auch eine @FunctionalInterface-Annotation ein, mit der Sie eine Schnittstelle als funktionale Schnittstelle markieren können:

Um die Abwärtskompatibilität mit früheren Java-Versionen zu gewährleisten, ist die Annotation @FunctionalInterface optional; Es wird jedoch empfohlen, sicherzustellen, dass Sie Ihre funktionalen Schnittstellen korrekt implementieren.

Wenn Sie versuchen, zwei oder mehr Methoden in einer als @FunctionalInterface gekennzeichneten Schnittstelle zu implementieren, beschwert sich der Compiler, dass er mehrere nicht überschreibende abstrakte Methoden entdeckt hat. Folgendes wird beispielsweise nicht kompiliert:

Und wenn Sie versuchen, eine @FunctionInterface-Schnittstelle zu kompilieren, die null Methoden enthält, werden Sie auf den Fehler Keine Zielmethode gefunden stoßen.

Funktionale Schnittstellen müssen genau eine abstrakte Methode enthalten, aber da Standard- und statische Methoden keinen Körper haben, werden sie als nicht abstrakt betrachtet. Das bedeutet, dass Sie mehrere Standard- und statische Methoden in eine Schnittstelle einschließen können, sie als @FunctionalInterface markieren und sie trotzdem kompiliert.

Java 8 hat auch ein java.util.function-Paket hinzugefügt, das viele funktionale Schnittstellen enthält. Es lohnt sich, sich die Zeit zu nehmen, sich mit all diesen neuen funktionalen Schnittstellen vertraut zu machen, damit Sie genau wissen, was sofort einsatzbereit ist.

JSR-310: Javas neue Datums- und Uhrzeit-API

Das Arbeiten mit Datum und Uhrzeit in Java war noch nie besonders einfach. Viele APIs haben wichtige Funktionen wie Zeitzoneninformationen nicht berücksichtigt.

Java 8 hat eine neue Datums- und Uhrzeit-API (JSR-310) eingeführt, die darauf abzielt, diese Probleme zu lösen, aber leider wird diese API zum Zeitpunkt des Schreibens nicht auf der Android-Plattform unterstützt. Sie können jedoch einige der neuen Funktionen für Datum und Uhrzeit in Ihren Android-Projekten heute mithilfe einer Bibliothek eines Drittanbieters verwenden.

In diesem letzten Abschnitt werde ich Ihnen zeigen, wie Sie zwei beliebte Bibliotheken von Drittanbietern einrichten und verwenden, mit denen Sie die Datums- und Uhrzeit-API von Java 8 unter Android verwenden können.

ThreeTen Android Backport

ThreeTen Android Backport (auch bekannt als ThreeTenABP) ist eine Adaption des beliebten ThreeTen Backport-Projekts, das eine Implementierung von JSR-310 für Java 6.0 und Java 7.0 bereitstellt. ThreeTenABP bietet Zugriff auf alle Date- und Time-API-Klassen (wenn auch mit einem anderen Paketnamen), ohne Ihren Android-Projekten eine große Anzahl von Methoden hinzuzufügen.

Um diese Bibliothek zu verwenden, öffnen Sie Ihre Datei build.gradle auf Modulebene und fügen Sie ThreeTenABP als Projektabhängigkeit hinzu:

Anschließend müssen Sie die ThreeTenABP-Importanweisung hinzufügen:

Und initialisieren Sie die Zeitzoneninformationen in Ihrer Application.onCreate()-Methode:

ThreeTenABP enthält zwei Klassen, die zwei "Arten" von Zeit- und Datumsinformationen anzeigen:

  • LocalDateTime, das eine Uhrzeit und ein Datum im Format 2017-10-16T13:17:57.138 speichert
  • ZonedDateTime, die Zeitzonen berücksichtigt und Datums- und Uhrzeitinformationen im folgenden Format speichert: 2011-12-03T10:15:30+01:00[Europe/Paris]

Um Ihnen eine Vorstellung davon zu geben, wie Sie diese Bibliothek zum Abrufen von Datums- und Uhrzeitinformationen verwenden würden, verwenden wir die LocalDateTime-Klasse, um das aktuelle Datum und die aktuelle Uhrzeit anzuzeigen:

Display the date and time using the ThreeTen Android Backport libraryDisplay the date and time using the ThreeTen Android Backport libraryDisplay the date and time using the ThreeTen Android Backport library

Dies ist nicht die benutzerfreundlichste Art, Datum und Uhrzeit anzuzeigen! Um diese Rohdaten in etwas besser lesbares zu parsen, können Sie die DateTimeFormatter-Klasse verwenden und sie auf einen der folgenden Werte festlegen:

  • BASIC_ISO_DATE. Formatiert das Datum als 2017-1016+01.00
  • ISO_LOCAL_DATE. Formatiert das Datum als 2017-10-16
  • ISO_LOCAL_TIME. Formatiert die Zeit als 14:58:43.242
  • ISO_LOCAL_DATE_TIME. Formatiert Datum und Uhrzeit als 2017-10-16T14:58:09.616
  • ISO_OFFSET_DATE. Formatiert das Datum als 2017-10-16+01.00
  • ISO_OFFSET_TIME. Formatiert die Zeit als 14:58:56.218+01:00
  • ISO_OFFSET_DATE_TIME. Formatiert Datum und Uhrzeit als 2017-10-16T14:5836.758+01:00
  • ISO_ZONED_DATE_TIME. Formatiert Datum und Uhrzeit als 2017-10-16T14:58:51.324+01:00(Europa/London)
  • ISO_INSTANT. Formatiert Datum und Uhrzeit als 2017-10-16T13:52:45.246Z
  • ISO_DATE. Formatiert das Datum als 16.10.2017+01:00
  • ISO_TIME. Formatiert die Zeit als 14:58:40.945+01:00
  • ISO_DATE_TIME. Formatiert Datum und Uhrzeit als 2017-10-16T14:55:32.263+01:00(Europa/London)
  • ISO_ORDINAL_DATE. Formatiert das Datum als 2017-289+01:00
  • ISO_WEEK_DATE. Formatiert das Datum als 2017-W42-1+01:00
  • RFC_1123_DATE_TIME. Formatiert Datum und Uhrzeit als Mon, 16 OCT 2017, 14:58:43+01:00

Hier aktualisieren wir unsere App, um Datum und Uhrzeit mit der DateTimeFormatter.ISO_DATE-Formatierung anzuzeigen:

Um diese Informationen in einem anderen Format anzuzeigen, ersetzen Sie einfach DateTimeFormatter.ISO_DATE durch einen anderen Wert. Beispielsweise:

Joda-Zeit

Vor Java 8 galt die Joda-Time-Bibliothek als die Standardbibliothek für die Handhabung von Datum und Uhrzeit in Java, bis zu dem Punkt, an dem die neue Date and Time API von Java 8 tatsächlich "stark auf die Erfahrungen aus dem Joda-Time-Projekt" zurückgreift.

Während die Joda-Time-Website empfiehlt, dass Benutzer so schnell wie möglich auf Java 8 Date and Time migrieren, da Android diese API derzeit nicht unterstützt, ist Joda-Time immer noch eine praktikable Option für die Android-Entwicklung. Beachten Sie jedoch, dass Joda-Time über eine große API verfügt und Zeitzoneninformationen mithilfe einer JAR-Ressource lädt. Beides kann die Leistung Ihrer App beeinträchtigen.

Um mit der Joda-Time-Bibliothek zu arbeiten, öffnen Sie die Datei build.gradle auf Modulebene und fügen Sie Folgendes hinzu:

Die Joda-Time-Bibliothek hat sechs große Datums- und Zeitklassen:

  • Instant: Stellt einen Punkt in der Timeline dar; Sie können beispielsweise das aktuelle Datum und die aktuelle Uhrzeit abrufen, indem Sie Instant.now() aufrufen.
  • DateTime: Ein universeller Ersatz für die Calendar-Klasse von JDK.
  • LocalDate: Ein Datum ohne Uhrzeit oder Bezug auf eine Zeitzone.
  • LocalTime: Eine Zeit ohne Datum oder Verweis auf eine Zeitzone, z. B. 14:00:00.
  • LocalDateTime: Ein lokales Datum und eine lokale Uhrzeit, noch ohne Zeitzoneninformationen.
  • ZonedDateTime: Ein Datum und eine Uhrzeit mit einer Zeitzone.

Schauen wir uns an, wie Sie Datum und Uhrzeit mit Joda-Time drucken. Im folgenden Beispiel verwende ich Code aus unserem ThreeTenABP-Beispiel wieder. Um die Sache interessanter zu machen, verwende ich auch withZone, um das Datum und die Uhrzeit in einen ZonedDateTime-Wert umzuwandeln.

Display the date and time using the Joda-Time libraryDisplay the date and time using the Joda-Time libraryDisplay the date and time using the Joda-Time library

Eine vollständige Liste der unterstützten Zeitzonen finden Sie in den offiziellen Joda-Time-Dokumenten.

Abschluss

In diesem Beitrag haben wir uns angeschaut, wie man mithilfe von Typannotationen robusteren Code erstellen kann, und den "Pipes-and-Filters"-Ansatz für die Datenverarbeitung mit der neuen Stream-API von Java 8 untersucht.

Wir haben uns auch angesehen, wie sich Schnittstellen in Java 8 entwickelt haben und wie sie in Kombination mit anderen Funktionen verwendet werden, die wir in dieser Serie untersucht haben, einschließlich Lambda-Ausdrücken und statischen Schnittstellenmethoden.

Zum Abschluss habe ich Ihnen gezeigt, wie Sie mit den Projekten Joda-Time und ThreeTenABP auf einige zusätzliche Java 8-Funktionen zugreifen können, die Android derzeit standardmäßig nicht unterstützt.

Weitere Informationen zur Java 8-Version finden Sie auf der Oracle-Website.

Und während Sie hier sind, lesen Sie einige unserer anderen Beiträge über Java 8 und Android-Entwicklung!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.