Wir haben lange darauf gewartet, dass Lambda das Konzept der Abschlüsse nach Java bringt, aber wenn wir es nicht in Sammlungen verwenden, verlieren wir viel Wert. Das Problem der Migration bestehender Schnittstellen zum Lambda-Stil wurde durch Standardmethoden gelöst. In diesem Artikel werden wir die Massendatenoperation (Massenoperation) in Java-Sammlungen eingehend analysieren und das Geheimnis der mächtigsten Rolle von Lambda lüften.
1.Über JSR335
JSR ist die Abkürzung für Java Specification Requests, was Java Specification Request bedeutet. Die wichtigste Verbesserung der Java 8-Version ist das Lambda-Projekt (JSR 335), das darauf abzielt, Java einfacher zu machen, Code für Multi-Core-Prozessoren zu schreiben. JSR 335 = Lambda-Ausdruck + Schnittstellenverbesserung (Standardmethode) + Batch-Datenoperation. Zusammen mit den beiden vorherigen Artikeln haben wir den relevanten Inhalt von JSR335 vollständig kennengelernt.
2. Externe vs. interne Iteration
In der Vergangenheit konnten Java-Sammlungen keine interne Iteration ausdrücken, sondern boten nur eine Möglichkeit der externen Iteration, nämlich eine for- oder while-Schleife.
Kopieren Sie den Codecode wie folgt:
List personen = asList(new Person("Joe"), new Person("Jim"), new Person("John"));
für (Person p : Personen) {
p.setLastName("Doe");
}
Das obige Beispiel ist unser vorheriger Ansatz, die sogenannte externe Iteration. Die Schleife ist eine Schleife mit fester Sequenz. Wenn wir im heutigen Multi-Core-Zeitalter eine Parallelschleife durchführen möchten, müssen wir den obigen Code ändern. Wie stark die Effizienz verbessert werden kann, ist noch ungewiss und birgt bestimmte Risiken (Probleme mit der Thread-Sicherheit usw.).
Um die interne Iteration zu beschreiben, müssen wir eine Klassenbibliothek wie Lambda verwenden. Schreiben wir die obige Schleife mit Lambda und Collection.forEach neu.
Kopieren Sie den Code wie folgt: personen.forEach(p->p.setLastName("Doe"));
Jetzt steuert die JDK-Bibliothek die Schleife. Wir müssen uns nicht darum kümmern, wie der Nachname für jedes Personenobjekt festgelegt wird. Die Bibliothek kann je nach laufender Umgebung entscheiden, wie dies geschieht: parallel, außer Betrieb oder verzögert Laden. Dies ist eine interne Iteration und der Client übergibt das Verhalten p.setLastName als Daten an die API.
Tatsächlich ist die interne Iteration nicht eng mit der Stapeloperation von Sammlungen verbunden. Mit ihrer Hilfe können wir die Änderungen im grammatikalischen Ausdruck spüren. Das wirklich Interessante an Batch-Operationen ist die neue Stream-API. Das neue Paket java.util.stream wurde zu JDK 8 hinzugefügt.
3.Stream-API
Stream stellt nur einen Datenfluss dar und hat keine Datenstruktur, sodass er nach einmaligem Durchlaufen nicht mehr durchlaufen werden kann (dies muss bei der Programmierung beachtet werden, im Gegensatz zu Collection sind immer noch Daten darin, egal wie oft). es wird durchlaufen). Die Quelle kann Sammlung, Array, io usw. sein.
3.1 Zwischen- und Endpunktmethoden
Streaming bietet eine Schnittstelle für den Betrieb großer Datenmengen und macht Datenvorgänge einfacher und schneller. Es verfügt über Methoden wie Filtern, Zuordnen und Reduzieren der Anzahl von Durchläufen. Diese Methoden sind in zwei Typen unterteilt: Zwischenmethoden und Terminalmethoden. Die „Stream“-Abstraktion sollte von Natur aus kontinuierlich sein Wir möchten das Endergebnis erhalten. Wenn ja, müssen Endpunktoperationen verwendet werden, um die vom Stream erzeugten Endergebnisse zu sammeln. Der Unterschied zwischen diesen beiden Methoden besteht darin, dass sie sich den Rückgabewert ansehen. Wenn es sich um einen Stream handelt, handelt es sich um eine Zwischenmethode, andernfalls handelt es sich um eine Endmethode. Weitere Informationen finden Sie in der Stream-API.
Stellen Sie kurz mehrere Zwischenmethoden (Filter, Karte) und Endpunktmethoden (Sammeln, Summen) vor.
3.1.1Filter
Die Implementierung von Filterfunktionen in Datenströmen ist die natürlichste Operation, die wir uns vorstellen können. Die Stream-Schnittstelle stellt eine Filtermethode bereit, die eine Predicate-Implementierung akzeptiert, die einen Vorgang darstellt, um einen Lambda-Ausdruck zu verwenden, der Filterbedingungen definiert.
Kopieren Sie den Codecode wie folgt:
Personen auflisten = …
Stream personenOver18 = personen.stream().filter(p -> p.getAge() > 18);//Personen über 18 Jahre filtern
3.1.2Karte
Angenommen, wir filtern jetzt einige Daten, beispielsweise beim Konvertieren von Objekten. Mit der Map-Operation können wir eine Implementierung einer Funktion ausführen (das generische T und R von Function<T, R> stellen Ausführungseingaben bzw. Ausführungsergebnisse dar), die Eingabeparameter akzeptiert und zurückgibt. Sehen wir uns zunächst an, wie man es als anonyme innere Klasse beschreibt:
Kopieren Sie den Codecode wie folgt:
Stream Erwachsene = Personen
.Strom()
.filter(p -> p.getAge() > 18)
.map(neue Funktion() {
@Override
öffentlicher Erwachsener bewerben(Person Person) {
einen neuen Erwachsenen(eine neue Person) zurückgeben;//Eine Person über 18 Jahre in einen Erwachsenen umwandeln
}
});
Konvertieren Sie nun das obige Beispiel in einen Lambda-Ausdruck:
Kopieren Sie den Codecode wie folgt:
Streammap = personen.stream()
.filter(p -> p.getAge() > 18)
.map(Person -> neuer Erwachsener(Person));
3.1.3Zählen
Die Zählmethode ist die Endpunktmethode eines Streams, die die endgültigen Statistiken der Stream-Ergebnisse erstellen und eine Ganzzahl zurückgeben kann. Berechnen wir beispielsweise die Gesamtzahl der Personen ab 18 Jahren:
Kopieren Sie den Codecode wie folgt:
int countOfAdult=persons.stream()
.filter(p -> p.getAge() > 18)
.map(Person -> neuer Erwachsener(Person))
.zählen();
3.1.4Sammeln
Die Collect-Methode ist auch eine Endpunktmethode eines Streams, die die Endergebnisse sammeln kann.
Kopieren Sie den Codecode wie folgt:
Liste adultList= personen.stream()
.filter(p -> p.getAge() > 18)
.map(Person -> neuer Erwachsener(Person))
.collect(Collectors.toList());
Oder wenn wir eine bestimmte Implementierungsklasse verwenden möchten, um die Ergebnisse zu sammeln:
Kopieren Sie den Codecode wie folgt:
Liste adultList = Personen
.Strom()
.filter(p -> p.getAge() > 18)
.map(Person -> neuer Erwachsener(Person))
.collect(Collectors.toCollection(ArrayList::new));
Aus Platzgründen werden andere Zwischenmethoden und Endpunktmethoden nicht einzeln vorgestellt. Nachdem Sie die obigen Beispiele gelesen haben, müssen Sie nur den Unterschied zwischen diesen beiden Methoden verstehen und können entscheiden, ob Sie sie entsprechend Ihren Anforderungen verwenden möchten später.
3.2 Sequentielle Strömung und Parallelströmung
Jeder Stream verfügt über zwei Modi: sequentielle Ausführung und parallele Ausführung.
Ablauf:
Kopieren Sie den Codecode wie folgt:
List <Person> people = list.getStream.collect(Collectors.toList());
Parallele Streams:
Kopieren Sie den Codecode wie folgt:
List <Person> people = list.getStream.parallel().collect(Collectors.toList());
Wie der Name schon sagt, wird bei Verwendung der sequentiellen Methode zum Durchlaufen jedes Element gelesen, bevor das nächste Element gelesen wird. Bei Verwendung der parallelen Durchquerung wird das Array in mehrere Segmente unterteilt, die jeweils in einem anderen Thread verarbeitet werden, und die Ergebnisse werden dann gemeinsam ausgegeben.
3.2.1 Parallelstromprinzip:
Kopieren Sie den Codecode wie folgt:
Liste originalList = someData;
split1 = originalList(0, mid);//Teilen Sie die Daten in kleine Teile
split2 = originalList(mid,end);
new Runnable(split1.process());//Operationen in kleinen Teilen ausführen
new Runnable(split2.process());
Liste überarbeitetList = split1 + split2;//Füge die Ergebnisse zusammen
3.2.2 Vergleich von sequentiellen und parallelen Leistungstests
Wenn es sich um eine Multi-Core-Maschine handelt, ist der parallele Stream theoretisch doppelt so schnell wie der sequentielle Stream. Das Folgende ist der Testcode
Kopieren Sie den Codecode wie folgt:
long t0 = System.nanoTime();
// Initialisieren Sie einen ganzzahligen Stream mit einem Bereich von 1 Million und finden Sie eine Zahl, die durch 2 teilbar ist. toArray() ist die Endpunktmethode
int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray();
long t1 = System.nanoTime();
//Gleiche Funktion wie oben, hier verwenden wir einen parallelen Stream zur Berechnung
int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray();
long t2 = System.nanoTime();
//Die Ergebnisse meiner lokalen Maschine sind seriell: 0,06 s, parallel 0,02 s, was beweist, dass der parallele Fluss tatsächlich schneller ist als der sequentielle Fluss.
System.out.printf("seriell: %.2fs, parallel %.2fs%n", (t1 - t0) * 1e-9, (t2 - t1) * 1e-9);
3.3 Über das Folk/Join-Framework
Anwendungshardwareparallelität ist in Java 7 verfügbar. Eine der neuen Funktionen des Pakets java.util.concurrent ist ein paralleles Zerlegungsframework im Fork-Join-Stil. Es ist auch sehr leistungsfähig und effizient Gehen Sie hier auf Details ein. Im Vergleich zu Stream.parallel() bevorzuge ich Letzteres.
4. Zusammenfassung
Ohne Lambda ist die Verwendung von Stream ziemlich umständlich. Es generiert eine große Anzahl anonymer interner Klassen, wie im obigen 3.1.2map-Beispiel. Wenn es keine Standardmethode gibt, führen Änderungen am Sammlungsframework zwangsläufig zu vielen Änderungen. Daher macht die Lambda + Standardmethode die JDK-Bibliothek leistungsfähiger und flexibler. Die Verbesserungen des Stream- und Sammlungsframeworks sind der beste Beweis.