Meine Ansichten zu Lambda-Ausdrücken in Java sind ziemlich kompliziert:
Einer meiner Gedanken ist dieser: Lambda-Ausdrücke verringern das Leseerlebnis von Java-Programmen. Java-Programme zeichneten sich noch nie durch ihre Ausdruckskraft aus. Im Gegenteil, einer der Faktoren, die Java so beliebt machen, ist seine Sicherheit und Konservativität – selbst Anfänger können robusten und leicht zu wartenden Code schreiben, solange sie aufmerksam sind. Lambda-Ausdrücke stellen relativ höhere Anforderungen an Entwickler, was die Wartung erschwert.
Eine andere Sache, über die ich nachdenke, ist Folgendes: Als Programmierer ist es notwendig, die neuen Funktionen der Sprache zu lernen und zu akzeptieren. Wenn man seine Ausdrucksvorteile nur aufgrund der schlechten Leseerfahrung aufgibt, wird es manchen Menschen schwerfallen, selbst den trinokularen Ausdruck zu verstehen. Auch die Sprache entwickelt sich weiter und wer nicht mithalten kann, wird freiwillig zurückgelassen.
Ich möchte nicht zurückgelassen werden. Wenn ich jedoch eine Wahl treffen muss, ist meine Entscheidung immer noch relativ konservativ: Es besteht keine Notwendigkeit, Lambda in der Java-Sprache zu verwenden. Dies führt dazu, dass sich viele Menschen im aktuellen Java-Kreis nicht daran gewöhnen und die Arbeitskosten steigen . Wenn es Ihnen sehr gefällt, können Sie die Verwendung von Scala in Betracht ziehen.
Wie auch immer, ich habe angefangen, Lambda zu beherrschen. Schließlich verwenden einige der bei der Arbeit gepflegten Codes Lambda (glauben Sie mir, ich werde es nach und nach entfernen). Die Tutorials, die ich studiert habe, sind verwandte Tutorials auf der offiziellen Website von Oracla-Java.
——————————
Angenommen, Sie erstellen gerade eine Social-Networking-Anwendung. Eine Funktion besteht darin, dass Administratoren bestimmte Aktionen für Mitglieder durchführen können, die bestimmte Kriterien erfüllen, beispielsweise das Versenden von Nachrichten. In der folgenden Tabelle wird dieser Anwendungsfall detailliert beschrieben:
Verwenden Sie die folgende Personenklasse, um Mitgliedsinformationen im sozialen Netzwerk darzustellen:
public class Person { public enum Sex { MALE, FEMALE } String name; LocalDate birthday; Sex gender; String emailAddress; public int getAge() { // ... } public void printPerson() { // ... } }
Gehen Sie davon aus, dass alle Mitglieder in einer List
In diesem Abschnitt beginnen wir mit einer sehr einfachen Methode, versuchen dann, sie mithilfe lokaler und anonymer Klassen zu implementieren, und am Ende werden wir nach und nach die Leistungsfähigkeit und Effizienz von Lambda-Ausdrücken erleben. Den vollständigen Code finden Sie hier.
Option 1: Erstellen Sie nacheinander Methoden, um Mitglieder zu finden, die die angegebenen Kriterien erfüllen
Dies ist die einfachste und gröbste Lösung, um den oben genannten Fall umzusetzen: Erstellen Sie einfach mehrere Methoden. Jede Methode überprüft ein Kriterium (z. B. Alter oder Geschlecht). Der folgende Codeteil prüft, ob das Alter größer als ein angegebener Wert ist:
public static void printPersonsOlderThan(List<person> roster, int age) { for (Person p : roster) { if (p.getAge() >= age) { p.printPerson(); } } }
Dies ist eine sehr fragile Lösung, und es ist sehr wahrscheinlich, dass die Anwendung nicht ordnungsgemäß ausgeführt wird zu einem kleinen Update. Wenn wir der Person-Klasse neue Mitgliedsvariablen hinzufügen oder den Algorithmus zur Altersmessung im Standard ändern, müssen wir viel Code neu schreiben, um ihn an diese Änderung anzupassen. Darüber hinaus sind die Einschränkungen hier zu starr. Was sollen wir beispielsweise tun, wenn wir Mitglieder drucken möchten, deren Alter unter einem bestimmten angegebenen Wert liegt? Eine neue Methode printPersonsYoungerThan hinzufügen? Das ist offensichtlich eine dumme Methode.
Option 2: Erstellen Sie eine allgemeinere Methode
Die folgende Methode ist anpassungsfähiger als printPersonsOlderThan; diese Methode druckt Mitgliedsinformationen innerhalb der angegebenen Altersgruppe:
public static void printPersonsWithinAgeRange( List<person> roster, int low, int high) { for (Person p : roster) { if (low <= p.getAge() && p.getAge() < high) { p.printPerson(); } } }
Jetzt habe ich eine neue Idee: Was wäre, wenn wir Mitgliederinformationen eines bestimmten Geschlechts oder Mitgliederinformationen, die sowohl dem angegebenen Geschlecht entsprechen als auch innerhalb der angegebenen Altersgruppe liegen, ausdrucken möchten? Was wäre, wenn wir die Person-Klasse optimieren und Attribute wie Freundlichkeit und Standort hinzufügen würden? Obwohl das Schreiben dieser Methode allgemeiner ist als das Schreiben von printPersonsYoungerThan, führt das Schreiben einer Methode für jede mögliche Abfrage auch zu brüchigem Code. Es ist besser, den Standardprüfcode in eine neue Klasse aufzuteilen.
Option 3: Standardprüfung in einer lokalen Klasse implementieren
Die folgende Methode druckt Mitgliedsinformationen, die den Suchkriterien entsprechen:
public static void printPersons(List<person> roster, CheckPerson tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
Im Programm wird ein CheckPerso-Objekttester verwendet, um jede Instanz im List-Parameter-Roter zu überprüfen. Wenn tester.test() true zurückgibt, wird die Methode printPerson() ausgeführt. Um die Abrufkriterien festzulegen, müssen Sie die CheckPerson-Schnittstelle implementieren.
Die folgende Klasse implementiert CheckPerson und stellt eine spezifische Implementierung der Testmethode bereit. Die Testmethode in dieser Klasse filtert die Informationen von Mitgliedern, die die Voraussetzungen für den Militärdienst in den Vereinigten Staaten erfüllen: Das heißt, das Geschlecht ist männlich und das Alter liegt zwischen 18 und 25 Jahren.
class CheckPersonEligibleForSelectiveService implements CheckPerson { public boolean test(Person p) { return p.gender == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } }
Um diese Klasse zu verwenden, müssen Sie eine Instanz erstellen und die printPersons-Methode auslösen:
printPersons( roster, new CheckPersonEligibleForSelectiveService());
Der Code jetzt sieht anders aus. So fragil – wir müssen den Code aufgrund von Änderungen in der Person-Klassenstruktur nicht neu schreiben. Aber es gibt noch zusätzlichen Code: eine neu definierte Schnittstelle und eine innere Klasse, die für jedes Suchkriterium in der Anwendung definiert wird.
Da CheckPersonEligibleForSelectiveService eine Schnittstelle implementiert, können anonyme Klassen verwendet werden, anstatt für jeden Standard eine innere Klasse zu definieren.
Option 4: Verwenden Sie anonyme Klassen, um Standardprüfungen zu implementieren
Ein Parameter in der unten aufgerufenen printPersons-Methode ist eine anonyme Klasse. Die Funktion dieser anonymen Klasse ist dieselbe wie die der CheckPersonEligibleForSelectiveService-Klasse in Lösung 3: Beide filtern Mitglieder, deren Geschlecht männlich ist und die zwischen 18 und 25 Jahre alt sind.
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
interface Predicate<t> { boolean test(T t); }
Predicate
interface Predicate<person> { boolean test(Person t); }
在这个参数化类中有一个与CheckPerson.boolean test(Person p)方法的参数和返回值都一致的方法。因此就可以同如下方法所演示的一样使用Predicate
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表达式
再来看一下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
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
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));
分析下如上代码的执行过程,整理如下表:
表中的filter、map和forEach操作都是聚集操作。聚集操作处理的元素来自Stream,而非是直接从集合中获取(就是因为这示例程序中调用的第一个方法是stream())。Stream是一个数据序列。和集合不同,Stream并没有用特定的结构存储数据。相反的,Stream从一个特定的源获取数据,比如从集合获取数据,通过一个pipeline。pipeline是一个Stream操作序列,在这个例子中就是filter-map-forEach。此外,聚集操作通常采用lambda表达式作为参数,这也给了我们许多自定义的空间。
更多Einfacher Anwendungsfall des Lambda-Ausdrucks in Java相关文章请关注PHP中文网!