Warum Sie bereits auf Java 8 upgraden müssen

Veröffentlicht: 2022-03-11

Die neueste Version der Java-Plattform, Java 8, wurde vor mehr als einem Jahr veröffentlicht. Viele Unternehmen und Entwickler arbeiten immer noch mit früheren Versionen, was verständlich ist, da es viele Probleme bei der Migration von einer Plattformversion auf eine andere gibt. Trotzdem starten viele Entwickler immer noch neue Anwendungen mit alten Java-Versionen. Dafür gibt es nur wenige gute Gründe, denn Java 8 hat einige wichtige Verbesserungen der Sprache gebracht.

Es gibt viele neue Funktionen in Java 8. Ich zeige Ihnen eine Handvoll der nützlichsten und interessantesten:

  • Lambda-Ausdrücke
  • Stream-API zum Arbeiten mit Sammlungen
  • Asynchrone Aufgabenverkettung mit CompletableFuture
  • Brandneue Zeit-API

Lambda-Ausdrücke

Ein Lambda ist ein Codeblock, der referenziert und an einen anderen Codeabschnitt zur späteren Ausführung einmal oder mehrmals übergeben werden kann. Anonyme Funktionen in anderen Sprachen sind beispielsweise Lambdas. Wie Funktionen können Lambdas zum Zeitpunkt ihrer Ausführung Argumente übergeben werden, wodurch ihre Ergebnisse geändert werden. Java 8 führte Lambda-Ausdrücke ein, die eine einfache Syntax zum Erstellen und Verwenden von Lambdas bieten.

Sehen wir uns ein Beispiel an, wie dies unseren Code verbessern kann. Hier haben wir einen einfachen Komparator, der zwei Integer Werte nach ihrem Modulo 2 vergleicht:

 class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }

Eine Instanz dieser Klasse kann in Zukunft im Code aufgerufen werden, wo dieser Komparator benötigt wird, wie folgt:

 ... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...

Die neue Lambda-Syntax ermöglicht es uns, dies einfacher zu tun. Hier ist ein einfacher Lambda-Ausdruck, der dasselbe tut wie die compare von BinaryComparator :

 (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

Die Struktur hat viele Ähnlichkeiten mit einer Funktion. In Klammern haben wir eine Liste von Argumenten erstellt. Die Syntax -> zeigt, dass dies ein Lambda ist. Und im rechten Teil dieses Ausdrucks richten wir das Verhalten unseres Lambda ein.

JAVA 8 LAMBDA-AUSDRUCK

Jetzt können wir unser vorheriges Beispiel verbessern:

 ... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...

Mit diesem Objekt können wir eine Variable definieren. Mal sehen, wie es aussieht:

 Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

Jetzt können wir diese Funktionalität wie folgt wiederverwenden:

 ... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...

Beachten Sie, dass in diesen Beispielen das Lambda auf dieselbe Weise an die Methode sort() übergeben wird, wie die Instanz von BinaryComparator im vorherigen Beispiel übergeben wird. Woher weiß die JVM, dass sie das Lambda richtig interpretiert?

Damit Funktionen Lambdas als Argumente annehmen können, führt Java 8 ein neues Konzept ein: Functional Interface . Eine funktionale Schnittstelle ist eine Schnittstelle, die nur eine abstrakte Methode hat. Tatsächlich behandelt Java 8 Lambda-Ausdrücke als spezielle Implementierung einer funktionalen Schnittstelle. Das bedeutet, dass der deklarierte Typ dieses Arguments nur eine funktionale Schnittstelle sein muss, um ein Lambda als Methodenargument zu erhalten.

Wenn wir eine funktionale Schnittstelle deklarieren, können wir die Notation @FunctionalInterface hinzufügen, um Entwicklern zu zeigen, was es ist:

 @FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }

Jetzt können wir die Methode sendDTO und verschiedene Lambdas übergeben, um ein unterschiedliches Verhalten zu erzielen, wie folgt:

 sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));

Methodenreferenzen

Lambda-Argumente ermöglichen es uns, das Verhalten einer Funktion oder Methode zu ändern. Wie wir im letzten Beispiel sehen können, dient das Lambda manchmal nur zum Aufrufen einer anderen Methode ( sendToAndroid oder sendToIos ). Für diesen speziellen Fall führt Java 8 eine praktische Abkürzung ein: Methodenreferenzen . Diese abgekürzte Syntax stellt ein Lambda dar, das eine Methode aufruft, und hat die Form objectName::methodName . Dadurch können wir das vorherige Beispiel noch prägnanter und lesbarer machen:

 sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);

In diesem Fall sind die Methoden sendToAndroid und sendToIos in this Klasse implementiert. Wir können auch auf die Methoden eines anderen Objekts oder einer anderen Klasse verweisen.

Stream-API

Java 8 bringt neue Möglichkeiten für die Arbeit mit Collections in Form einer brandneuen Stream-API. Diese neue Funktionalität wird vom Paket java.util.stream bereitgestellt und zielt darauf ab, einen funktionaleren Ansatz für die Programmierung mit Sammlungen zu ermöglichen. Wie wir sehen werden, ist dies vor allem dank der neuen Lambda-Syntax möglich, die wir gerade besprochen haben.

Die Stream-API bietet ein einfaches Filtern, Zählen und Zuordnen von Sammlungen sowie verschiedene Möglichkeiten, um Segmente und Teilmengen von Informationen daraus abzurufen. Dank der Syntax im funktionalen Stil ermöglicht die Stream-API kürzeren und eleganteren Code für die Arbeit mit Sammlungen.

Beginnen wir mit einem kurzen Beispiel. Wir werden dieses Datenmodell in allen Beispielen verwenden:

 class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }

Stellen wir uns vor, wir müssten alle Autoren in einer books drucken, die nach 2005 ein Buch geschrieben haben. Wie würden wir das in Java 7 machen?

 for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }

Und wie würden wir es in Java 8 machen?

 books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element

Es ist nur ein Ausdruck! Das Aufrufen der Methode stream() für eine beliebige Collection gibt ein Stream Objekt zurück, das alle Elemente dieser Sammlung kapselt. Dies kann mit verschiedenen Modifikatoren aus der Stream-API wie filter() und map() manipuliert werden. Jeder Modifikator gibt ein neues Stream Objekt mit den Ergebnissen der Änderung zurück, das weiter manipuliert werden kann. Die .forEach() Methode ermöglicht es uns, eine Aktion für jede Instanz des resultierenden Streams auszuführen.

Dieses Beispiel demonstriert auch die enge Beziehung zwischen funktionaler Programmierung und Lambda-Ausdrücken. Beachten Sie, dass das an jede Methode im Stream übergebene Argument entweder ein benutzerdefiniertes Lambda oder eine Methodenreferenz ist. Technisch gesehen kann jeder Modifikator jede funktionale Schnittstelle erhalten, wie im vorherigen Abschnitt beschrieben.

Die Stream-API hilft Entwicklern, Java-Sammlungen aus einem neuen Blickwinkel zu betrachten. Stellen Sie sich jetzt vor, dass wir eine Map der verfügbaren Sprachen in jedem Land benötigen. Wie würde dies in Java 7 implementiert werden?

 Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }

In Java 8 sind die Dinge etwas ordentlicher:

 import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));

Die Methode collect() ermöglicht es uns, die Ergebnisse eines Streams auf unterschiedliche Weise zu sammeln. Hier können wir sehen, dass es zuerst nach Land gruppiert und dann jede Gruppe nach Sprache abbildet. (groupingBy( groupingBy() und toSet() sind beides statische Methoden der Collectors -Klasse.)

JAVA 8 STREAM-API

Es gibt viele andere Fähigkeiten der Stream-API. Die vollständige Dokumentation finden Sie hier. Ich empfehle, weiterzulesen, um ein tieferes Verständnis für all die leistungsstarken Tools zu erlangen, die dieses Paket zu bieten hat.

Asynchrone Aufgabenverkettung mit CompletableFuture

Im Paket java.util.concurrent von Java 7 gibt es eine Schnittstelle Future<T> , die es uns ermöglicht, den Status oder das Ergebnis einer asynchronen Aufgabe in der Zukunft abzurufen. Um diese Funktion nutzen zu können, müssen wir:

  1. Erstellen Sie einen ExecutorService , der die Ausführung asynchroner Aufgaben verwaltet und Future -Objekte generieren kann, um deren Fortschritt zu verfolgen.
  2. Erstellen Sie eine asynchron Runnable Aufgabe.
  3. Führen Sie die Aufgabe im ExecutorService aus, der ein Future bereitstellt, das Zugriff auf den Status oder die Ergebnisse gewährt.

Um die Ergebnisse einer asynchronen Aufgabe zu nutzen, ist es notwendig, ihren Fortschritt von außen mit den Methoden der Future -Schnittstelle zu überwachen und, wenn sie fertig ist, die Ergebnisse explizit abzurufen und weitere Aktionen damit durchzuführen. Dies kann ziemlich komplex sein, um es fehlerfrei zu implementieren, insbesondere in Anwendungen mit einer großen Anzahl gleichzeitiger Tasks.

In Java 8 wird das Future -Konzept jedoch mit der Schnittstelle CompletableFuture<T> weiterentwickelt, die die Erstellung und Ausführung von Ketten asynchroner Aufgaben ermöglicht. Es ist ein leistungsstarker Mechanismus zum Erstellen asynchroner Anwendungen in Java 8, da es uns ermöglicht, die Ergebnisse jeder Aufgabe nach Abschluss automatisch zu verarbeiten.

Sehen wir uns ein Beispiel an:

 import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);

Die Methode CompletableFuture.supplyAsync erstellt eine neue asynchrone Aufgabe im Standard- Executor (normalerweise ForkJoinPool ). Wenn die Aufgabe abgeschlossen ist, werden ihre Ergebnisse automatisch als Argumente an die Funktion this::getLinks , die auch in einer neuen asynchronen Aufgabe ausgeführt wird. Schließlich werden die Ergebnisse dieser zweiten Stufe automatisch in System.out . thenApply() und thenAccept() sind nur zwei von mehreren nützlichen Methoden, die Ihnen helfen, gleichzeitige Tasks zu erstellen, ohne manuell Executors zu verwenden.

CompletableFuture vereinfacht die Verwaltung der Sequenzierung komplexer asynchroner Vorgänge. Angenommen, wir müssen eine mehrstufige mathematische Operation mit drei Aufgaben erstellen. Aufgabe 1 und Aufgabe 2 verwenden unterschiedliche Algorithmen, um ein Ergebnis für den ersten Schritt zu finden, und wir wissen, dass nur einer von ihnen funktioniert, während der andere fehlschlägt. Welches funktioniert, hängt jedoch von den Eingabedaten ab, die wir im Voraus nicht wissen. Das Ergebnis dieser Aufgaben muss mit dem Ergebnis von Aufgabe 3 summiert werden. Daher müssen wir entweder das Ergebnis von Aufgabe 1 oder Aufgabe 2 und das Ergebnis von Aufgabe 3 finden. Um dies zu erreichen, können wir so etwas schreiben:

 import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution

Wenn wir untersuchen, wie Java 8 damit umgeht, werden wir sehen, dass alle drei Aufgaben gleichzeitig und asynchron ausgeführt werden. Obwohl Aufgabe 2 mit einer Ausnahme fehlschlägt, wird das Endergebnis erfolgreich berechnet und gedruckt.

JAVA 8 ASYNCHRONE PROGRAMMIERUNG MIT CompletableFuture

CompletableFuture macht es viel einfacher, asynchrone Aufgaben mit mehreren Phasen zu erstellen, und gibt uns eine einfache Schnittstelle, um genau zu definieren, welche Aktionen nach Abschluss jeder Phase durchgeführt werden sollen.

Java-API für Datum und Uhrzeit

Wie von Javas eigenem Eingeständnis angegeben:

Vor der Version Java SE 8 wurde der Java-Mechanismus für Datum und Uhrzeit von den Klassen java.util.Date , java.util.Calendar und java.util.TimeZone sowie deren Unterklassen wie java.util.GregorianCalendar . Diese Klassen hatten mehrere Nachteile, darunter

  • Die Calendar-Klasse war nicht typsicher.
  • Da die Klassen änderbar waren, konnten sie nicht in Multithread-Anwendungen verwendet werden.
  • Fehler im Anwendungscode waren aufgrund der ungewöhnlichen Anzahl von Monaten und der fehlenden Typsicherheit häufig.“

Java 8 löst diese seit langem bestehenden Probleme endlich mit dem neuen java.time -Paket, das Klassen für die Arbeit mit Datum und Uhrzeit enthält. Alle von ihnen sind unveränderlich und haben ähnliche APIs wie das beliebte Framework Joda-Time, das fast alle Java-Entwickler in ihren Anwendungen anstelle der nativen Date , Calendar und TimeZone verwenden.

Hier sind einige der nützlichen Klassen in diesem Paket:

  • Clock – Eine Uhr, die die aktuelle Uhrzeit angibt, einschließlich des aktuellen Zeitpunkts, des Datums und der Uhrzeit mit Zeitzone.
  • Duration und Period – Eine Zeitspanne. Duration verwendet zeitbasierte Werte wie „76,8 Sekunden“ und „ Period “ datumsbasierte Werte wie „4 Jahre, 6 Monate und 12 Tage“.
  • Instant - Ein sofortiger Zeitpunkt in mehreren Formaten.
  • LocalDate , LocalDateTime , LocalTime , Year , YearMonth – Ein Datum, eine Uhrzeit, ein Jahr, ein Monat oder eine Kombination davon ohne Zeitzone im ISO-8601-Kalendersystem.
  • OffsetDateTime , OffsetTime – Ein Datum/Uhrzeit mit einem Versatz von UTC/Greenwich im ISO-8601-Kalendersystem, z. B. „2015-08-29T14:15:30+01:00“.
  • ZonedDateTime – Ein Datum/Uhrzeit mit einer zugeordneten Zeitzone im ISO-8601-Kalendersystem, z. B. „1986-08-29T10:15:30+01:00 Europe/Paris“.

JAVA 8 TIME-API

Manchmal müssen wir ein relatives Datum wie „erster Dienstag des Monats“ finden. Für diese Fälle stellt java.time eine spezielle Klasse TemporalAdjuster bereit. Die TemporalAdjuster -Klasse enthält einen Standardsatz von Anpassungselementen, die als statische Methoden verfügbar sind. Diese ermöglichen uns:

  • Finden Sie den ersten oder letzten Tag des Monats.
  • Finden Sie den ersten oder letzten Tag des nächsten oder vorherigen Monats.
  • Finden Sie den ersten oder letzten Tag des Jahres.
  • Finden Sie den ersten oder letzten Tag des nächsten oder vorherigen Jahres.
  • Finden Sie den ersten oder letzten Wochentag innerhalb eines Monats, z. B. „erster Mittwoch im Juni“.
  • Suchen Sie den nächsten oder vorherigen Wochentag, z. B. „nächsten Donnerstag“.

Hier ist ein kurzes Beispiel, wie man den ersten Dienstag des Monats erhält:

 LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Verwenden Sie immer noch Java 7? Holen Sie sich mit dem Programm! #Java8
Twittern

Java 8 im Überblick

Wie wir sehen können, ist Java 8 eine epochale Version der Java-Plattform. Es gibt viele Sprachänderungen, insbesondere mit der Einführung von Lambdas, die einen Schritt darstellen, um mehr funktionale Programmierfähigkeiten in Java einzubringen. Die Stream-API ist ein gutes Beispiel dafür, wie Lambdas die Art und Weise verändern können, wie wir mit Standard-Java-Tools arbeiten, an die wir bereits gewöhnt sind.

Außerdem bringt Java 8 einige neue Funktionen für die Arbeit mit asynchroner Programmierung und eine dringend benötigte Überarbeitung seiner Datums- und Uhrzeit-Tools.

Zusammen stellen diese Änderungen einen großen Schritt nach vorn für die Java-Sprache dar und machen die Java-Entwicklung interessanter und effizienter.