系统学习Lambda表达式
1. 語法
首先我們要知道如何寫Lambda表達式,或者說怎么樣才能寫出有效的Lambda表達式,這就需要了解其語法。
Lambda表達式由三部分組成:
參數列表
箭頭
主體
有兩種風格,分別是:
表達式-風格
(parameters) -> expression
塊-風格
(parameters) -> { statements; }
依據上面的風格介紹,來試著判斷下面給出的示例是否有效:
() -> {}() -> "Apple"() -> { return "Apple"; }(Integer i) -> return "Apple" + i(String s) -> { "Apple"; }解析:(1)是塊風格,沒有語句;(2)是表達式風格,一個字符串表達式;(3)是塊風格,有花括號和返回語句;(4)非有效,寫了返回語句,但缺少花括號,補上花括號和分號,為塊風格,而去掉return則為表達式風格;(5)非有效,"Apple"是一個字符串表達式,不是一個語句,加上return,或者去掉分號和花括號。
?
2. 函數式接口
Lambda表達式寫好了,我們要知道哪里能用Lambda表達式。已知Lambda表達式可看作是匿名內部類的實現,那對于匿名內部類來說最重要的是類所實現的接口,而Lambda表達式是否可用于所有接口?答案“不是的”,Lambda表達式對接口有一定的要求,必須是函數式接口。
所謂的函數式接口指的是只定義一個抽象方法的接口。
例如:
public?interface?Comparator<T>?{int?compare(T?o1,?T?o2); }public?interface?Runnable?{void?run(); }public?interface?Callable<V>?{V?call()?throws?Exception; }可見上面三個接口都只有一個抽象方法,但是三個方法的簽名都不一樣,這要求Lambda表達式與實現接口的方法簽名要一致。下面用函數描述符來表示上述三個方法的簽名,箭頭前面是方法的入參類型,后面是返回類型。
compare:(T, T) -> int,兩個泛型T類型的入參,返回int類型
Lambda表達式:(Apple a1, Apple a2) -> a1.getWeight - a2.getWeight
run:() -> void,無入參,無返回值
Lambda表達式:() -> { System.out.println("Hi"); }
call:() -> V,無入參,返回一個泛型V類型的對象
Lambda表達式:() -> new Apple()
看call方法的示例,你是否會疑惑,new Apple()是一個語句,為什么沒有花括號和分號,是不是非有效的。你需要記住這是合法的,這是一個特殊的規定,不需要用括號環繞返回值為void的單行方法調用。
?
3. 常用的函數式接口
下面介紹在Java中內置的常用Lambda表達式:
3.1 Predicate
public?interface?Predicate<T>?{boolean?test(T?t); }test:T -> boolean,接收一個泛型T對象,返回一個boolean。
適用場景:表示一個涉及類型T的布爾表達式。
//?判斷空白字符串 Predicate<String>?blankStrPredicate?=?s?->?s?!=?null?&&?s.trim().length()?==?0; blankStrPredicate.test("??");?//?true //?判斷蘋果重量是否大于150 Predicate<Apple>?heavyApplePredicate?=?a?->?a.getWeight()?>?150; heavyApplePredicate.test(new?Apple(100));?//?false注意,參數部分缺少了參數類型,是因為可根據上下文推斷出Lambda表達式的參數類型,所以可以省略不寫。比如這里因為將Lambda表達式賦值給一個Predicate類型的變量,又因為函數描述符為(T) -> boolean,則可推斷出參數T的實際類型為String。而且當只有一個參數時,可以將括號也省略。
3.2 Consumer
public?interface?Consumer<T>?{void?accept(T?t); }accept:T -> void,接收一個泛型T對象,無返回值(void)。
適用場景:訪問類型T的對象,對其執行某些操作。
//?打印蘋果重量 Consumer<Apple>?appleWeighter?=?a?->?System.out.println("The?apple?weights?"?+?a.getWeight()?+?"?grams"); appleWeighter.accept(new?Apple(200));? //?The?apple?weights?200?grams3.3 Supplier
public?interface?Supplier<T>?{T?get(); }get:() -> T,無入參,返回一個泛型T對象。
適用場景:定義類型T的對象的生產規則。
Consumer<Apple>?appleWeighter?=(a)?->?System.out.println("The?apple?weights?"?+?a.getWeight()?+?"?grams"); //?生產200克的蘋果 Supplier<Apple>?heavyAppleSupplier?=?()?->?new?Apple(200); appleWeighter.accept(heavyAppleSupplier.get());3.4 Function
public?interface?Function<T,?R>?{R?apply(T?t); }apply:T -> R,接受一個泛型T對象,返回一個泛型R對象
適用場景:將輸入對象轉換輸出。
double?unitPrice?=?0.01; //?計算蘋果價格 Function<Apple,?Double>?priceAppleFunction?=?a?->?a.getWeight()?*?unitPrice; priceAppleFunction.apply(new?Apple(100));?//?1這里做個補充,上面這段代碼特別的地方在于使用到了外部的局部變量。Lambda表達式使用外部變量有什么要求?對于Lambda表達式所在的主體(類)的實例變量和靜態變量,可以無限制使用,但局部變量必須顯示聲明為final或實際上是final的。
聲明為final好理解,什么是實際上是final的,意思就是不能被代碼進行修改,比如這里的unitPrice雖然沒有聲明為final,但后續的代碼并沒有修改該變量,所以實際上也是final的。感興趣的讀者可以自己試下,對unitPrice進行修改,看下會發生什么。
3.5 Comparator
public?interface?Comparator<T>?{int?compare(T?o1,?T?o2); }compare:(T, T) -> int,兩個泛型T類型的入參,返回int類型
適用場景:比較兩個對象
Comparator<Apple>?weightComparator?=?(a1,?a2)?->?a1.getWeight()?-?a2.getWeight(); weightComparator.compare(new?Apple(100),?new?Apple(150));?//?-1?
4. 方法引用
Java還提供了一種更簡潔的寫法,先上示例:
Function<Apple,?Integer>?weightor?=?a?->?a.getWeight();可改寫為:
Function<Apple,?Integer>?weightor?=?Apple::getWeight;這種寫法被稱作方法引用,僅當在Lambda表達式中直接調用了一個方法時可以使用。其寫法為目標引用::方法名稱。
根據目標引用可分為三類:
(1)指向靜態方法的方法引用
目標引用為類,調用其靜態方法,例如:
Function<String,?Integer>?fun?=?s?->?Integer.parseInt(s);調用了?Integer?的靜態方法?parseInt,可寫為:
Function<String,?Integer>?fun?=?Integer::parseInt;(2)指向任意類型實例方法的方法引用
目標引用為實例對象,調用其實例方法,例如:
Function<String,?Integer>?fun?=?s?->?s.length();調用了?String?類型實例?s?的?length?方法,可寫為:
Function<String,?Integer>?fun?=?String::length;目標引用寫實例的類型。和第一種寫法相同,只不過第一種的方法是靜態方法,這里是實例方法。
(3)指向現存外部對象實例方法的方法引用
目標引用為現存外部對象,調用其實例方法,例如:
String?s?=?"草捏子"; Supplier<Integer>?len?=?()?->?s.length();調用了局部變量?s?的?length?方法,可寫為:
String?s?=?"草捏子"; Supplier<Integer>?len?=?s::length;目標引用寫變量名,區別于前兩種。
?
5. 復合Lambda表達式
之前的例子都是使用的單個Lambda表達式,現在我們把多個Lambda表達式組合在一起,構建更復雜一點的表達式。
5.1 比較器復合(Comparator)
我們使用?Comparator?對蘋果進行排序,按重量從小到大:
List<Apple>?apples?=?Arrays.asList(new?Apple("red",?50),?new?Apple("red",?100),?new?Apple("green",?100)); apples.sort(Comparator.comparing(Apple::getWeight));對?Comparator?的靜態方法comparing?簡單介紹下,接受一個?Function?類型的參數,返回一個?Comparator?類型的實例,定義如下:
public?static?<T,?U?extends?Comparable<??super?U>>?Comparator<T>?comparing(Function<??super?T,???extends?U>?keyExtractor) {Objects.requireNonNull(keyExtractor);return?(Comparator<T>?&?Serializable)(c1,?c2)?->?keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2)); }通過使用compareTo,實現了重量從小到大的排序,那想按重量從大到小排序,怎么辦呢?可以使用?Comparator?的?reversed?方法:
apples.sort(Comparator.comparing(Apple::getWeight).reversed());reversed?的實現如下:
default?Comparator<T>?reversed()?{return?Collections.reverseOrder(this); }使用工具類得到一個反序的?Comparator。你可能會好奇Comparator?作為一個接口,reversed?方法可以有具體的實現,接口的實例方法應該都是抽象方法,那它還是一個有效的函數式接口嗎,或者說還是一個有效的接口嗎?
回想下第二節的內容,函數式接口是只定義一個抽象方法的接口。Comparator的抽象方法只有一個?compare,其他是具體方法,所以是合法的函數式接口。那么接口中為什么能定義具體方法呢?Java8 之前是不支持的,但在 Java8 中引入了?default?關鍵字。
當在接口中用default聲明一個方法時,允許它是一個具體方法。這樣的好處在于,我們可以在Lambda表達式之后直接跟上一個具體方法,對Lambda表達式增強,實現更復雜的功能。在后文介紹的用于復合表達式的方法都是接口中的?default?方法。
下面我們試著實現更復雜的排序,在按重量從大到小排序后,按顏色排序:
apples.sort(Comparator.comparing(Apple::getWeight).reversed()); apples.sort(Comparator.comparing(Apple::getColor));先后用兩個Comparator。而使用?Comparator?的?thenComparing?方法可以繼續連接一個?Comparator,從而構建更復雜的排序:
apples.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor));5.2 謂詞復合(Predicate)
Predicate?的?test?方法?(T) -> boolean返回一個布爾表達式。類似 Java 在為布爾表達式提供的與或非,Predicate中也有對應的方法?and、or、negate。例如:
//?重的蘋果 Predicate<Apple>?heavyApple?=?a?->?a.getWeight()?>?100; //?紅的蘋果 Predicate<Apple>?redApple?=?a?->?a.getColor().equals("red");//?輕的蘋果 Predicate<Apple>?lightApple?=?heavyApple.negate(); //?不紅的蘋果 Predicate<Apple>?nonRedApple?=?redApple.negate(); //?重且紅的蘋果 Predicate<Apple>?heavyAndRedApple?=?heavyApple.and(redApple); //?重或紅的蘋果 Predicate<Apple>?heavyOrRedApple?=?heavyApple.or(redApple);5.3 函數復合(Function)
Function?(T) -> R,對輸入做映射。我們通過將多個Function進行組合,實現將一個Function的輸出作為另一個Function的輸入,是不是有管道的感覺。下面請看具體的方法。
andThen方法,a.andThen(b),將先執行a,再執行b。
Function<Integer,?Integer>?f?=?x?->?x?+?1; Function<Integer,?Integer>?g?=?x?->?x?*?2; Function<Integer,?Integer>?h?=?f.andThen(g); int?result?=?h.apply(1);?//?4compose方法,a.compose(b),將先執行b,再執行a。
Function<Integer,?Integer>?f?=?x?->?x?+?1; Function<Integer,?Integer>?g?=?x?->?x?*?2; Function<Integer,?Integer>?h?=?f.compose(g); int?result?=?h.apply(1);?//?3?
總結
以上是生活随笔為你收集整理的系统学习Lambda表达式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 艿艿连肝了几个周末,写了一篇贼长的 Sp
- 下一篇: 和6岁孩子的函数式编程对话