在《挑蘋果中的行爲參數化思想》已經介紹了使用Lambda表達式將行爲抽象化,對Lambda表達式有必定認識。而本文將對Lambda表達式進行系統性的介紹。java
首先咱們要知道如何寫Lambda表達式,或者說怎麼樣才能寫出有效的Lambda表達式,這就須要瞭解其語法。express
Lambda表達式由三部分組成:segmentfault
有兩種風格,分別是:app
(parameters) -> expression
函數
(parameters) -> { statements; }
工具
依據上面的風格介紹,來試着判斷下面給出的示例是否有效:ui
() -> {}
() -> "Apple"
() -> { return "Apple"; }
(Integer i) -> return "Apple" + i
(String s) -> { "Apple"; }
解析:(1)是塊風格,沒有語句;(2)是表達式風格,一個字符串表達式;(3)是塊風格,有花括號和返回語句;(4)非有效,寫了返回語句,但缺乏花括號,補上花括號和分號,爲塊風格,而去掉return
則爲表達式風格;(5)非有效,"Apple"
是一個字符串表達式,不是一個語句,加上return
,或者去掉分號和花括號。this
Lambda表達式寫好了,咱們要知道哪裏能用Lambda表達式。已知Lambda表達式可看做是匿名內部類的實現,那對於匿名內部類來講最重要的是類所實現的接口,而Lambda表達式是否可用於全部接口?答案「不是的」,Lambda表達式對接口有必定的要求,必須是函數式接口。spa
所謂的函數式接口指的是只定義一個抽象方法的接口。code
例如:
public interface Comparator<T> { int compare(T o1, T o2); }
public interface Runnable { void run(); }
public interface Callable<V> { V call() throws Exception; }
可見上面三個接口都只有一個抽象方法,可是三個方法的簽名都不同,這要求Lambda表達式與實現接口的方法簽名要一致。下面用函數描述符來表示上述三個方法的簽名,箭頭前面是方法的入參類型,後面是返回類型。
(T, T) -> int
,兩個泛型T類型的入參,返回int類型Lambda表達式:(Apple a1, Apple a2) -> a1.getWeight - a2.getWeight
() -> void
,無入參,無返回值Lambda表達式:() -> { System.out.println("Hi"); }
() -> V
,無入參,返回一個泛型V類型的對象Lambda表達式:() -> new Apple()
看call方法的示例,你是否會疑惑,new Apple()
是一個語句,爲何沒有花括號和分號,是否是非有效的。你須要記住這是合法的,這是一個特殊的規定,不須要用括號環繞返回值爲void的單行方法調用。
下面介紹在Java中內置的經常使用Lambda表達式:
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<String>類型的變量,又由於函數描述符爲
(T) -> boolean
,則可推斷出參數T的實際類型爲String。並且當只有一個參數時,能夠將括號也省略。
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 grams
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());
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進行修改,看下會發生什麼。
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
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;
目標引用寫變量名,區別於前兩種。
以前的例子都是使用的單個Lambda表達式,如今咱們把多個Lambda表達式組合在一塊兒,構建更復雜一點的表達式。
咱們使用 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));
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);
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); // 4
compose
方法,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