Maison  >  Article  >  Java  >  Cas d'utilisation simple de l'expression lambda en Java

Cas d'utilisation simple de l'expression lambda en Java

高洛峰
高洛峰original
2017-01-23 15:08:301526parcourir

Mon point de vue sur les expressions lambda en Java est assez compliqué :

Une de mes pensées est la suivante : les expressions lambda réduisent l'expérience de lecture des programmes Java. Les programmes Java n'ont jamais été remarquables par leur expressivité. Au contraire, l'un des facteurs qui rendent Java populaire est sa sécurité et son conservatisme : même les débutants peuvent écrire du code robuste et facile à maintenir à condition d'y prêter attention. Les expressions Lambda ont des exigences relativement plus élevées pour les développeurs, ce qui rend la maintenance plus difficile.

Une autre chose à laquelle je pense est la suivante : en tant que codeur, il est nécessaire d'apprendre et d'accepter les nouvelles fonctionnalités du langage. Si vous renoncez à ses avantages expressifs simplement en raison de sa mauvaise expérience de lecture, certaines personnes auront du mal à comprendre même l'expression trinoculaire. La langue se développe également, et ceux qui ne peuvent pas suivre seront volontairement laissés pour compte.

Je ne veux pas être laissé pour compte. Mais si je dois faire un choix, ma décision reste relativement conservatrice : il n'est pas nécessaire d'utiliser lambda dans le langage Java - cela rend de nombreuses personnes dans le cercle Java actuel non habituées et entraînera une augmentation des coûts de main-d'œuvre. . Si vous l'aimez beaucoup, vous pouvez envisager d'utiliser Scala.

Quoi qu'il en soit, j'ai commencé à essayer de maîtriser Lambda. Après tout, certains des codes maintenus au travail utilisent Lambda (croyez-moi, je vais le supprimer progressivement). Les tutoriels que j'ai étudiés sont des tutoriels connexes sur le site officiel d'Oracla-Java.

——————————

Supposons que vous soyez en train de créer une application de réseau social. Une fonctionnalité est que les administrateurs peuvent effectuer certaines actions sur les membres qui répondent à des critères spécifiés, comme l'envoi de messages. Le tableau suivant décrit ce cas d'utilisation en détail :

Cas dutilisation simple de lexpression lambda en Java

Utilisez la classe Person suivante pour représenter les informations sur les membres dans le réseau social :

public class Person {
  
  public enum Sex {
    MALE, FEMALE
  }
  
  String name;
  LocalDate birthday;
  Sex gender;
  String emailAddress;
  
  public int getAge() {
    // ...
  }
  
  public void printPerson() {
    // ...
  }
}

Supposons que tous les membres sont stockés dans une instance List

Dans cette section, nous commençons par une méthode très simple, puis essayons de l'implémenter en utilisant des classes locales et des classes anonymes, et à la fin nous expérimenterons progressivement la puissance et l'efficacité des expressions Lambda. Le code complet peut être trouvé ici.

Option 1 : Créer les méthodes une par une pour trouver les membres qui répondent aux critères spécifiés

C'est la solution la plus simple et la plus grossière pour mettre en œuvre le cas évoqué ci-dessus : il suffit de créer plusieurs méthodes, chaque méthode vérifie un critère (comme l'âge ou le sexe). Le morceau de code suivant vérifie si l'âge est supérieur à une valeur spécifiée :

public static void printPersonsOlderThan(List<person> roster, int age) {
  for (Person p : roster) {
    if (p.getAge() >= age) {
      p.printPerson();
    }
  }
}

Il s'agit d'une solution très fragile, et il est très probable que l'application ne fonctionnera pas en raison à une petite mise à jour. Si nous ajoutons de nouvelles variables membres à la classe Person ou modifions l'algorithme de mesure de l'âge dans la norme, nous devrons réécrire beaucoup de code pour nous adapter à ce changement. De plus, les restrictions ici sont trop rigides. Par exemple, que devons-nous faire si nous voulons imprimer des membres dont l'âge est inférieur à une certaine valeur spécifiée ? Ajouter une nouvelle méthode printPersonsYoungerThan ? C'est évidemment une méthode stupide.

Option 2 : Créer une méthode plus générale

La méthode suivante est plus adaptable que printPersonsOlderThan ; cette méthode imprime les informations sur les membres dans le groupe d'âge spécifié :

public static void printPersonsWithinAgeRange(
    List<person> roster, int low, int high) {
  for (Person p : roster) {
    if (low <= p.getAge() && p.getAge() < high) {
      p.printPerson();
    }
  }
}

Maintenant, j'ai une nouvelle idée : que se passe-t-il si nous voulons imprimer les informations sur les membres d'un sexe spécifié, ou les informations sur les membres qui correspondent à la fois au sexe spécifié et à la tranche d'âge spécifiée ? Et si nous modifiions la classe Person et ajoutions des attributs tels que la convivialité et l'emplacement. Bien que l'écriture de cette méthode soit plus générale que printPersonsYoungerThan, l'écriture d'une méthode pour chaque requête possible conduira également à un code fragile. Il est préférable de séparer le code de vérification standard dans une nouvelle classe.

Option 3 : Implémenter une vérification standard dans une classe locale

La méthode suivante imprime les informations sur les membres qui répondent aux critères de recherche :

public static void printPersons(List<person> roster, CheckPerson tester) {
  for (Person p : roster) {
    if (tester.test(p)) {
      p.printPerson();
    }
  }
}

Un testeur d'objet CheckPerso est utilisé dans le programme pour vérifier chaque instance dans le roter de paramètres List. Si tester.test() renvoie true, la méthode printPerson() sera exécutée. Afin de définir les critères de récupération, vous devez implémenter l'interface CheckPerson.

La classe suivante implémente CheckPerson et fournit une implémentation spécifique de la méthode de test. La méthode de test de cette classe filtre les informations des membres qui satisfont aux exigences du service militaire aux États-Unis : c'est-à-dire que le sexe est masculin et l'âge est compris entre 18 et 25 ans.

class CheckPersonEligibleForSelectiveService implements CheckPerson {
  public boolean test(Person p) {
    return p.gender == Person.Sex.MALE &&
        p.getAge() >= 18 &&
        p.getAge() <= 25;
  }
}

Pour utiliser cette classe, vous devez créer une instance et déclencher la méthode printPersons :

printPersons(
    roster, new CheckPersonEligibleForSelectiveService());

Le code maintenant semble différent Tellement fragile - nous n'avons pas besoin de réécrire le code en raison des changements dans la structure de la classe Person. Mais il reste encore du code supplémentaire : une interface nouvellement définie et une classe interne définie pour chaque critère de recherche dans l'application.

Étant donné que CheckPersonEligibleForSelectiveService implémente une interface, des classes anonymes peuvent être utilisées au lieu de définir une classe interne pour chaque standard.

Option 4 : Utiliser des classes anonymes pour implémenter des vérifications standard

Un paramètre de la méthode printPersons appelée ci-dessous est une classe anonyme. La fonction de cette classe anonyme est la même que celle de la classe CheckPersonEligibleForSelectiveService dans la solution 3 : les deux filtrent les membres dont le sexe est masculin et qui ont entre 18 et 25 ans.

printPersons(
    roster,
    new CheckPerson() {
      public boolean test(Person p) {
        return p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25;
      }
    }
);

这个方案减少了编码量,因为不再需要为每个要执行的检索方案创建新类。不过这样做仍让人有些不舒服:尽管CheckPerson接口只有一个方法,实现的匿名类仍是有些冗长笨重。此时可以使用Lambda表达式替换匿名类,下面会说下如何使用Lambda表达式替换匿名类。

方案五:使用Lambda表达式实现标准检查

CheckPerson接口是一个函数式接口。所谓的函数式接口就是指任何只包含一个抽象方法的接口。(一个函数式接口中也可以有多个default方法或静态方法)。既然函数式接口中只有一个抽象方法,那么在实现这个函数式接口的方法的时候可以省略掉方法的方法名。为了实现这个想法,可以使用Lambda表达式替换匿名类表达式。在下面重写的printPersons方法中,相关的代码做了高亮处理:

printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

在这里还可以使用一个标准的函数接口来替换CheckPerson接口,从而进一步简化代码。

方案六:在Lambda表达式中使用标准函数式接口

再来看一下CheckPerson接口:

interface CheckPerson {
  boolean test(Person p);
}

这是一个非常简单的接口。因为只有一个抽象方法,所以它也是一个函数式接口。这个抽象方法只接受一个参数并返回一个boolean值。这个抽象接口太过简单了,以至于我们会考虑是否有必要在应用中定义一个这样的接口。此时可以考虑使用JDK定义的标准函数式接口,可以在java.util.function包下找到这些接口。

在这个例子中,我们就可以使用Predicate接口来替换CheckPerson。在这个接口中有一个boolean test(T t)方法:

interface Predicate<t> {
  boolean test(T t);
}

Predicate接口是一个泛型接口。泛型类(或者是泛型接口)使用一对尖括号()指定了一个或多个类型参数。在这个接口中只有一个类型参数。在使用具体类声明或实例化一个泛型类时,就会获得一个参数化类。比如说参数化类Predicate就是这样的:

interface Predicate<person> {
  boolean test(Person t);
}

在这个参数化类中有一个与CheckPerson.boolean test(Person p)方法的参数和返回值都一致的方法。因此就可以同如下方法所演示的一样使用Predicate接口来替换CheckPerson接口:

public static void printPersonsWithPredicate(
    List<person> roster, Predicate<person> tester) {
  for (Person p : roster) {
    if (tester.test(p)) {
      p.printPerson();
    }
  }
}

然后使用下面的代码就可以像方案三中一样筛选适龄服兵役的会员了:

printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

有没有注意到,这里使用Predicate作为参数类型时并没有显式指定参数类型。这里并不是唯一适用lambda表达式的地方,下面的方案会介绍更多lambda表达式的用法。

方案七:在整个应用中使用lambda表达式

再来看一下printPersonsWithPredicate 方法,考虑是否可以在这里使用lambda表达式:

public static void printPersonsWithPredicate(
    List<person> roster, Predicate<person> tester) {
  for (Person p : roster) {
    if (tester.test(p)) {
      p.printPerson();
    }
  }
}

在这个方法中使用Predicate实例tester检查了roster中的每个Person实例。如果Person实例符合tester中定义的检查标准,将会触发Person实例的printPerson方法。

除了触发printPerson方法,满足tester标准的Person实例还可以执行其他的方法。可以考虑使用lambda表达式指定要执行的方法(私以为这个特性很好,解决了java中方法不能作为对象传递的问题)。现在需要一个类似printPerson方法的lambda表达式——一个只需要一个参数且返回为void的lambda表达式。记住一点:要使用lambda表达式,需要先实现一个函数式接口。在这个例子中需要一个函数式接口,其中只包含一个抽象方法,这个抽象方法有个类型为Person的参数,且返回为void。可以看一下JDK提供的标准函数式接口Consumer,它有一个抽象方法void accept(T t)正好满足这个要求。在下面的代码中使用一个Consumer的实例调用accept方法替换了p.printPerson():

public static void processPersons(
    List<person> roster,
    Predicate<person> tester,
    Consumer<person> block) {
  for (Person p : roster) {
    if (tester.test(p)) {
      block.accept(p);
    }
  }
}

对应这里,可以使用如下代码筛选适龄服兵役的会员:

processPersons(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.printPerson()
);

如果我们想做的事情不只是打印会员信息,而是更多的事情,比如验证会员身份、获取会员联系方式等等。此时,我们需要一个有返回值方法的函数式接口。JDK的标准函数式接口Function就有一个这样的方法R apply(T t)。下面的方法从参数mapper中获取数据,并在这些数据上执行参数block指定的行为:

public static void processPersonsWithFunction(
    List<person> roster,
    Predicate<person> tester,
    Function<person  , string> mapper,
    Consumer<string> block) {
  for (Person p : roster) {
    if (tester.test(p)) {
      String data = mapper.apply(p);
      block.accept(data);
    }
  }
}

下面的代码获取了roster中适龄服兵役的所有会员的邮箱信息并打印出来:

processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

方案八:多使用泛型

再来回顾一下processPersonsWithFunction方法。下面是这个方法的泛型版本,新方法在参数类型上要求更为宽容:

public static <x  , Y> void processElements(
    Iterable<x> source,
    Predicate<x> tester,
    Function<x  , Y> mapper,
    Consumer<y> block) {
  for (X p : source) {
    if (tester.test(p)) {
      Y data = mapper.apply(p);
      block.accept(data);
    }
  }
}

 

要打印适龄服兵役的会员信息可以像下面这样调用processElements方法:

processElements(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

在方法的调用过程中执行了如下行为:

从一个集合中获取对象信息,在这个例子里是从集合实例roster中获取Person对象信息。
过滤能够匹配Predicate实例tester的对象。在这个例子里,Predicate对象是一个lambda表达式,它指定了过滤适龄服兵役的条件。

将过滤后的对象交给一个Function对象mapper处理,mapper会为这个对象匹配一个值。在这个例子中Function对象mapper是一个lambda表达式,它返回了每个会员的邮箱地址。

由Consumer对象block为mapper匹配的值指定一个行为。在这个例子里,Consumer对象是一个lambda表达式,它的作用是打印一个字符串,也就是Function实例mapper返回的会员邮件地址。

方案九:使用将lambda表达式作为参数的聚集操作

下面的代码中使用了聚集操作来打印roster集合中适龄服兵役会员的邮件地址:

roster.stream()
    .filter(
        p -> p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25)
    .map(p -> p.getEmailAddress())
    .forEach(email -> System.out.println(email));

分析下如上代码的执行过程,整理如下表:

Cas dutilisation simple de lexpression lambda en Java

表中的filter、map和forEach操作都是聚集操作。聚集操作处理的元素来自Stream,而非是直接从集合中获取(就是因为这示例程序中调用的第一个方法是stream())。Stream是一个数据序列。和集合不同,Stream并没有用特定的结构存储数据。相反的,Stream从一个特定的源获取数据,比如从集合获取数据,通过一个pipeline。pipeline是一个Stream操作序列,在这个例子中就是filter-map-forEach。此外,聚集操作通常采用lambda表达式作为参数,这也给了我们许多自定义的空间。

更多Cas dutilisation simple de lexpression lambda en Java相关文章请关注PHP中文网!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn