系統學習Lambda表達式

《挑蘋果中的行爲參數化思想》已經介紹了使用Lambda表達式將行爲抽象化,對Lambda表達式有必定認識。而本文將對Lambda表達式進行系統性的介紹。java

1. 語法

首先咱們要知道如何寫Lambda表達式,或者說怎麼樣才能寫出有效的Lambda表達式,這就須要瞭解其語法。express

Lambda表達式由三部分組成:segmentfault

  1. 參數列表
  2. 箭頭
  3. 主體

Lambda表達式的三部分

兩種風格,分別是:app

  1. 表達式-風格

    (parameters) -> expression函數

  2. 塊-風格

    (parameters) -> { statements; }工具

依據上面的風格介紹,來試着判斷下面給出的示例是否有效:ui

  1. () -> {}
  2. () -> "Apple"
  3. () -> { return "Apple"; }
  4. (Integer i) -> return "Apple" + i
  5. (String s) -> { "Apple"; }

解析:(1)是塊風格,沒有語句;(2)是表達式風格,一個字符串表達式;(3)是塊風格,有花括號和返回語句;(4)非有效,寫了返回語句,但缺乏花括號,補上花括號和分號,爲塊風格,而去掉return則爲表達式風格;(5)非有效,"Apple"是一個字符串表達式,不是一個語句,加上return,或者去掉分號和花括號。this

2. 函數式接口

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表達式與實現接口的方法簽名要一致。下面用函數描述符來表示上述三個方法的簽名,箭頭前面是方法的入參類型,後面是返回類型。

  1. compare:(T, T) -> int,兩個泛型T類型的入參,返回int類型

    Lambda表達式:(Apple a1, Apple a2) -> a1.getWeight - a2.getWeight

  2. run:() -> void,無入參,無返回值

    Lambda表達式:() -> { System.out.println("Hi"); }

  3. 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<String>類型的變量,又由於函數描述符爲 (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 grams

3.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 類型實例 slength 方法,可寫爲:

Function<String, Integer> fun = String::length;

目標引用寫實例的類型。和第一種寫法相同,只不過第一種的方法是靜態方法,這裏是實例方法。

(3)指向現存外部對象實例方法的方法引用

目標引用爲現存外部對象,調用其實例方法,例如:

String s = "草捏子";
Supplier<Integer> len = () -> s.length();

調用了局部變量 slength 方法,可寫爲:

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,實現了重量從小到大的排序,那想按重量從大到小排序,怎麼辦呢?可使用 Comparatorreversed 方法

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。而使用 ComparatorthenComparing 方法能夠繼續鏈接一個 Comparator,從而構建更復雜的排序:

apples.sort(Comparator.comparing(Apple::getWeight)
            .reversed().thenComparing(Apple::getColor));

5.2 謂詞複合(Predicate)

Predicatetest 方法 (T) -> boolean返回一個布爾表達式。相似 Java 在爲布爾表達式提供的與或非,Predicate中也有對應的方法 andornegate。例如:

// 重的蘋果
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); // 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

相關文章
相關標籤/搜索