JDK 1.8 新特性之Lambda表示式

FXBStudy發表於2018-08-24

Lambda表示式基礎

Lambda表示式【Lambda Expressions】也可稱為閉包,是推動 Java 8 釋出的最重要新特性。Lambda 允許把函式作為一個方法的引數(函式作為引數傳遞進方法中),使用 Lambda 表示式可以使程式碼變的更加簡潔緊湊。

我們在哪裡可以使用Lambda呢?我們可以在函式式介面上使用Lambda表示式【這種說法有點抽象】!Lambda表示式可以為函式式介面生成一個例項,Lambda表示式可以被賦給一個變數,或傳遞給一個接受函式式介面作為引數的方法!但Lambda表示式的簽名要和函式式介面的抽象方法保持一致!

Lambda表示式語法:

(parameters) -> expression
或
(parameters) -> { statements; }

lambda表示式的重要特徵:

  • 可選型別宣告:不需要宣告引數型別,編譯器可以從上下文環境中推斷出Lambda表示式的引數型別。
  • 可選的引數圓括號:一個引數且沒有引數型別時無需定義圓括號,但多個引數需要定義圓括號。
  • 可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
  • 可選的返回關鍵字:如果主體只有一個表示式返回值則編譯器會自動返回值,大括號需要指定明表示式返回了一個數值。
  • 控制語句程式碼必須使用花括號進行括起來。
  • 如果主體程式碼只是一個表示式(如:{“IronMan”;})而不是一個語句,我們需要去掉分號以及花括號,或者顯示返回語句(如:{return “Ironman”;}

注意:(String s) -> { "IronMan"; } 編譯無法通過!

“IronMan” 是一個表示式,而不是一個語句。要使此Lambda有效,需要去除花括號和分號,如(String s) -> "IronMan",或者可以使用顯式返回語句,如(String s) -> { return "IronMan"; }

// 定義函式式介面
interface A{
    public String test(String a);
}
A a;// 宣告
// 注意:等號與最後一個分號之間是Lambda表示式
a = temp -> temp;           //正常
a = temp -> { temp; };      //異常
a = temp -> { return temp; };//正常

a = temp -> "IronMan";      //正常
a = temp -> { "IronMan"; }; //異常
a = temp -> { return "IronMan"; }; //正常

a = temp -> "test" + temp ; //正常
a = temp -> { "test" + temp; } ; //異常
a = temp -> { return "test" + temp; } ; //正常

變數作用域:

  1. Lambda 表示式只能引用標記了 final 的外層區域性變數,這就是說不能在 Lambda 內部修改定義在域外的區域性變數,否則會編譯錯誤。
  2. Lambda 表示式的區域性變數可以不用宣告為 final,但是必須不可被後面的程式碼修改(即隱性的具有 final 的語義)
  3. Lambda 表示式當中不允許宣告一個與區域性變數同名的引數或者區域性變數。
// 定義函式式介面
public interface Converter<T1, T2> {
    void convert(int i);
}
int num = 1;  
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
num = 5;  
//報錯資訊:Local variable num defined in an enclosing scope must be final or effectively final
String first = "";  
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(), second.length());  //編譯會出錯 

1. 函式式介面

函式式介面(Functional Interface):有且僅有一個抽象方法,但是可以有多個非抽象方法【預設方法】的介面。函式式介面可以被隱式轉換為Lambda表示式。

@FunctionalInterface註解是 Java 8 新加入的一個註解,用於介面定義上表示其為函式式介面。主要用於編譯級錯誤檢查,加上該註解,當你寫的介面不符合函式式介面定義的時候,編譯器會報錯。

注意:加不加@FunctionalInterface對於介面是不是函式式介面沒有影響,該註解只是提醒編譯器去檢查該介面是否僅包含一個抽象方法 。

說明: JDK 1.8之前只要是包含一個抽象方法的介面都是函式式介面,而 JDK 1.8 之後新增加的java.util.function包定義了各種型別的函式式介面,這樣我們就無需重複自定義函式式介面了。

我們用函式式介面可以幹什麼呢?Lambda表示式允許你直接以內聯的形式為函式式介面的抽象方法提供實現,並把整個表示式作為函式式介面的例項(具體來說,是函式式介面一個具體實現的例項)。

我們用匿名內部類也可以完成同樣的事情,只不過比較笨拙:需要提供一個實現,然後在直接內聯將它例項化。如下:

public static void process(Runnable r){
    r.run();
}
public static void main(String[] arg) {
    Runnable r1=()-> System.out.println("Hello World 1"); // 使用 Lambda
    Runnable r2=new Runnable() { // 使用匿名類
        @Override
        public void run() {
            System.out.println("Hello World 2");
        }
    };      
    process(r1);
    process(r2);
    process(()-> System.out.println("Hello World 3"));// 使用 Lambda
}

  通俗的理解是 Lambda 表示式的程式碼補充其呼叫處的介面所呼叫地方的抽象方法的程式碼,如上面的程式碼中r1程式碼新增到了r.run()的執行程式碼中。並且Lambda表示式的引數無論是數量還是型別都應與介面的抽象方法保持一致。

  使用 Lambda 表示式這樣做的優點就是行為的分離,我們想利用process執行另一種行為,只需要在呼叫處將行為程式碼進行傳遞就可以了,大大降低了程式碼的依賴性,同時也簡化了程式碼量,如果我們使用匿名內部類,如上r2的宣告中的開頭與結尾完全重複的程式碼還得再書寫一遍,利用 Lambda 就可以省去這部分重複的程式碼。

  **Lambda 的這種方式就是行為引數化,使用函式式介面來傳遞行為。**Lambda表示式允許你直接內聯,為函式式介面的抽象方法提供實現,並且將整個表示式作為函式式介面的一個例項。

2. 函式描述符

函式式介面的抽象方法的簽名基本上就是Lambda表示式的簽名。我們將這種抽象方法叫做函式描述符(function descriptor)

java.util.function包中定義的函式式接檢視錶:《java.util.function包函式式介面

java.util.function包中定義的函式式介面基本上從名稱上就可以判斷其簽名!

  • 命名時的單詞:
    • 表示輸入引數:
    • Bi:表示兩個不同型別的引數
    • Binary:表示兩個相同型別的引數
    • Supplier:表示參,有返回值的時候命名:返回值型別 + Supplier
    • 表示返回值:
    • Consumer:表示返回值為 void
    • Function:表示 返回一個值
    • Operator:表示 返回值型別與引數型別相同,當多個引數的時候通常與Binary共同使用
    • Predicate:表示返回值為 boolean

命名的基本順序:

  • 輸入引數型別 + To + 輸出型別 + 輸入引數【單詞】 + 輸出型別【單詞】
  • 無參並具有返回值:返回值型別 + Supplier
  • 僅單詞命名的函式式介面:
    • Supplier:無引數,返回一個結果
    • Consumer:代表了接受一個輸入引數並且無返回的操作
    • Function:接受一個輸入引數,返回一個結果
    • Predicate:接受一個輸入引數,返回一個布林值結果

Predicate介面使用示例:

// 定義List集合過濾器
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> results = new ArrayList<>();
    for (T s : list) {
        if (p.test(s)) {
            results.add(s);
        }
    }
    return results;
}
// 定義過濾方法
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

Consumer介面示例:

public static <T> void forEach(List<T> list, Consumer<T> c) {
    for (T i : list) {
        c.accept(i);
    }
}
forEach(Arrays.asList(1,2,3,4,5),(Integer i) -> System.out.println(i));

Function介面示例:

public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for (T s : list) {
        result.add(f.apply(s));
    }
    return result;
}
// [7, 2, 6]
List<Integer> L = map(Arrays.asList("lambdas","in","action"), (String s) -> s.length());

原始型別特化

  Java 型別要麼是引用型別(比如Byte、Integer、Object、List),要麼是原始型別(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能繫結到引用型別,這是由泛型內部的實現方式造成的。不過 Java 提供了自動裝箱技術,這個自動的轉變是虛擬機器自動轉換的,很有意義,但是在效能方面是要付出代價的。裝箱後的值本質上就是把原始型別包裹起來,並儲存在堆中。因此裝箱後的值需要更多的記憶體,並需要額外的記憶體搜尋來獲取被包裹的原始值。

  Java 8 為我們前面所說函式式介面帶來了一個專門的版本,以便在輸入和輸出都是原始型別時避免自動裝箱的操作

例如:使用IntPerdicate就避免了對值進行裝箱操作,但要是用Predicate<Integer>就會把引數裝箱成Integer物件中。

import java.util.function.Predicate;
public interface IntPredicate{//JDK中已經定義,再次只是強調說明
    boolean test (int t);
}
IntPredicate evenNumbers = (int i)->i%2==0;
evenNumbers.test(1000);//無自動裝箱
Predicate<Integer> oddNumbers=(Integer i)->i%2==1;
oddNumbers.test(1000);//自動裝箱

  一般來說,針對專門的輸入引數型別的函式式介面的名稱都要加上對應的原始型別字首,比如DoublePerdicateFunction介面還有針對輸出引數型別的變種:ToIntFunction<T>IntToDoubleFunction等。

注意:Java API 提供了最常用的函式式介面及其函式描述符(java.util.function包中以及只有一個抽象方法的介面),如果有需要,我們完全可以自己設計一個。

函式式介面 函式描述符 原始型別特化
Predicate<T> T → boolean Int*,Long*,Double*
Consumer<T> T → void Int*,Long*,Double*
Function<T> T → R Int*<R>, Long*<R>,Double*<R>,IntToDouble*, IntToLong*, LongToDouble*,
LongToInt*, DoubleToInt*<R>, DoubleToLong*<T>
Supplier<T> () → T Boolean*,Int*,Long*, Double*
UnaryOperator<T> T → T Int*,Long*,Double*
BinaryOperator<T> (T,T) → T Int*,Long*,Double*
BiPredicate<L,R> (L,R) → boolean
BiConsumer<T,U> (T,U) → void ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>
BiFunction<T,U,R> (T,U) → R ToIntBiFunction<T,U>,ToDoubleBiFunction<T,U>, ToLongBiFunction<T,U>

3. 型別檢查、推斷及限制

型別檢查

Lambda 的型別是從使用 Lambda 的上下文推斷出來的。上下文(例如接受它傳遞的方法的引數,或接受它的值得區域性變數)中 Lambda 表示式需要的型別稱為目標型別

解讀Lambda表示式的型別檢查過程:

解讀Lambda表示式的型別檢查過程

有了目標型別的概念,同一個Lambda表示式就可以與不同的函式式介面聯絡起來,只要它們的抽象方法簽名能夠相容。例如下面兩個賦值是有效的:

Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;

特殊的 void 相容規則:

如果一個 Lambda 的主體是一個語句表示式, 它就和一個返回 void 的函式描述符相容(當然需要引數列表也相容)。例如,以下兩行都是合法的,儘管Listadd方法返回了一個boolean,而不是Consumer上下文(T -> void)所要求的void

// Predicate返回了一個boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一個void
Consumer<String> b = s -> list.add(s);

Lambda 表示式可以從賦值的上下文、方法呼叫的上下文(引數和返回值),以及型別轉換的上下文中獲得目標型別。

型別推斷

Java 編譯器會從上下文(目標型別)推斷出用什麼函式式介面來配合 Lambda 表示式,這意味著它也可以推斷出適合 Lambda 的簽名,因為函式描述符可以通過目標型別來得到。這樣做的好處在於,編譯器可以瞭解 Lambda 表示式的引數型別,這樣就可以在 Lambda 語法中省去標註引數型別。換句話說, Java 編譯器會像下面這樣推斷Lambda 的引數型別:

//引數a沒有顯式型別
List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));

注意:有時候顯式寫出型別更易讀,有時候去掉它們更易讀。沒有什麼法則說哪種更好;對於如何讓程式碼更易讀,程式設計師必須做出自己的選擇。

區域性變數

通常情況下我們所使用的的 Lambda 表示式都只用到了其主體裡面的引數。Lambda 表示式也允許使用自由變數(不是引數,而是在外層作用域中定義的變數),就像匿名類一樣。它們被稱作 捕獲Lambda

例如,下面的 Lambda 捕獲了portNumber變數:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

儘管如此,還有一點點小麻煩:關於能對這些變數做什麼有一些限制。

Lambda 可以沒有限制地捕獲(也就是在其主體中引用)例項變數和靜態變數。但區域性變數必須顯式宣告為final,或事實上是final。換句話說, Lambda 表示式只能捕獲指派給它們的區域性變數一次。(注:捕獲例項變數可以被看作捕獲最終區域性變數this。) 例如,下面的程式碼無法編譯,因為portNumber變數被賦值兩次:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;

注意:JDK8允許內部類使用的外部變數可以不用宣告其為final,但實際上還是final型別。

對區域性變數的限制

為什麼區域性變數有這些限制?

第一,例項變數和區域性變數背後的實現有一個關鍵不同。例項變數都儲存在堆中,而區域性變數則儲存在棧上。如果Lambda 可以直接訪問區域性變數,而且 Lambda 是在一個執行緒中使用的,則使用Lambda的執行緒,可能會在分配該變數的執行緒將這個變數收回之後,去訪問該變數。因此, Java 在訪問自由區域性變數時,實際上是在訪問它的副本,而不是訪問原始變數。如果區域性變數僅僅賦值一次那就沒有什麼區別了 – 因此就有了這個限制。

第二,這一限制不鼓勵你使用改變外部變數的典型指令式程式設計模式(這種模式會阻礙很容易做到的並行處理)。

閉包

你可能已經聽說過閉包(closure,不要和Clojure程式語言混淆)這個詞,你可能會想 Lambda 是否滿足閉包的定義。用科學的說法來說,閉包就是一個函式的例項,且它可以無限制地訪問那個函式的非本地變數。例如,閉包可以作為引數傳遞給另一個函式。它也可以訪問和修改其作用域之外的變數。現在, Java 8 的 Lambda 和匿名類可以做類似於閉包的事情:它們可以作為引數傳遞給方法,並且可以訪問其作用域之外的變數。但有一個限制:它們不能修改定義 Lambda 的方法的區域性變數的內容。這些變數必須是隱式最終的。可以認為 Lambda 是對值封閉,而不是對變數封閉。如前所述,這種限制存在的原因在於區域性變數儲存在棧上,並且隱式表示它們僅限於其所線上程。如果允許捕獲可改變的區域性變數,就會引發造成執行緒不安全的新的可能性,而這是我們不想看到的(例項變數可以,因為它們儲存在堆中,而堆是線上程之間共享的)。

4. 異常

注意:任何函式式介面都不允許丟擲受檢異常(checked exception)。
如果我們需要Lambda表示式來丟擲異常,我們有兩種辦法:

  1. 定義一個自己的函式式介面,並宣告受檢異常。

  2. 把Lambda包在一個try/catch塊中。

注意:如果 Lambda 表示式丟擲一個異常,那麼抽象方法所宣告的 throws 語句也必須與之匹配。

5. 方法引用

方法引用讓你可以重複使用現有的方法定義,並像Lambda一樣傳遞它們。在一些情況下,比起使用 Lambda 表示式,它們似乎更易讀,感覺也更自然。

方法引用可以被看作僅僅呼叫特定方法的Lambda的一種快捷寫法。它的基本思想是,如果一個 Lambda 代表的只是“直接呼叫這個方法”,那最好還是用名稱來呼叫它,而不是去描述如何呼叫它。事實上,方法引用就是讓你根據已有的方法實現來建立 Lambda 表示式,並顯式地指明方法的名稱,你的程式碼的可讀性會更好。

它是如何工作的呢?當你需要使用方法引用時,目標引用放在分隔符::前,方法的名稱放在後面。

例如 ,Apple::getWeight就是引用了Apple類中定義的方法getWeight注意:不需要括號,因為你沒有實際呼叫這個方法。方法引用就是 Lambda 表示式(Apple a) -> a.getWeight()的快捷寫法。下表給出了Java 8中方法引用的其他一些例子。

Lambda及其等效方法引用的例子

你可以把方法引用看作針對僅僅涉及單一方法的Lambda的語法糖,因為你表達同樣的事情時要寫的程式碼更少了。

方法引用主要有三類:

  1. 靜態方法引用(例如IntegerparseInt方法,寫作Integer::parseInt)。
    • 語法:Class::static_method
    • 形式等價引數:Lambda 表示式的引數 即 靜態方法的引數
  2. 特定類的任意物件的方法引用(例如Stringlength方法,寫作String::length)。
    • 語法:Class::method
    • 形式等價引數:Lambda 表示式的引數 即 類引用 + 方法的引數
  3. 特定物件的方法引用(假設你有一個區域性變數expensiveTransaction用於存放Transaction型別的物件,它支援例項方法getValue,那麼你就可以寫expensiveTransaction::getValue)。
    • 語法:instance::method
    • 形式等價引數:Lambda 表示式的引數 即 方法的引數

第二種和第三種方法引用可能乍看起來有點兒暈。

類似於String::length的第二種方法引用的思想就是你在引用一個物件的方法,而這個物件本身是Lambda的一個引數。例如, Lambda表示式(String s) -> s.toUppeCase()可以寫作String::toUpperCase

但第三種方法引用指的是,你在 Lambda 中呼叫一個已經存在的外部物件中的方法。例如,Lambda表示式()->expensiveTransaction.getValue()可以寫作expensiveTransaction::getValue

依照一些簡單的方子,我們就可以將 Lambda 表示式重構為等價的方法引用,如下圖所示:

將Lambda表示式重構為等價的方法引用

示例(對一個字串的List排序,並忽略大小寫):

List<String> str =Arrays.asList("a","b","A","B");
str.sort((s1,s2) -> s1.compareTo(s2));
// Lambda表示式的簽名與  Comparator 的函式描述符相容
// 利用方法引用可改寫為
List<String> str =Arrays.asList("a","b","A","B");
str.sort(String::compareToIgnoreCase);

請注意,編譯器會進行一種與Lambda表示式類似的型別檢查過程,來確定對於給定的函式式介面,這個方法引用是否有效:方法引用的簽名必須和上下文型別匹配。

建構函式引用

對於一個現有建構函式,你可以利用它的名稱和關鍵字new來建立它的一個引用:ClassName::new。它的功能與靜態方法引用類似。

例如,假設有一個建構函式沒有引數。它適合 Supplier 的簽名() -> Apple。你可以這樣做:

Supplier<Apple> c1 = Apple::new;//建構函式引用指向預設的Apple()建構函式
Apple a1 = c1.get();//呼叫Supplier的get方法將產生一個新的Apple

這就等價於:

Supplier<Apple> c1 = () -> new Apple();//利用預設建構函式建立Apple的Lambda表示式
Apple a1 = c1.get();//呼叫Supplier的get方法將產生一個新的Apple

如果你的建構函式的簽名是Apple(Integer weight),那麼它就適合 Function 介面的簽名,於是你可以這樣寫:

Function<Integer, Apple> c2 = Apple::new;//指向Apple(Integer weight)的建構函式引用
Apple a2 = c2.apply(110);//呼叫該Function函式的apply方法,並給出要求的重量,將產生一個Apple

這就等價於:

//用要求的重量建立一個Apple的Lambda表示式
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
//呼叫該Function函式的apply方法,並給出要求的重量,將產生一個新的Apple物件
Apple a2 = c2.apply(110);

在下面的程式碼中,一個由 Integer 構成的 List 中的每個元素都通過我們前面定義的類似的 map 方法傳遞給了Apple 的建構函式,得到了一個具有不同重量蘋果的 List

List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new);//將建構函式引用傳遞給map方法

public static List<Apple> map(List<Integer> list, Function<Integer, Apple> f) {
    List<Apple> result = new ArrayList<>();
    for (Integer e : list) {
        result.add(f.apply(e));
    }
    return result;
}

如果你有一個具有兩個引數的建構函式Apple(String color, Integer weight),那麼它就適合BiFunction介面的簽名,於是你可以這樣寫:

//指向Apple(String color,Integer weight)的建構函式引用
BiFunction<String, Integer, Apple> c3 = Apple::new;
//呼叫該BiFunction函式的apply方法,並給出要求的顏色和重量,將產生一個新的Apple物件
Apple c3 = c3.apply("green", 110);

這就等價於:

//用要求的顏色和重量建立一個 Apple 的 Lambda 表示式
BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
//呼叫該BiFunction函式的apply方法,並給出要求的顏色和重量,將產生一個新的Apple物件
Apple c3 = c3.apply("green", 110);

不將建構函式例項化卻能夠引用它,這個功能有一些有趣的應用。

例如,你可以使用Map來將建構函式對映到字串值。你可以建立一個giveMeFruit方法,給它一個String和一個Integer,它就可以建立出不同重量的各種水果:

static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
    map.put("apple", Apple::new);
    map.put("orange", Orange::new);
    // etc...
}

public static Fruit giveMeFruit(String fruit, Integer weight) {
    return map.get(fruit.toLowerCase())//你用 map 得到了一個Function<Integer,Fruit>
            //用Integer型別的weight引數呼叫Function的apply()方法將提供所要求的Fruit
            .apply(weight);
}

Lambda 和方法引用實戰

Java 8 實戰_高清中文版》第3.7章

  • 需求:蘋果根據重量排序!
  • 背景:利用List集合inventory來儲存蘋果Apple,並利用Listsort方法進行排序。

傳統做法

//建立比較器
public class AppleComparator implements Comparator<Apple> {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
}
inventory.sort(new AppleComparator());

使用匿名類

List inventory = new ArrayList<>();
inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

使用Lambda表示式

inventory.sort(
    (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
);

Java 編譯器可以根據 Lambda 出現的上下文來推斷 Lambda 表示式引數的型別。因此可以重寫為:

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

Comparator具有一個叫做comparing的靜態輔助方法,他可以接受一個Function來提取Comparable鍵值,並生成一個Comparator物件。可以如下方式使用(當前傳遞的Lambda只有一個引數:Lambda說明了如何從蘋果中提取需要比較的鍵值):

Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());

通過靜態匯入的方式簡化程式碼如下:

import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));

使用方法引用

方法引用就是替代那些轉發引數的Lambda表示式的語法糖。

import static java.util.Comparator.comparing;
inventory.sort(comparing(Apple::getWeight));

複合Lambda表示式

謂詞:在計算機語言的環境下,謂詞是指條件表示式的求值返回真或假的過程。

Java 8 的函式式介面基本上都有為方便而設計的方法。具體而言,許多函式式介面,比如用於傳遞Lambda表示式的ComparatorFunctionPredicate都提供了允許你進行復合的方法。這是什麼意思呢?在實踐中,這意味著你可以把多個簡單的 Lambda 複合成複雜的表示式。比如,你可以讓兩個謂詞之間做一個or操作,組合成一個更大的謂詞。而且,你還可以讓一個函式的結果成為另一個函式的輸入。你可能會想,函式式介面中怎麼可能有更多的方法呢?這些都是預設方法,而不是抽象方法。

比較器Comparator複合

我們前面使用靜態方法Comparator.comparing,根據提取用於比較的鍵值的Function來返回一個Comparator,如下所示:

// 建立根據重量比較的比較器:正序
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
// 建立根據重量比較的比較器:逆序
Comparator<Apple> c = Comparator.comparing(Apple::getWeight).reversed()
// 建立根據重量比較的比較器:比較器鏈
Comparator<Apple> c = Comparator.comparing(Apple::getWeight)
        .reversed() // 按重量遞減排序
        .thenComparing(Apple::getCountry); // 兩個蘋果一樣重時,進一步按國家排序

謂詞Predicate複合

謂詞介面包括三個方法: negateandor,讓你可以重用已有的 Predicate 來建立更復雜的謂詞。比如,你可以使用 negate 方法來返回一個 Predicate 的非,比如蘋果不是紅的:

Predicate<Apple> notRedApple = redApple.negate();//產生現有Predicate物件redApple的非

你可能想要把兩個 Lambda 用 and 方法組合起來,比如一個蘋果既是紅色又比較重:

// 連結兩個謂詞來生成另一個Predicate物件
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);

你可以進一步組合謂詞,表達要麼是重(150克以上)的紅蘋果,要麼是綠蘋果:

//連結Predicate的方法來構造更復雜Predicate物件
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150)
        .or(a -> "green".equals(a.getColor()));

這一點為什麼很好呢?從簡單 Lambda 表示式出發,你可以構建更復雜的表示式,但讀起來仍然和問題的陳述差不多!請注意, andor 方法是按照在表示式鏈中的位置,從左向右確定優先順序的。因此, a.or(b).and(c)可以看作(a || b) && c

函式Function複合

把 Function 介面所代表的 Lambda 表示式複合起來。 Function 介面為此配了andThencompose兩個預設方法,它們都會返回 Function 的一個例項。

andThen 方法會返回一個函式,它先對輸入應用一個給定函式,再對輸出應用另一個函式。比如,假設有一個函式f給數字加1 (x -> x + 1),另一個函式g給數字乘2,你可以將它們組合成一個函式h,先給數字加1,再給結果乘2

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);//數學上會寫作g(f(x))或(g o f)(x)
int result = h.apply(1);//這將返回4

你也可以類似地使用compose方法,先把給定的函式用作compose的引數裡面給的那個函式,然後再把函式本身用於結果。

比如在上一個例子裡用compose的話,它將意味著f(g(x)),而andThen則意味著g(f(x))

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);//數學上會寫作f(g(x))或(f o g)(x)
int result = h.apply(1);//這將返回3

下圖說明了 andThencompose 之間的區別:
andThen和compose之間的區別

這一切聽起來有點太抽象了。那麼在實際中這有什麼用呢?比方說你有一系列工具方法,對用String表示的一封信做文字轉換:

public class Letter {
    public static String addHeader(String text) {
        return "From Raoul, Mario and Alan: " + text;
    }

    public static String addFooter(String text) {
        return text + " Kind regards";
    }

    public static String checkSpelling(String text) {
        return text.replaceAll("labda", "lambda");
    }
}

現在你可以通過複合這些工具方法來建立各種轉型流水線了,比如建立一個流水線:先加上抬頭,然後進行拼寫檢查,最後加上一個落款,如下圖所示:

Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline = addHeader.andThen(Letter::checkSpelling)
        .andThen(Letter::addFooter);

使用andThen的轉換流水線

第二個流水線可能只加抬頭、落款,而不做拼寫檢查:

Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline = addHeader.andThen(Lette

讚賞

相關文章