JavaRush /Java-Blog /Random-DE /Java 8-Leitfaden. 1 Teil.
ramhead
Level 13

Java 8-Leitfaden. 1 Teil.

Veröffentlicht in der Gruppe Random-DE

„Java lebt noch – und die Leute fangen an, es zu verstehen.“

Willkommen zu meiner Einführung in Java 8. Dieser Leitfaden führt Sie Schritt für Schritt durch alle neuen Funktionen der Sprache. Anhand kurzer, einfacher Codebeispiele erfahren Sie, wie Sie Standardmethoden der Schnittstelle , Lambda-Ausdrücke , Referenzmethoden und wiederholbare Anmerkungen verwenden . Am Ende des Artikels werden Sie mit den neuesten Änderungen an APIs wie Streams, Funktionsschnittstellen, Assoziationserweiterungen und der neuen Date-API vertraut sein. Keine langweiligen Textwände – nur ein paar kommentierte Codeschnipsel. Genießen!

Standardmethoden für Schnittstellen

Mit Java 8 können wir nicht-abstrakte Methoden hinzufügen, die in der Schnittstelle implementiert werden, indem wir das Standardschlüsselwort verwenden . Diese Funktion wird auch als Erweiterungsmethoden bezeichnet . Hier ist unser erstes Beispiel: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } Zusätzlich zur abstrakten Methode berechne definiert die Formelschnittstelle auch eine Standardmethode sqrt . Klassen, die die Formelschnittstelle implementieren, implementieren nur die abstrakte Berechnungsmethode . Die standardmäßige sqrt- Methode kann sofort verwendet werden. Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 Das Formelobjekt wird als anonymes Objekt implementiert . Der Code ist ziemlich beeindruckend: 6 Codezeilen zur einfachen Berechnung von sqrt(a * 100) . Wie wir im nächsten Abschnitt sehen werden, gibt es in Java 8 eine attraktivere Möglichkeit, einzelne Methodenobjekte zu implementieren.

Lambda-Ausdrücke

Beginnen wir mit einem einfachen Beispiel für das Sortieren eines String-Arrays in früheren Java-Versionen: Die statistische Hilfsmethode Collections.sort verwendet eine Liste und einen Komparator, um die Elemente der angegebenen Liste zu sortieren. Was häufig vorkommt, ist, dass Sie anonyme Komparatoren erstellen und diese an Sortiermethoden übergeben. Anstatt ständig anonyme Objekte zu erstellen, können Sie mit Java 8 viel weniger Syntax und Lambda-Ausdrücke verwenden : Wie Sie sehen, ist der Code viel kürzer und einfacher zu lesen. Aber hier wird es noch kürzer: Für eine einzeilige Methode können Sie auf die geschweiften Klammern {} und das Schlüsselwort return verzichten . Aber hier wird der Code noch kürzer: Der Java-Compiler kennt die Parametertypen, Sie können sie also auch weglassen. Lassen Sie uns nun genauer untersuchen, wie Lambda-Ausdrücke im wirklichen Leben verwendet werden können. List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator () { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));

Funktionale Schnittstellen

Wie passen Lambda-Ausdrücke in das Java-Typsystem? Jedes Lambda entspricht einem bestimmten Typ, der durch eine Schnittstelle definiert wird. Und das sogenannte funktionale Interface muss genau eine deklarierte abstrakte Methode enthalten. Jeder Lambda-Ausdruck eines bestimmten Typs entspricht dieser abstrakten Methode. Da Standardmethoden keine abstrakten Methoden sind, können Sie Ihrer Funktionsschnittstelle Standardmethoden hinzufügen. Wir können eine beliebige Schnittstelle als Lambda-Ausdruck verwenden, sofern die Schnittstelle nur eine abstrakte Methode enthält. Um sicherzustellen, dass Ihre Schnittstelle diese Bedingungen erfüllt, müssen Sie die Annotation @FunctionalInterface hinzufügen . Der Compiler wird durch diese Annotation darüber informiert, dass die Schnittstelle nur eine Methode enthalten darf. Wenn er in dieser Schnittstelle auf eine zweite abstrakte Methode stößt, wird ein Fehler ausgegeben. Beispiel: Beachten Sie, dass dieser Code auch dann gültig wäre, wenn die Annotation @FunctionalInterface nicht deklariert worden wäre. @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

Verweise auf Methoden und Konstruktoren

Das obige Beispiel kann durch die Verwendung einer statistischen Methodenreferenz weiter vereinfacht werden: Mit Java 8 können Sie Referenzen auf Methoden und Konstruktoren mithilfe des Schlüsselworts symbols “ übergeben . Das obige Beispiel zeigt, wie statistische Methoden eingesetzt werden können. Wir können aber auch Methoden auf Objekte referenzieren: Schauen wir uns an, wie die Verwendung von :: für Konstruktoren funktioniert. Definieren wir zunächst ein Beispiel mit verschiedenen Konstruktoren: Als nächstes definieren wir die PersonFactory- Factory-Schnittstelle zum Erstellen neuer Personenobjekte : Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } interface PersonFactory

{ P create(String firstName, String lastName); } Anstatt die Factory manuell zu implementieren, verknüpfen wir alles über eine Konstruktorreferenz: Wir erstellen über Person::new eine Referenz auf den Konstruktor der Person- Klasse . Der Java-Compiler ruft automatisch den entsprechenden Konstruktor auf, indem er die Signatur der Konstruktoren mit der Signatur der PersonFactory.create -Methode vergleicht . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

Lambda-Region

Das Organisieren des Zugriffs auf Variablen des äußeren Bereichs über Lambda-Ausdrücke ähnelt dem Zugriff über ein anonymes Objekt. Sie können vom lokalen Bereich aus auf endgültige Variablen sowie auf Instanzfelder und Aggregatvariablen zugreifen.
Zugriff auf lokale Variablen
Wir können eine lokale Variable mit dem Modifikator final aus dem Gültigkeitsbereich eines Lambda-Ausdrucks lesen: Aber im Gegensatz zu anonymen Objekten müssen Variablen nicht als final deklariert werden, um über einen Lambda-Ausdruck zugänglich zu sein . Auch dieser Code ist korrekt: Allerdings muss die Variable num unveränderlich bleiben, d.h. für die Codekompilierung implizit final sein . Der folgende Code lässt sich nicht kompilieren: Änderungen an num innerhalb eines Lambda-Ausdrucks sind ebenfalls nicht zulässig. final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
Zugriff auf Instanzfelder und statistische Variablen
Im Gegensatz zu lokalen Variablen können wir Instanzfelder und statistische Variablen in Lambda-Ausdrücken lesen und ändern. Wir kennen dieses Verhalten von anonymen Objekten. class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
Zugriff auf Standardmethoden von Schnittstellen
Erinnern Sie sich an das Beispiel mit der Formelinstanz aus dem ersten Abschnitt? Die Formula- Schnittstelle definiert eine Standard- SQL- Methode , auf die von jeder Instanz von Formula aus zugegriffen werden kann , einschließlich anonymer Objekte. Dies funktioniert nicht mit Lambda-Ausdrücken. Auf Standardmethoden kann innerhalb von Lambda-Ausdrücken nicht zugegriffen werden. Der folgende Code lässt sich nicht kompilieren: Formula formula = (a) -> sqrt( a * 100);

Integrierte Funktionsschnittstellen

Die JDK 1.8-API enthält viele integrierte Funktionsschnittstellen. Einige davon sind aus früheren Java-Versionen bekannt. Zum Beispiel Comparator oder Runnable . Diese Schnittstellen werden mithilfe der Annotation @FunctionalInterface um Lambda-Unterstützung erweitert . Aber auch die Java 8 API steckt voller neuer funktionaler Schnittstellen, die Ihnen das Leben erleichtern werden. Einige dieser Schnittstellen sind aus der Guava-Bibliothek von Google bekannt . Auch wenn Sie mit dieser Bibliothek vertraut sind, sollten Sie sich die Erweiterung dieser Schnittstellen mit einigen nützlichen Erweiterungsmethoden genauer ansehen.
Prädikate
Prädikate sind boolesche Funktionen mit einem Argument. Die Schnittstelle enthält verschiedene Standardmethoden zum Erstellen komplexer logischer Ausdrücke (und/oder zum Negieren) mithilfe von Prädikaten Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
Funktionen
Funktionen benötigen ein Argument und erzeugen ein Ergebnis. Mit Standardmethoden können mehrere Funktionen zu einer Kette zusammengefasst werden (compose und andThen). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Lieferanten
Lieferanten geben ein Ergebnis (Instanz) des einen oder anderen Typs zurück. Im Gegensatz zu Funktionen akzeptieren Anbieter keine Argumente. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Verbraucher
Verbraucher repräsentieren Schnittstellenmethoden mit einem einzigen Argument. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Komparatoren
Komparatoren sind uns aus früheren Java-Versionen bekannt. Mit Java 8 können Sie Schnittstellen verschiedene Standardmethoden hinzufügen. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
Optionen
Die Optionals-Schnittstelle ist nicht funktionsfähig, aber sie ist ein großartiges Dienstprogramm zur Verhinderung von NullPointerException . Dies ist ein wichtiger Punkt für den nächsten Abschnitt, also werfen wir einen kurzen Blick auf die Funktionsweise dieser Schnittstelle. Die optionale Schnittstelle ist ein einfacher Container für Werte, die null oder nicht null sein können. Stellen Sie sich vor, dass eine Methode einen Wert oder nichts zurückgeben kann. In Java 8 geben Sie statt null eine optionale Instanz zurück . Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

Strom

java.util.Stream ist eine Folge von Elementen, für die eine oder mehrere Operationen ausgeführt werden. Jeder Stream-Vorgang ist entweder Zwischen- oder Endvorgang. Terminaloperationen geben ein Ergebnis eines bestimmten Typs zurück, während Zwischenoperationen das Stream-Objekt selbst zurückgeben, sodass eine Kette von Methodenaufrufen erstellt werden kann. Stream ist eine Schnittstelle wie java.util.Collection für Listen und Mengen (Karten werden nicht unterstützt). Jede Stream-Operation kann entweder sequentiell oder parallel ausgeführt werden. Werfen wir einen Blick darauf, wie Stream funktioniert. Zunächst erstellen wir Beispielcode in Form einer Liste von Zeichenfolgen: Sammlungen in Java 8 werden erweitert, sodass Sie ganz einfach Streams erstellen können, indem Sie Collection.stream() oder Collection.parallelStream() aufrufen . Im nächsten Abschnitt werden die wichtigsten, einfachsten Stream-Operationen erläutert. List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
Filter
Filter akzeptiert Prädikate, um alle Elemente des Streams zu filtern. Diese Operation ist eine Zwischenoperation, die es uns ermöglicht, andere Stream-Operationen (z. B. forEach) für das resultierende (gefilterte) Ergebnis aufzurufen. ForEach akzeptiert eine Operation, die für jedes Element des bereits gefilterten Streams ausgeführt wird. ForEach ist eine Terminaloperation. Darüber hinaus ist es unmöglich, andere Operationen aufzurufen. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Sortiert
Sorted ist eine Zwischenoperation, die eine sortierte Darstellung des Streams zurückgibt. Die Elemente werden in der richtigen Reihenfolge sortiert, es sei denn, Sie geben Ihren Comparator an . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" Beachten Sie, dass „sorted“ eine sortierte Darstellung des Streams erstellt, ohne dass sich dies auf die Sammlung selbst auswirkt. Die Reihenfolge der stringCollection- Elemente bleibt unverändert: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Karte
Die Zwischenkartenoperation wandelt jedes Element mithilfe der resultierenden Funktion in ein anderes Objekt um. Im folgenden Beispiel wird jede Zeichenfolge in eine Großbuchstabenzeichenfolge konvertiert. Sie können Map aber auch verwenden, um jedes Objekt in einen anderen Typ zu konvertieren. Der Typ der resultierenden Stream-Objekte hängt von der Art der Funktion ab, die Sie an Map übergeben. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Übereinstimmen
Verschiedene Matching-Operationen können verwendet werden, um die Wahrheit eines bestimmten Prädikats in der Stream-Relation zu testen. Alle Vergleichsoperationen sind terminal und geben ein boolesches Ergebnis zurück. boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
Zählen
Count ist eine Terminaloperation, die die Anzahl der Elemente des Streams als long zurückgibt . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Reduzieren
Dies ist eine Terminaloperation, die die Elemente des Streams mithilfe der übergebenen Funktion verkürzt. Das Ergebnis ist ein Optional, das den gekürzten Wert enthält. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Parallele Streams

Wie oben erwähnt, können Streams sequentiell oder parallel sein. Sequentielle Stream-Operationen werden in einem seriellen Thread ausgeführt, während parallele Stream-Operationen in mehreren parallelen Threads ausgeführt werden. Das folgende Beispiel zeigt, wie Sie die Leistung mithilfe eines parallelen Streams einfach steigern können. Erstellen wir zunächst eine große Liste einzigartiger Elemente: Jetzt ermitteln wir die Zeit, die für das Sortieren des Streams dieser Sammlung aufgewendet wird. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
Serieller Stream
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
Paralleler Stream
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms Wie Sie sehen, sind beide Fragmente nahezu identisch, die parallele Sortierung ist jedoch 50 % schneller. Sie müssen lediglich stream() in parallelStream() ändern .

Karte

Wie bereits erwähnt, unterstützen Karten keine Streams. Stattdessen begann Map, neue und nützliche Methoden zur Lösung allgemeiner Probleme zu unterstützen. Der obige Code sollte intuitiv sein: putIfAbsent warnt uns davor, zusätzliche Nullprüfungen zu schreiben. forEach akzeptiert eine Funktion, die für jeden der Kartenwerte ausgeführt werden soll. Dieses Beispiel zeigt, wie Operationen an Kartenwerten mithilfe von Funktionen ausgeführt werden: Als Nächstes erfahren Sie, wie Sie einen Eintrag für einen bestimmten Schlüssel nur dann entfernen, wenn er einem bestimmten Wert zugeordnet ist: Eine weitere gute Methode: Das Zusammenführen von Karteneinträgen ist recht einfach: Zusammenführen fügt entweder den Schlüssel/Wert in die Map ein, wenn für den angegebenen Schlüssel kein Eintrag vorhanden ist, oder es wird die Merge-Funktion aufgerufen, die den Wert des vorhandenen Eintrags ändert. Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null map.getOrDefault(42, "not found"); // not found map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION