Nous attendons depuis longtemps que Lambda apporte le concept de fermeture à Java, mais si nous ne l'utilisons pas dans les collections, nous perdons beaucoup de valeur. Le problème de la migration des interfaces existantes vers le style lambda a été résolu grâce à des méthodes par défaut. Dans cet article, nous analyserons en profondeur l'opération de données en masse (opération en masse) dans les collections Java et percerons le mystère du rôle le plus puissant de lambda.
1. À propos du JSR335
JSR est l'abréviation de Java Spécification Requests, qui signifie demande de spécification Java. La principale amélioration de la version Java 8 est le projet Lambda (JSR 335), qui vise à faciliter l'écriture de code Java pour les processeurs multicœurs. JSR 335=expression lambda + amélioration de l'interface (méthode par défaut) + opération de données par lots. Avec les deux articles précédents, nous avons complètement appris le contenu pertinent du JSR335.
2. Itération externe ou interne
Dans le passé, les collections Java ne pouvaient pas exprimer une itération interne, mais ne fournissaient qu'un seul moyen d'itération externe, c'est-à-dire une boucle for ou while.
Copiez le code comme suit :
Liste des personnes = asList(new Person("Joe"), new Person("Jim"), new Person("John"));
pour (Personne p : personnes) {
p.setLastName("Biche");
}
L'exemple ci-dessus est notre approche précédente, qui est ce qu'on appelle l'itération externe. La boucle est une boucle à séquence fixe. À l'ère multicœur d'aujourd'hui, si nous voulons boucler en parallèle, nous devons modifier le code ci-dessus. La mesure dans laquelle l'efficacité peut être améliorée est encore incertaine, et cela entraînera certains risques (problèmes de sécurité des threads, etc.).
Pour décrire l'itération interne, nous devons utiliser une bibliothèque de classes comme Lambda. Réécrivons la boucle ci-dessus en utilisant lambda et Collection.forEach.
Copiez le code comme suit : people.forEach(p->p.setLastName("Doe"));
Désormais, la bibliothèque jdk contrôle la boucle. Nous n'avons plus besoin de nous soucier de la façon dont le nom de famille est défini pour chaque objet personne. La bibliothèque peut décider comment le faire en fonction de l'environnement d'exécution, parallèle, dans le désordre ou paresseux. chargement. Il s'agit d'une itération interne et le client transmet le comportement p.setLastName sous forme de données dans l'API.
En fait, l'itération interne n'est pas étroitement liée au fonctionnement par lots des collections. Avec son aide, on peut ressentir les changements dans l'expression grammaticale. La chose vraiment intéressante liée aux opérations par lots est la nouvelle API de flux. Le nouveau package java.util.stream a été ajouté au JDK 8.
3. API de flux
Le flux ne représente qu'un flux de données et n'a pas de structure de données, il ne peut donc plus être parcouru après avoir été parcouru une fois (il faut y prêter attention lors de la programmation, contrairement à Collection, il contient toujours des données, quel que soit le nombre de fois). il est parcouru). La source peut être Collection, array, io, etc.
3.1 Méthodes intermédiaires et finales
Le streaming fournit une interface pour exploiter le Big Data, rendant les opérations de données plus faciles et plus rapides. Il dispose de méthodes telles que le filtrage, le mappage et la réduction du nombre de parcours. Ces méthodes sont divisées en deux types : les méthodes intermédiaires et les méthodes terminales. L'abstraction "stream" doit être continue par nature. Les méthodes intermédiaires renvoient toujours un Stream, donc si. nous voulons obtenir le résultat final. Si tel est le cas, les opérations de point final doivent être utilisées pour collecter les résultats finaux produits par le flux. La différence entre ces deux méthodes est de regarder sa valeur de retour. S'il s'agit d'un Stream, c'est une méthode intermédiaire, sinon c'est une méthode de fin. Veuillez vous référer à l'API de Stream pour plus de détails.
Présentez brièvement plusieurs méthodes intermédiaires (filtre, carte) et méthodes de point final (collecte, somme)
3.1.1Filtre
Implémenter des fonctions de filtrage dans les flux de données est l’opération la plus naturelle à laquelle on puisse penser. L'interface Stream expose une méthode de filtre, qui accepte une implémentation de Predicate représentant une opération pour utiliser une expression lambda qui définit les conditions de filtre.
Copiez le code comme suit :
Liste des personnes = …
Stream peopleOver18 = people.stream().filter(p -> p.getAge() > 18);//Filtrer les personnes de plus de 18 ans
3.1.2Carte
Supposons que nous filtrions certaines données maintenant, par exemple lors de la conversion d'objets. L'opération Map nous permet d'exécuter une implémentation d'une fonction (les T et R génériques de Function<T, R> représentent respectivement l'entrée d'exécution et les résultats d'exécution), qui accepte les paramètres d'entrée et les renvoie. Voyons d’abord comment la décrire comme une classe interne anonyme :
Copiez le code comme suit :
Flux adulte = personnes
.flux()
.filter(p -> p.getAge() > 18)
.map(nouvelle fonction() {
@Outrepasser
public Adulte postuler (Personne personne) {
return new Adult(person);//Convertir une personne de plus de 18 ans en adulte
}
});
Maintenant, convertissez l'exemple ci-dessus en une expression lambda :
Copiez le code comme suit :
Carte du flux = personnes.stream()
.filter(p -> p.getAge() > 18)
.map(personne -> new Adult(person));
3.1.3Comptage
La méthode count est la méthode du point final d'un flux, qui peut établir les statistiques finales des résultats du flux et renvoyer un entier. Par exemple, calculons le nombre total de personnes âgées de 18 ans ou plus :
Copiez le code comme suit :
int countOfAdult=personnes.stream()
.filter(p -> p.getAge() > 18)
.map(personne -> nouvel adulte(personne))
.compter();
3.1.4Collecter
La méthode collect est également une méthode de point final d’un flux, qui peut collecter les résultats finaux.
Copiez le code comme suit :
Liste adultList= personnes.stream()
.filter(p -> p.getAge() > 18)
.map(personne -> nouvel adulte(personne))
.collect(Collectors.toList());
Ou si nous voulons utiliser une classe d’implémentation spécifique pour collecter les résultats :
Copiez le code comme suit :
Liste adulteListe = personnes
.flux()
.filter(p -> p.getAge() > 18)
.map(personne -> nouvel adulte(personne))
.collect(Collectors.toCollection(ArrayList::new));
En raison de l'espace limité, les autres méthodes intermédiaires et méthodes finales ne seront pas présentées une par une. Après avoir lu les exemples ci-dessus, il vous suffit de comprendre la différence entre ces deux méthodes et vous pouvez décider de les utiliser en fonction de vos besoins. plus tard.
3.2 Flux séquentiel et flux parallèle
Chaque Stream a deux modes : exécution séquentielle et exécution parallèle.
Flux de séquence :
Copiez le code comme suit :
Liste <Personne> people = list.getStream.collect(Collectors.toList());
Flux parallèles :
Copiez le code comme suit :
Liste <Personne> people = list.getStream.parallel().collect(Collectors.toList());
Comme son nom l'indique, lors de l'utilisation de la méthode séquentielle pour parcourir, chaque élément est lu avant la lecture de l'élément suivant. Lors de l'utilisation du parcours parallèle, le tableau sera divisé en plusieurs segments, chacun étant traité dans un thread différent, puis les résultats sont affichés ensemble.
3.2.1 Principe du flux parallèle :
Copiez le code comme suit :
Liste originalList = someData ;
split1 = originalList(0, mid);//Diviser les données en petites parties
split2 = originalList (milieu, fin);
new Runnable(split1.process());//Exécuter des opérations en petites parties
new Runnable(split2.process());
Liste réviséeListe = split1 + split2;//Fusionner les résultats
3.2.2 Comparaison des tests de performances séquentiels et parallèles
S'il s'agit d'une machine multicœur, le flux parallèle sera théoriquement deux fois plus rapide que le flux séquentiel. Voici le code de test.
Copiez le code comme suit :
long t0 = System.nanoTime();
//Initialisez un flux entier avec une plage de 1 million et trouvez un nombre qui peut être divisible par 2. toArray() est la méthode du point final
int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray();
long t1 = System.nanoTime();
// Même fonction que ci-dessus, nous utilisons ici un flux parallèle pour calculer
int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray();
long t2 = System.nanoTime();
//Les résultats de ma machine locale sont en série : 0,06s, parallèle 0,02s, ce qui prouve que le flux parallèle est effectivement plus rapide que le flux séquentiel.
System.out.printf("série : %.2fs, parallèle %.2fs%n", (t1 - t0) * 1e-9, (t2 - t1) * 1e-9);
3.3 À propos du framework Folk/Join
Le parallélisme matériel d'application est disponible dans Java 7. L'une des nouvelles fonctionnalités du package java.util.concurrent est un cadre de décomposition parallèle de style fork-join. Il est également très puissant et efficace. Les étudiants intéressés peuvent l'étudier. entrez dans les détails ici. Par rapport à Stream.parallel(), je préfère ce dernier.
4. Résumé
Sans lambda, Stream est assez difficile à utiliser. Il générera un grand nombre de classes internes anonymes, comme l'exemple 3.1.2map ci-dessus, s'il n'y a pas de méthode par défaut, les modifications apportées au framework de collection entraîneront inévitablement de nombreux changements. donc la méthode lambda+default rend la bibliothèque jdk plus puissante et flexible, les améliorations de Stream et du framework de collection en sont la meilleure preuve.