Java8學習(3)- Lambda 表達式


豬腳:以下內容參考《Java 8 in Action》

本次學習內容:

  • Lambda 基本模式
  • 環繞執行模式
  • 函數式接口,類型推斷
  • 方法引用
  • Lambda 復合

代碼: https://github.com/Ryan-Miao/someTest/blob/master/src/main/java/com/test/java8/c3/AppleSort.java

上一篇: Java8學習(2)- 通過行為參數化傳遞代碼--lambda代替策略模式


1. 結構

初始化一個比較器:

Comparator<Apple> byWeight = new Comparator<Apple>() {
    public int copare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight() );
    }
}

使用Lambda表達式:

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight() );

  • 參數列表--compare方法的的兩個參數
  • 箭頭 --- 把參數列表與lambda主體分割開
  • Lambda主體 --- 表達式的值就是Lambda的返回值

1.1 Java8中有效的Lambda表達式

接收一個字符串,並返回字符串長度int

(String a) -> s.length()

接收一個Apple類參數,返回一個boolean值

(Apple a) -> a.getWeight() > 150

接收兩個參數,沒有返回值(void),多行語句需要用大括號包圍

(int x, int y) -> {
    System.out.println("Result:");
    System.out.println(x + y);
}

不接收參數,返回一個值

()-> 42

接收兩個參數,返回一個值

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

1.2 Lambda的基本語法

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

2. 函數式接口

在上次的學習中的通過行為參數化傳遞代碼, Predicate(T)就是一個函數式接口:

public interface Predicate<T> {
    boolean test(T t);
}

函數式接口就是只定義一個抽象方法的接口。
Java API中很多符合這個條件。比如:

public interface Comparable<T> {
    public int compareTo(T o);
}

public interface Runnable {
    public abstract void run();
}

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

2.1 函數式接口可以做什么

Lambda表達式允許你直接以內聯的形式為函數式接口的抽象方法提供實現,並把表達式作為函數式接口的實例(函數式接口一個具體實現的實例)。就像內部類一樣,但看起來比內部類簡潔。

Runnable r1 = () -> System.out.println("1");

Runnable r2 = new Runnable(){
    public void run(){
        System.out.println("2");
    }
};

public static void process(Runnable r) {
    r.run();
}

process(r1);
process(r2);
process(() -> System.out.println(3));

@FunctionalInterface是一個標注,用來告訴編譯器這是一個函數式接口,如果不滿足函數式接口的條件,編譯器就會報錯。當然,這不是必須的。好處是編譯器幫助檢查問題。

3. 一步步修改為Lambda表達式

Lambda式提供了傳遞方法的能力。這種能力首先可以用來處理樣板代碼。比如JDBC連接,比如file讀寫。這些操作會有try-catcha-finally,但我們更關心的是中間的部分。那么,是不是可以將中間的部分提取出來,當做參數傳遞進來?

3.1 第1步: 行為參數化

下面是讀一行:

public String read(){
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return br.readLine();
    } catch (IOException e) {
        e.printStackTrace();
    }
    
    return null;
}

行為參數化就是把一個過程行為轉換成參數。在這里就是將br.readLine()提取成參數。

3.2 第2步:使用函數式接口來傳遞行為

定義一個接口來執行上述的行為:

public interface BufferedReaderProcessor{
    String process(BufferedReader b) throws IOException;
}

然后把這個接口當作參數:

public String read(BufferedReaderProcessor p) throws IOException{
    try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
        return p.process(br);
    }
}

3.3 第3步: 傳遞Lambda

@Test
public void readFile() throws IOException {
    String oneLine = read(BufferedReader::readLine);
    String twoLine = read((BufferedReader b) -> b.readLine() + b.readLine());
}

如此,我們就把中間的邏輯抽出來了。把行為抽象成一個接口調用,然后通過Lambda來實現接口的行為。傳遞參數。完畢。

4. Java API中內置的一些函數式接口

Java API中內置了一些很有用的Function接口。

4.1 Predicate

java.util.function.Predicate<T> 定義了一個抽象方法,返回一個boolean
使用demo如下:

private <T>  List<T> filter(List<T> list, Predicate<T> p){
    List<T> results = new ArrayList<>();
    for (T t : list) {
        if (p.test(t)){
            results.add(t);
        }
    }
    return results;
}
@Test
public void testPredicate(){
    List<String> list = Arrays.asList("aa","bbb","ccc");
    List<String> noEmpty = filter(list, (String s) -> !s.isEmpty());
}

4.2 Consuer

java.util.function.Consumer<T>定義了一個抽象方法,接收一個參數。

private <T> void forEach(List<T> list, Consumer<T> c){
    for (T t : list) {
        c.accept(t);
    }
}
@Test
public void testConsumer() {
    List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
    forEach(integers, System.out::println);
}

4.3 Function

java.util.function.Function<T,R>定義了一個抽象方法,接收一個參數T,返回一個對象R

private <T,R> List<R> map(List<T> list, Function<T,R> f){
    List<R> result = new ArrayList<>();
    for (T t : list) {
        result.add(f.apply(t));
    }
    return result;
}

@Test
public void testFunction(){
    List<String> strings = Arrays.asList("a", "bb", "ccc");
    List<Integer> lengths = map(strings, String::length);
}

4.4 基本類型函數接口

前面三個泛型函數式接口Predicate<T>Consumer<T>Function<T,R>,這些接口是專門為引用類型設計的。那么基本類型怎么辦?我們知道可以自動裝箱嘛。但裝箱是有損耗的。裝箱(boxing)的本質是把原始類型包裹起來,並保存在堆里。因此裝箱后的值需要更多的內存,並需要額外的內存搜索來獲取包裹的原始值。

Java8為函數式接口帶來了專門的版本。

@Test
public void testIntPredicate() {
    //無裝箱
    IntPredicate intPredicate = (int t) -> t%2 == 0;
    boolean isEven = intPredicate.test(100);
    Assert.assertTrue(isEven);
    //裝箱
    Predicate<Integer> integerPredicate = (Integer i) -> i%2 == 0;
    boolean isEven2 = integerPredicate.test(100);
    Assert.assertTrue(isEven2);
}

類似的還有:

Java 8中的常用函數式接口

5. Lambda原理

  • 編譯器可以推斷出方法的參數類型,由此可以省略一些樣板代碼。
  • void和其他返回值做了兼容性處理

6. Lambda的局部變量

在Lambda中可以使用局部變量,但要求必須是final的。因為Lambda可能在另一個線程中運行,而局部變量是在棧上的,Lambda作為額外的線程會拷貝一份變量副本。這樣可能會出現同步問題,因為主線程的局部變量或許已經被回收了。基於此,必須要求final的。

而實例變量則沒問題,因為實例變量存儲於堆中,堆是共享的。

7. 方法引用

Lambda表達式可以用方法引用來表示。比如

(String s) -> s.length()
==
String::length

這是因為可以通過Lambda表達式的參數以及方法來確定一個方法。在這里,每個方法都叫做方法簽名。方法簽名由方法名+參數列表唯一確定。其實就是重載的判斷方式。

當Lambda的主體只是一個簡單的方法調用的時候,我們可以直接使用一個方法引用來代替。方法引用可以知道要接受的參數類型,以及方法體的邏輯。

方法引用結構:
類名::方法名

什么可以使用方法引用?

  • 靜態方法。
  • 指向任意類型實例方法的方法引用。
  • 指向現有對象的實例方法。

8. 構造函數引用

構造函數可以通過類名::new的方式引用。

9. Lambda實戰

目標: 用不同的排序策略給apple排序。
過程: 把一個原始粗暴的解決方案變得更加簡單。
資料: 行為參數化, 匿名類Lambda, 方法引用.
最終: inventory.sort(comparing(Apple::getWeight) );

9.1 原始方案

/**
 * Created by ryan on 7/20/17.
 */
public class AppleSort {
    private List<Apple> inventory;

    @Before
    public void setUp() {
        inventory = new ArrayList<>();
        inventory.add(new Apple("red", 1));
        inventory.add(new Apple("red", 3));
        inventory.add(new Apple("red", 2));
        inventory.add(new Apple("red", 21));
    }

    @Test
    public void sort_old() {
        Collections.sort(inventory, new Comparator<Apple>() {
            @Override
            public int compare(Apple o1, Apple o2) {
                return o1.getWeight() - o2.getWeight();
            }
        });

        printApples();
    }

    private void printApples() {
        inventory.forEach(System.out::println);
    }
}

排序首先要注意的一點就是排序的標准。那么要搞清楚為什么這樣寫?

Comparator定義的其實就是一個方法,此處就是將排序的原則抽取出來。特別符合Lambda的思想!這里先不說Lambda,先說這個方法的作用:定義什么時候發生交換
跟蹤源碼可以發現這樣一段代碼:

//java.util.Arrays#mergeSort(java.lang.Object[], java.lang.Object[], int, int, int, java.util.Comparator)
if (length < INSERTIONSORT_THRESHOLD) {
    for (int i=low; i<high; i++)
        for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--)
            swap(dest, j, j-1);
    return;
}

假設比較的兩個數為o1o2,並且o1o2前一位(left>right)。如下:

....o1,o2...

compare(o1,o2)的結果大於0則,o1o2交換。那么,顯然,如果

compare(o1,o2) = o1-o2

則說明,前一個值比后一個值大的時候,發生交換。也即大的往后冒泡。就是升序了。
所以:

  • o1-o2 升序
  • o2-o1 降序

9.2 使用List內置sort

好消息是Java8提供了sort方法給list:java.util.List#sort:
則原始方案轉換為:

@Test
public void sort1(){
    inventory.sort(new Comparator<Apple>() {
        @Override
        public int compare(Apple o1, Apple o2) {
            return o1.getWeight() - o2.getWeight();
        }
    });

    printApples();
}

9.3 Lambda表達式代替匿名內部類

從之前的學習可以得到,幾乎所有的匿名內部類都可以用Lambda表達式替代!

inventory.sort((o1, o2) -> o1.getWeight() - o2.getWeight());

9.4 進一步優化Lambda

Comparator提供了一個生成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));
}

其中,Function<T,R>已經在前面學習過了,就是一個接受一個參數並返回另一個參數的函數式接口。在本例中,apple.getWeight()符合接受一個參數apple返回一個int。那么,就可以使用這個方法:

inventory.sort(Comparator.comparing((Apple a)->a.getWeight()));

進一步,將Lambda改為方法引用:

inventory.sort(Comparator.comparing(Apple::getWeight));

這里有個問題,記得之前講的基本類型的自動裝箱嗎。Apple::getWeight的返回值是int。而comparing的返回值是一個對象。那么,必然要經過自動裝箱的過程。所以,應該使用基本類型的函數式接口:

inventory.sort(Comparator.comparingInt(Apple::getWeight));

至此,基本已經改造完畢了。最多就是靜態引入comparingInt方法:

inventory.sort(comparingInt(Apple::getWeight));

目標達到。相比原始方法,不要太簡潔!

話說,這種是不是只能默認升序?因此沒有任何一個單詞可以看出排序規則。

是的,想要降序?

inventory.sort(comparingInt(Apple::getWeight).reversed());

10 復合Lambda

上節看到逆序的方法就是后面追加一個逆序的方法。現在需求變更了。需要先按照顏色排序,然后再按照重量從大到小排序。

10.1 比較器鏈

這里,一共涉及了3個過程。往常的做法是連續寫在一個方法里,或者3個方法連續調用。Lambda提供了類似語句陳述一般的寫法。

inventory.sort(comparing(Apple::getColor)
       .reversed()
       .thenComparingInt(Apple::getWeight));

10.2 謂詞復合

前面的Prediacate接口包含4個方法:negate,and,orisEqual,對應邏輯運算里的取反,,,==。這樣,通過復合就可以寫出語義聲明式的代碼:

想要紅蘋果:

Predicate<Apple> red = apple -> "red".equalsIgnoreCase(apple.getColor());

想要不是紅的蘋果:

Predicate<Apple> nonRed = red.negate();

想要大的紅蘋果:

Predicate<Apple> redAndHeavy = red.and(apple -> apple.getWeight() > 150);

想要大的紅蘋果或者綠色的:

Predicate<Apple> redAndHeavyOrGreen = redAndHeavy.or(apple -> "green".equalsIgnoreCase(apple.getColor()));
或者:
redAndHeavyOrGreen = ((Predicate<Apple>) apple -> "red".equalsIgnoreCase(apple.getColor()))
                .and(apple -> apple.getWeight() > 150)
                .or(apple -> "green".equalsIgnoreCase(apple.getColor()));

10.3 函數復合

f(x) = (x+1) * 2;
求 f(2)

普通寫法:

Assert.assertEquals(6, f(2));

private int f(int x){
    return (x + 1) * 2;
}

函數式可以這樣寫:

Function<Integer, Integer> f = x -> x +1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int r = h.apply(2);
Assert.assertEquals(6, r);

看起來似乎更麻煩了,但這只是一個舉例。事實上,Function提供了連續處理邏輯的能力,可以不斷的處理上一次計算的返回值。

比如,封裝一個寫信的類:

public class Letter {
    public static String addHeader(String text){
        return "From Ryan Miao: " + text;
    }

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

    public static String checkSpelling(String text){
        return text.replace("<", "&lt;");
    }
}

@Test
public void testFunction3(){
    Function<String, String> transformationPipeline =
            ((Function<String, String>)Letter::addHeader)
                    .andThen(Letter::checkSpelling)
                    .andThen(Letter::addFooter);
    String letter = transformationPipeline.apply("Hello world!");
    Assert.assertEquals("From Ryan Miao: Hello world! Kind regards", letter);
}

11 小結

  • Lambda表達式可以理解為一種匿名函數:它沒有名稱,但有參數列表、函數主題、返回值類型,可能還有一個可以拋出的異常列表。
  • Lambda表達式讓你可以更簡潔的傳遞代碼
  • 函數式接口就是僅僅聲明了一個抽象方法的接口。
  • 只有在接受函數式接口的地方才可以使用Lambda表達式。
  • Lambda表達式允許你直接內聯,為函數式接口的抽象方法提供實現,並且將整個表達式作為函數式接口的一個實例
  • Java 8自帶了一些常用函數式接口,放在java.util.function里。包括Prediacate<T>,Function<T,R>,Supplier<T>,Consumer<T>,BinaryOperator<T>
  • 為了避免裝箱操作,對Predicate 和Function<T,R>等通用函數式接口的原始類特殊化:IntPredicate,InToLong等。
  • 環繞執行模式(方法的中間代碼)可以配合Lambda提高靈活性和可重用性。
  • Lambda表達式所需要代表的類型成為目標類型。
  • Comparator,Predicate,Function等函數接口都有幾個可以用來結合Lambda表達式的默認方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM