Introduction à Lambda
Les expressions de lambda sont une nouvelle caractéristique importante de Java SE 8. Les expressions Lambda vous permettent de remplacer les interfaces fonctionnelles par expressions. L'expression de lambda est comme la méthode, qui fournit une liste de paramètres normale et un corps (corps, qui peut être une expression ou un bloc de code) qui utilise ces paramètres.
Lambda Expressions améliore également la bibliothèque de collecte. Java SE 8 ajoute 2 packages qui exploitent des opérations par lots sur les données de collecte: package java.util.function et package java.util.stream . Un flux est comme un itérateur, mais avec de nombreuses fonctionnalités supplémentaires attachées. En général, les expressions et les flux Lambda sont les plus grands changements puisque la langue Java ajoute des génériques et des annotations.
Les expressions de Lambda sont essentiellement des méthodes anonymes, et leur couche sous-jacente est mise en œuvre par le biais de directives invokedynamic pour générer des classes anonymes. Il fournit une syntaxe et une méthode d'écriture plus simples, vous permettant de remplacer les interfaces fonctionnelles par des expressions. Aux yeux de certaines personnes, Lambda peut rendre votre code plus concis et ne pas l'utiliser du tout - ce point de vue est certainement OK, mais l'important est que Lambda apporte des fermetures à Java. Grâce à la prise en charge de LAMDBA pour les collections, Lambda a considérablement amélioré les performances lors de la traversée des collections dans des conditions de processeur multi-core. De plus, nous pouvons traiter les collections sous forme de flux de données - ce qui est très attrayant.
Syntaxe lambda
La syntaxe de Lambda est extrêmement simple, similaire à la structure suivante:
(paramètres) -> Expression
ou
(paramètres) -> {instructions; }Les expressions de lambda sont composées de trois parties:
1. Paramats: une liste de paramètres formels dans des méthodes similaires, les paramètres ici sont des paramètres dans l'interface fonctionnelle. Les types de paramètres ici peuvent être explicitement déclarés ou non déclarés mais implicitement déduits par le JVM. De plus, lorsqu'il n'y a qu'un seul type d'inférence, les parenthèses peuvent être omises.
2. ->: Il peut être compris comme "être utilisé"
3. Méthode Corps: il peut s'agir d'une expression ou d'un bloc de code, c'est l'implémentation de la méthode dans l'interface fonctionnelle. Un bloc de code peut renvoyer une valeur ou une inversion rien. Le bloc de code ici équivaut à la méthode du corps de la méthode. S'il s'agit d'une expression, vous pouvez également renvoyer une valeur ou ne rien retourner.
Utilisons les exemples suivants pour illustrer:
// Exemple 1: pas besoin d'accepter les paramètres, renvoie directement 10 () -> 10 // Exemple 2: Acceptez deux paramètres de type int et renvoyez la somme de ces deux paramètres (int x, int y) -> x + y; // Exemple 2: accepter deux paramètres de x et y, le type de ce paramètre est déduit par le JVM basé sur le contexte, et renvoie la somme des deux paramètres (x, y) - x + Acceptez une chaîne et imprimez la chaîne à contrôler, sans inverser le résultat (nom de chaîne) -> System.out.println (nom); // Exemple 4: Acceptez un type de paramètre de type inféré et imprimez la chaîne au nom de console-> System.out.println (nom); // Exemple 5: Acceptez deux paramètres de type de chaîne et les sorties séparé Sex) -> {System.out.println (nom); System.out.println (Sex)} // Exemple 6: Acceptez un paramètre x et renvoyez deux fois le paramètre x-> 2 * xOù utiliser lambda
Dans [Interface fonctionnelle] [1], nous savons que le type cible d'expression de lambda est une interface fonctionnelle - chaque lambda peut correspondre à un type donné via une interface fonctionnelle spécifique. Par conséquent, une expression de lambda peut être appliquée partout où correspond à son type de cible. L'expression de lambda doit avoir le même type de paramètre que la description de la fonction abstraite de l'interface fonctionnelle, son type de retour doit également être compatible avec le type de retour de la fonction abstrait, et les exceptions qu'il peut lancer est limitée à la plage de description de la fonction.
Ensuite, regardons un exemple d'interface fonctionnelle personnalisée:
@FunctionalInterface Interface Converter <f, t> {t convert (f de);}Tout d'abord, utilisez l'interface de la manière traditionnelle:
Converter <String, Integer> converter = new Converter <String, Integer> () {@Override public Integer convert (String from) {return Integer.ValueOf (from); }}; Résultat entier = Converter.Convert ("200"); System.out.println (résultat);De toute évidence, il n'y a aucun problème avec cela, donc la prochaine chose est le moment où Lambda arrive sur le terrain, en utilisant Lambda pour implémenter l'interface du convertisseur:
Convertisseur <string, entier> convertisseur = (param) -> Integer.valueof (param); Résultat entier = Converter.Convert ("101"); System.out.println (résultat);Grâce à l'exemple ci-dessus, je pense que vous avez une compréhension simple de l'utilisation de lambda. Ci-dessous, nous utilisons un runnable couramment utilisé pour démontrer:
Dans le passé, nous aurions pu écrire ce code:
nouveau thread (new Runnable () {@Override public void run () {System.out.println ("Hello Lambda");}}). start ();Dans certains cas, un grand nombre de classes anonymes peuvent faire apparaître le code encombré. Vous pouvez maintenant utiliser Lambda pour les simples:
nouveau thread (() -> System.out.println ("Hello Lambda")). start ();Référence de la méthode
La référence de la méthode est un moyen simplifié d'écrire des expressions de lambda. La méthode référencée est en fait une implémentation du corps de la méthode de l'expression de Lambda, et sa structure de syntaxe est:
Objectref :: Methodname
Le côté gauche peut être le nom de classe ou le nom de l'instance, le milieu est le symbole de référence de la méthode "::", et le côté droit est le nom de méthode correspondant.
Les références de méthode sont divisées en trois catégories:
1. Référence de méthode statique
Dans certains cas, nous pourrions écrire du code comme ceci:
classe publique référenceTest {public static void main (string [] args) {converter <String, Integer> converter = new Converter <String, Integer> () {@Override public Integer convert (String from) {return ReferenceTest.String2Int (from); }}; converter.convert ("120"); } @FunctionalInterface Interface Converter <f, t> {t convert (f de); } static int string2int (string from) {return Integer.ValueOf (de); }}Pour le moment, si vous utilisez des références statiques, le code sera plus concis:
Converter <String, Integer> converter = RefortEstEst :: string2int; converter.convert ("120");2. Référence de la méthode d'instance
Nous pourrions également écrire du code comme ceci:
classe publique référenceTest {public static void main (string [] args) {converter <String, Integer> converter = new Converter <String, Integer> () {@Override public Integer convert (String from) {return new Helper (). String2Int (from); }}; converter.convert ("120"); } @FunctionalInterface Interface Converter <f, t> {t convert (f de); } statique class helper {public int string2int (string from) {return Integer.valueof (from); }}}De plus, l'utilisation d'exemples de méthodes pour référence apparaîtra plus concise:
Helper helper = new Helper (); Convertisseur <string, entier> convertisseur = helper :: string2int; converter.convert ("120");3. Référence de la méthode du constructeur
Maintenant, démontrons des références aux constructeurs. Nous définissons d'abord un animal de classe parent:
classe Animal {nom de chaîne privée; Âge privé; Animal public (nom de chaîne, int age) {this.name = name; this.age = âge; } public void comportement () {}} Ensuite, nous définissons deux sous-classes d'animaux: Dog、Bird
La classe publique Bird étend Animal {Bird public (nom de la chaîne, Int Age) {Super (nom, âge); } @Override public void comportement () {System.out.println ("fly"); }} Class Dog étend Animal {Public Dog (Nom de la chaîne, int Age) {Super (nom, âge); } @Override public void comportement () {System.out.println ("run"); }}Ensuite, nous définissons l'interface d'usine:
Interface Factory <T étend Animal> {t Create (Nom de la chaîne, int Age); }Ensuite, nous utiliserons la méthode traditionnelle pour créer des objets de classes de chiens et d'oiseaux:
Factory Factory = New Factory () {@Override Public Animal Create (String Name, int Age) {return new Dog (nom, âge); }}; factory.create ("alias", 3); factory = new factory () {@Override public animal Create (String Name, int Age) {return new Bird (nom, âge); }}; Factory.Create ("Smook", 2);J'ai écrit plus de dix codes juste pour créer deux objets. Essayons maintenant d'utiliser la référence du constructeur:
Factory <Animal> dogfactory = Dog :: Nouveau; Animal Dog = Dogfactory.Create ("Alias", 4); Factory <Bird> Birdfactory = Bird :: Nouveau; Oiseau oiseau = oiseau-oiseau.Create ("Smook", 3); De cette façon, le code semble propre et soigné. Lorsque vous utilisez Dog::new pour pénétrer les objets, sélectionnez la fonction de création correspondante en signant la fonction Factory.create .
Le domaine et les restrictions d'accès de Lambda
Le domaine est la portée, et les paramètres de la liste des paramètres dans l'expression de Lambda sont valides dans le cadre de l'expression de Lambda (domaine). Dans l'expression de Lambda, les variables externes sont accessibles: variables locales, variables de classe et variables statiques, mais le degré de limites de fonctionnement est différent.
Accéder aux variables locales
Les variables locales à l'extérieur de l'expression de Lambda seront implicitement compilées par le type JVM à type final, de sorte qu'elles ne peuvent être accessibles mais non modifiées.
classe publique référentieltest {public static void main (String [] args) {int n = 3; Calculer calculer = param -> {// n = 10; Retour d'erreur de compilation n + param; }; calculer.Calculate (10); } @FunctionalInterface Interface Calcule {int calcul (int value); }}Accéder aux variables statiques et membres
À l'intérieur des expressions de Lambda, les variables statiques et membres sont lisibles et écrit.
classe publique référentiel {public int count = 1; public static int num = 2; public void test () {calcul calcul = param -> {num = 10; // modifier le nombre de variables statiques = 3; // modifier la variable de rendement de la variable n + param; }; calculer.Calculate (10); } public static void main (String [] args) {} @functionalInterface Interface calcul {int calcul (int value); }}Lambda ne peut pas accéder à la méthode par défaut de l'interface de fonction
Java8 améliore les interfaces, y compris les méthodes par défaut qui peuvent ajouter des définitions de mots clés par défaut aux interfaces. Nous devons noter ici que l'accès aux méthodes par défaut ne prend pas en charge en interne.
Pratique de lambda
Dans la section [Interface fonctionnelle] [2], nous avons mentionné que de nombreuses interfaces fonctionnelles sont intégrées dans le package java.util.function , et maintenant nous expliquerons les interfaces fonctionnelles couramment utilisées.
Interface de prédicat
Entrez un paramètre et renvoyez une valeur Boolean , qui contient de nombreuses méthodes par défaut pour le jugement logique:
@Test public void PredictTest () {Predicat <string> Predict = (S) -> S.Length ()> 0; booléen test = prédiction.test ("test"); System.out.println ("Longueur de chaîne est supérieure à 0:" + test); test = prédire.test (""); System.out.println ("Longueur de chaîne est supérieure à 0:" + test); Predicat <objet> pre = objets :: non null; Objet ob = null; test = pre.test (ob); System.out.println ("L'objet n'est pas vide:" + test); ob = nouvel objet (); test = pre.test (ob); System.out.println ("L'objet n'est pas vide:" + test); }Interface de fonction
Recevez un paramètre et renvoyez un seul résultat. La méthode par défaut ( andThen ) peut enchaîner plusieurs fonctions pour former une Funtion composite (avec entrée, sortie).
@Test public void functionTest () {function <String, Integer> toInteger = Integer :: ValueOf; // Le résultat d'exécution de ToInteger est utilisé comme entrée dans la deuxième fonction de backtoString <String, String> backtoString = toInteger.andThen (String :: ValueOf); String result = backtoString.Apply ("1234"); System.out.println (résultat); Function <Integer, entier> add = (i) -> {System.out.println ("FRIST ENTURE:" + i); retour i * 2; }; Function <Integer, Integer> zero = add.andThen ((i) -> {System.out.println ("deuxième entrée:" + i); return i * 0;}); Integer res = zero.Apply (8); System.out.println (RES); }Interface du fournisseur
Renvoie le résultat d'un type donné. Contrairement à Function , Supplier n'a pas besoin d'accepter les paramètres (fournisseur, avec sortie mais pas d'entrée)
@Test public void SupplierTest () {fournisseur <string> fournisseur = () -> "Valeur de type spécial"; String s = fournisseur.get (); System.out.println (s); }Interface de consommation
Représente les opérations qui doivent être effectuées sur un seul paramètre d'entrée. Contrairement à Function , Consumer ne renvoie pas de valeur (consommateur, entrée, pas de sortie)
@Test public void ConsumerTest () {Consumer <Integer> add5 = (p) -> {System.out.println ("Old Value:" + P); p = p + 5; System.out.println ("Nouvelle valeur:" + p); }; Add5.Accept (10); } L'utilisation des quatre interfaces ci-dessus représente les quatre types du package java.util.function . Après avoir compris ces quatre interfaces fonctionnelles, d'autres interfaces seront faciles à comprendre. Passons maintenant à un résumé simple:
Predicate est utilisé pour le jugement logique, Function est utilisée dans des endroits où il y a des entrées et des sorties, Supplier est utilisé dans des endroits où il n'y a pas d'entrée et de sorties, et Consumer est utilisé dans des endroits où il y a des entrées et pas de sorties. Vous pouvez connaître les scénarios d'utilisation basés sur la signification de son nom.
Flux
Lambda apporte des fermetures pour Java 8, ce qui est particulièrement important dans les opérations de collecte: Java 8 prend en charge les opérations fonctionnelles sur le flux d'objets de collecte. De plus, l'API Stream est également intégrée à l'API de collection, permettant des opérations par lots sur des objets de collecte.
Apprenons à connaître le flux.
Stream représente un flux de données. Il n'a pas de structure de données et ne stocke pas eux-mêmes les éléments. Ses opérations ne changeront pas le flux source, mais généreront un nouveau flux. En tant qu'interface pour les données de fonctionnement, il fournit le filtrage, le tri, la cartographie et la réglementation. Ces méthodes sont divisées en deux catégories en fonction du type de retour: Toute méthode qui renvoie le type de flux est appelée méthode intermédiaire (opération intermédiaire), et les autres sont des méthodes d'achèvement (opération complète). La méthode d'achèvement renvoie une valeur d'un certain type, tandis que la méthode intermédiaire renvoie un nouveau flux. L'appel des méthodes intermédiaires est généralement enchaîné et le processus formera un pipeline. Lorsque la méthode finale est appelée, elle entraînera la consommation de la valeur immédiatement à partir du pipeline. Ici, nous devons nous rappeler: les opérations de flux fonctionnent aussi «retardées» que possible, ce que nous appelons souvent des «opérations paresseuses», ce qui contribuera à réduire l'utilisation des ressources et à améliorer les performances. Pour toutes les opérations intermédiaires (sauf tri), elles sont exécutées en mode retard.
Stream fournit non seulement de puissantes capacités de fonctionnement des données, mais plus important encore, Stream prend en charge la série et le parallélisme. Le parallélisme permet au flux d'avoir de meilleures performances sur les processeurs multi-fond.
Le processus d'utilisation du flux a un modèle fixe:
1. Créez un flux
2. Grâce aux opérations intermédiaires, "modifie" le flux d'origine et générez un nouveau flux
3. Utilisez l'opération d'achèvement pour générer le résultat final
C'est
Créer -> Changer -> Compléter
Création de flux
Pour une collection, il peut être créé en appelant le stream() de la collection ou parallelStream() . De plus, ces deux méthodes sont également implémentées dans l'interface de collecte. Pour les tableaux, ils peuvent être créés par la méthode statique de Stream of(T … values) . De plus, Arrays fournit également une prise en charge des flux.
En plus de créer des flux basés sur des collections ou des tableaux ci-dessus, vous pouvez également créer un flux vide via Steam.empty() , ou utiliser generate() de Stream pour créer des flux infinis.
Prenons un flux série comme exemple pour illustrer plusieurs méthodes intermédiaires et complètes couramment utilisées du flux. Créez d'abord une collection de liste:
List <string> lists = new ArrayList <string> (); listS.Add ("A1"); listS.Add ("A2"); listS.Add ("B1"); listS.Add ("b2"); listS.Add ("b3"); listS.Add ("O1");Méthode intermédiaire
Filtre
Combiné avec l'interface de prédicat, filtrez tous les éléments de l'objet de streaming. Cette opération est une opération intermédiaire, ce qui signifie que vous pouvez effectuer d'autres opérations en fonction du résultat renvoyé par l'opération.
public static void streamFilterTest () {lists.stream (). filter ((s -> s.startswith ("a"))). foreach (System.out :: println); // équivalent à l'opération ci-dessus Predicat <string> predicat = (s) -> s.startswith ("a"); listS.Stream (). Filter (Predicat) .ForEach (System.out :: println); // Prédicat de filtrage continu <string> Predicat1 = (s -> s.endswith ("1")); listS.Stream (). Filter (Predicat) .Filter (Predicate1) .ForEach (System.out :: println); }Trier (trié)
Combiné avec l'interface du comparateur, cette opération renvoie une vue du flux trié, et l'ordre du flux d'origine ne changera pas. Les règles de collation sont spécifiées via le comparateur, et la valeur par défaut est de les trier dans l'ordre naturel.
public static void StreamSortEdTest () {System.out.println ("Comparateur par défaut"); listS.Stream (). tri (). filter ((s -> s.startswith ("a"))). foreach (System.out :: println); System.out.println ("Comparateur personnalisé"); listS.Stream (). Tri ((p1, p2) -> p2.compareto (p1)). Filter ((s -> s.startswith ("a"))). ForEach (System.out :: println); }Carte (carte)
Combiné avec l'interface Function , cette opération peut mapper chaque élément de l'objet Stream dans un autre élément, réalisant la conversion du type d'élément.
public static void streamMaptTest () {listS.Stream (). map (String :: ToupperCase) .Sorted ((a, b) -> b............... ce. System.out.println ("Règles de mappage personnalisé"); Function <string, string> function = (p) -> {return p + ".txt"; }; listS.Stream (). map (String :: ToupperCase) .map (fonction) .Sorted ((a, b) -> b.............. ce.............. ce................ ce }Ce qui précède introduit brièvement trois opérations couramment utilisées, ce qui simplifie considérablement le traitement de la collection. Ensuite, nous introduisons plusieurs façons de terminer:
Méthode de finition
Après le processus "transformée", le résultat doit être obtenu, c'est-à-dire que l'opération est terminée. Examinons les opérations connexes ci-dessous:
Correspondre
Utilisé pour déterminer si un predicate correspond à l'objet Stream et renvoie enfin un résultat de type Boolean , par exemple:
public static void streammatchTest () {// retourne true tant qu'un élément de l'objet Stream correspond à Boolean anyStartWitha = listS.Stream (). anyMatch ((s -> s.startswith ("a"))); System.out.println (AnyStartWitha); // Renvoie True lorsque chaque élément de l'objet Stream correspond à Boolean AllStartWitha = listS.Stream (). Allmatch ((S -> S.StStSwith ("A"))); System.out.println (AllStartWitha); }Collecter
Après la transformation, nous collectons les éléments du flux transformé, comme la sauvegarde de ces éléments en collection. Pour le moment, nous pouvons utiliser la méthode Collect fournie par Stream, par exemple:
public static void streamCoLectTest () {list <string> list = lists.stream (). filter ((p) -> p.startswith ("a")). tri (). Collect (collecors.tolist ()); System.out.println (liste); }Compter
Le nombre de types SQL est utilisé pour compter le nombre total d'éléments dans le flux, par exemple:
public static void streamCountTest () {long count = lists.stream (). filter ((s -> s.startswith ("a"))). count (); System.out.println (count); }Réduire
reduce nous permet de calculer les éléments à notre manière ou d'associer des éléments dans un flux avec un modèle, par exemple:
public static void streamReduceTest () {facultatif <string> optional = lists.stream (). trid (). Reduce ((s1, s2) -> {System.out.println (s1 + "|" + s2); return s1 + "|" + s2;}); }Les résultats de l'exécution sont les suivants:
A1 | A2A1 | A2 | B1A1 | A2 | B1 | B2A1 | A2 | B1 | B2 | B3A1 | A2 | B1 | B2 | B3 | O1
Stream parallèle vs flux série
Jusqu'à présent, nous avons introduit les opérations intermédiaires et terminées couramment utilisées. Bien sûr, tous les exemples sont basés sur le flux en série. Ensuite, nous présenterons le drame clé - Stream parallèle (flux parallèle). Le flux parallèle est mis en œuvre sur la base du cadre de décomposition parallèle de la fourche-joing, et divise l'ensemble de Big Data en plusieurs petites données et les remet à différents threads pour le traitement. De cette façon, les performances seront considérablement améliorées dans la situation du traitement multi-core. Cela est conforme au concept de conception de MapReduce: les grandes tâches deviennent plus petites et les petites tâches sont réaffectées à différentes machines pour l'exécution. Mais la petite tâche ici est remise à différents processeurs.
Créez un flux parallèle via parallelStream() . Pour vérifier si les flux parallèles peuvent vraiment améliorer les performances, nous exécutons le code de test suivant:
Créez d'abord une plus grande collection:
List <string> biglists = new ArrayList <> (); for (int i = 0; i <10000000; i ++) {uUid uuid = uuid.randomuuid (); biglists.add (uuid.toString ()); }Testez le temps pour trier sous des flux de série:
Private Static void notParallelStreamSortEdTest (list <string> biglists) {long startTime = System.NanoTime (); Long Count = Biglists.Stream (). TROED (). COUNT (); Long EndTime = System.NanoTime (); long millis = timeunit.nanoseconds.tomiillis (endtime - starttime); System.out.println (System.out.printf ("Tri série:% D MS", Millis)); }Testez le temps pour trier dans des flux parallèles:
Private Static void ParallelStreamSortEdTest (list <string> biglists) {long startTime = System.NanoTime (); Long Count = biglists.ParallelStream (). tri (). Count (); Long EndTime = System.NanoTime (); long millis = timeunit.nanoseconds.tomiillis (endtime - starttime); System.out.println (System.out.printf ("Parallelsorting:% d ms", millis)); }Les résultats sont les suivants:
Sort en série: 13336 ms
Sort parallèle: 6755 ms
Après avoir vu cela, nous avons constaté que la performance s'est améliorée d'environ 50%. Pensez-vous également que vous pourrez utiliser parallel Stream à l'avenir? En fait, ce n'est pas le cas. Si vous êtes encore un processeur monomoRE maintenant et que le volume de données n'est pas important, le streaming en série est toujours un si bon choix. Vous constaterez également que dans certains cas, les performances des flux de série sont meilleures. Quant à l'utilisation spécifique, vous devez d'abord le tester, puis décider en fonction du scénario réel.
Opération paresseuse
Ci-dessus, nous avons parlé du flux en cours d'exécution le plus tard possible, et nous l'expliquons ici en créant un flux infini:
Tout d'abord, utilisez la méthode generate du flux pour créer une séquence de nombres naturels, puis transformez le flux via map :
// classe de séquence incrémentielle Natureseq implémente le fournisseur <long> {Long Value = 0; @Override public long get () {value ++; valeur de retour; }} public void StreamCreateTest () {Stream <long> stream = stream.generate (new NaturSeQ ()); System.out.println ("Nombre d'éléments:" + stream.map ((param) -> {return Param;}). Limit (1000) .Count ()); }Le résultat de l'exécution est:
Nombre d'éléments: 1000
Nous avons constaté qu'au début, toutes les opérations intermédiaires (telles que filter,map , etc., mais sorted ne peut pas être effectuée) sont OK. Autrement dit, le processus d'exécution des opérations intermédiaires sur le flux et de survivre à un nouveau flux ne prend pas effet immédiatement (ou l'opération map dans cet exemple s'exécutera pour toujours et sera bloquée), et le flux commence à calculer lorsque la méthode d'achèvement est rencontrée. Grâce à limit() , convertissez ce flux infini en flux fini.
Résumer
Ce qui précède est tout le contenu de l'introduction rapide à Java Lambda. Après avoir lu cet article, avez-vous une compréhension plus profonde de Java Lambda? J'espère que cet article sera utile à tout le monde d'apprendre Java Lambda.