我對java中lambda表達式的看法是相當糾結的:
一個我這麼想:lambda表達式降低了java程式的閱讀體驗。 java程式一直不以表現力出眾,正相反地使Java流行的一個因素正是它的安全性和保守性——即使是初學者只要注意些也能寫出健壯且容易維護的程式碼來。 lambda表達式對開發人員的要求相對來說高了一層,因此也增加了一些維護難度。
另一個我這麼想:作為一個碼代碼的,有必要學習並接受語言的新特性。如果只是因為它的閱讀體驗差就放棄它在表現力方面的長處,那麼即使是三目表達式也有人覺得理解起來困難呢。語言也是在發展的,跟不上的就自願被丟下好了。
我不願意被丟下。不過非讓我做出選擇的話,我的決定還是比較保守的:沒必要一定在java語言中使用lambda——它讓目前Java圈子中的很多人不習慣,會造成人力成本的上升。如果非常喜歡的話,可以考慮使用scala。
不管怎樣,我還是開始試著掌握Lambda了,畢竟工作中維護的部分程式碼使用了Lambda(相信我,我會逐步把它去掉的)。學習的教學是在Oracla – Java官網的相關教學。
——————————
假設目前正在創建一個社群網路應用程式。其中的一個特性是管理員可以對符合指定條件的會員執行某些操作,例如發送訊息。下面的表格詳細描述了這個用例:
使用下面的Person類來表示社交網路中的會員資訊:
public class Person { public enum Sex { MALE, FEMALE } String name; LocalDate birthday; Sex gender; String emailAddress; public int getAge() { // ... } public void printPerson() { // ... } }
假設所有的會員都保存在一個List
這一節我們從一個非常簡單的方法開始,然後嘗試使用局部類別和匿名類別進行實現,到最後會逐步深入體驗Lambda表達式的強大和高效。可以在這裡找到完整的程式碼。
方案一:一個個地創建查找符合指定標準的會員的方法
這是實現前面提到的案例最簡單粗糙的方案了:就是創建幾個方法、每個方法校驗一項標準(如年齡或是性別)。下面的一段程式碼校驗了年齡大於一個指定值的情況:
public static void printPersonsOlderThan(List<person> roster, int age) { for (Person p : roster) { if (p.getAge() >= age) { p.printPerson(); } } }
這是一個很脆弱的方案,極有可能因為一點更新就導致應用程式無法運作。假如我們為Person類別添加了新的成員變數或更改了標準中衡量年齡的演算法,就需要重寫大量的程式碼來適應這種變化。再者,這裡的限制也太過僵化了,比方說我們要列印年齡小於某個指定值的成員又該怎麼做呢?再加入一個新方法printPersonsYoungerThan?這顯然是一個笨方法。
方案二:創建更通用的方法
下面的方法較之printPersonsOlderThan適應性更好一些;這個方法打印了在指定年齡段內的會員資訊:
public static void printPersonsWithinAgeRange( List<person> roster, int low, int high) { for (Person p : roster) { if (low <= p.getAge() && p.getAge() < high) { p.printPerson(); } } }
此時又有新的想法了:如果我們要列印指定性別的會員訊息,或同時符合指定性別又在指定年齡層內的會員資訊該怎麼辦呢?如果我們調整了Person類,添加了諸如友好度和地理位置這樣的屬性又該怎麼辦。儘管這樣寫方法要比printPersonsYoungerThan通用性更強一些,但是如果為每一種可能的查詢都寫一個方法也會導致程式碼的脆弱。倒不如把標準檢查這一塊程式碼給獨立到一個新的類別。
方案三:在一個局部類別中實現標準檢查
下面的方法列印了符合檢索標準的會員資訊:
public static void printPersons(List<person> roster, CheckPerson tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
在程式中使用了一個CheckPersotester對List參數物件中的每個物件參數實例進行校驗。如果tester.test()回傳true,就會執行printPerson()方法。為了設定檢索標準,需要實作CheckPerson介面。
下面的這個類別實作了CheckPerson並且提供了test方法的具體實作。這個類別的test方法過濾了滿足在美國服兵役條件的會員資訊:即性別為男、且年齡在18~25歲之間。
class CheckPersonEligibleForSelectiveService implements CheckPerson { public boolean test(Person p) { return p.gender == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } }
要使用這個類,需要創建一個實例並觸發printPersons方法:
printPersons( roster, new CheckPersonEligibleForSelectiveService());
現在的程式碼看起來不那麼脆弱了——我們不需要因為Person類結構的變化而重寫程式碼。不過這裡仍有額外的程式碼:一個新定義的介面、為應用程式中每個搜尋標準定義了一個內部類別。
因為CheckPersonEligibleForSelectiveService實作了一個接口,所以可以使用匿名類,而不需要再為每個標準分別定義一個內部類別。
方案四:使用匿名類別實作標準檢查
下面呼叫的printPersons方法中的一個參數是匿名類別。這個匿名類別的作用和方案三中的CheckPersonEligibleForSelectiveService類別一樣:都是過濾性別為男且年齡在18和25歲之間的會員。
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表达式作为参数,这也给了我们许多自定义的空间。
更多java中lambda表達式簡單用例相关文章请关注PHP中文网!