Java 基礎 —— Lambda 表達式

重慶-南山一棵樹

概述

閱讀項目代碼時,尤其是閱讀一些源碼時,經常會遇到 Lambda 表達式。對此之前看過相關文章,但是停留在模模糊糊的印象上。今天趁着有時間,通過一些 demo 示例,梳理一下它的用法,以備後期遺忘的時候快速查詢它的用法!

Lambda 表達式是 Java 8 的重要更新,它支持將代碼塊作為方法參數、允許使用更簡潔的代碼來創建只有一個抽象方法的接口的實例。

描述中提到的接口稱為函數式接口

語法

Lambda 表達式的主要作用就是可以用於簡化創建匿名內部類對象,Lambda 表達式的代碼塊將會用於實現抽象方法的方法體,Lambda 表達式就相當於一個匿名方法。

Lambda 表達式由三部分組成:

  • 形參列表:形參列表允許省略類型,如果形參列表中只有一個參數,形參列表的圓括號也可以省略;
  • 箭頭(->):通過英文畫線和大於符號組成;
  • 代碼塊:如果代碼塊只有一條語句,花括號可以省略。Lambda 代碼塊只有一條 return 語句,可以省略 return 關鍵字,Lambda 表達式會自動返回這條語句的值作為返回值。

示例

interface Eatable {
    void taste();
}

interface Flyable {
    void fly(String weather);
}

interface Addable {
    int add(int a, int b);
}


public class LambdaQs {
    // 調用該方法需要傳入一個 Eatable 類型的對象
    public void eat(Eatable e) {
        System.out.println(e);
        e.taste();
    }

    // 調用該方法需要傳入 Flyable 類型的對象
    public void drive(Flyable f) {
        System.out.println("我正在駕駛:" + f);
        f.fly("「夏日晴天」");
    }

    // 調用該方法需要 Addable 類型的對象
    public void calc(Addable add) {
        System.out.println("5 + 3 = " + add.add(5, 3));
    }

    public static void main(String[] args) {
        LambdaQs lq = new LambdaQs();
        // Lambda 表達式的代碼塊只有一句,因此省略了花括號
        lq.eat(() -> System.out.println("雪糕的味道不錯!"));
        // Lambda 表達式的形參只有一個參數,因此省略了圓括號
        lq.drive(weather -> {
            // 對接口中抽象方法 fly 的重寫
            System.out.println("今天天氣是:" + weather);
            System.out.println("飛機平穩飛行!");
        });
        // Lambda 表達式只有一條語句,即使該表達式需要返回值,也可以省略 return
        lq.calc((a, b) -> a + b);
        // 如果不用 Lambda 表達式,就需要如下匿名類的方式去重寫抽象方法
        lq.calc(new Addable() {
            @Override
            public int add(int a, int b) {
                return a + b;
            }
        });
    }
}

輸出結果:

oop.lambda.LambdaQs$$Lambda$1/1607521710@7ef20235
雪糕的味道不錯!
我正在駕駛:oop.lambda.LambdaQs$$Lambda$2/1329552164@15aeb7ab
今天天氣是:「夏日晴天」
飛機平穩飛行!
5 + 3 = 8
5 + 3 = 8

以上示例可以說明,Lambda 表達式實際上可以被當做一個具體的對象。

Lambda 表達式與函數式接口

Lambda 表達式的類型,也被稱為「目標類型(target type)」。Lambda 表達式的目標類型必須是「函數式接口(functional interface)」。函數式接口代表只包含一個抽象方法的接口。函數式接口可以包含多個默認方法、類方法,但僅能聲明一個抽象方法。

查詢 Java 8 的 API 文檔,可以發現大量的函數式接口,例如:Runnable、ActionListener 等接口都是函數式接口。

Java 8 專門為函數式接口提供了 @FunctionalInterface 註解。該註解就是用於告訴編譯器校驗接口必須是函數式接口,否則就報錯。

由於 Lambda 表達式的結果就是被當做對象/實例,因此,可以使用 Lambda 表達式進行賦值,示例:

Runnable r = () -> {
    for (int i = 0; i < 100; i++) {
        System.out.println(i);
    }
};

我們看一下 Runnable 接口的定義:

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

看一個錯誤示例:

Object obj = () -> {
    for (int i = 0; i < 100; i++) {
        System.out.println(i);
    }
};

上面這段代碼會報錯:Target type of a lambda conversion must be an interface。Lambda 表達式的目標類型必須是明確的函數式接口!將 Lambda 表達式賦值給 Object 類型的變量,編譯器只能推斷出它的表達類型為 Object,而 Object 並不是函數式接口,因此就報錯了!

為了保證 Lambda 表達式的目標類型是明確的函數式接口,有如下三種常見方式:

  • 將 Lambda 表達式賦值給函數式接口類型的變量;
  • 將 Lambda 表達式作為函數式接口類型的參數傳給某個方法;
  • 使用函數式接口對 Lambda 表達式進行強制類型轉換;

將上面出錯的代碼可以進行如下的改寫:

Object obj1 = (Runnable)() -> {
    for (int i = 0; i < 100; i++) {
        System.out.println(i);
    }
};

綜上,Lambda 表達式的本質很簡單,就是使用簡單的語法來創建函數式接口的實例,避免匿名內部類的繁瑣。

方法引用於構造器引用

如果 Lambda 表達式的代碼塊只有一條代碼,還可以在代碼中使用方法引用和構造器引用。

方法引用和構造器引用的好處是使 Lambda 表達式的代碼塊更加簡潔。方法引用和構造器引用都需要使用兩個英文冒號 ::

種類 示例 說明 對應的 Lambda 表達式
引用類方法 類名::類方法 函數式接口中被實現的方法的全部參數傳給該類方法作為參數 (a,b,...) -> 類名.類方法(a,b,...)
引用特定對象的實例方法 特定對象::實例方法 函數式接口中被實現的方法的全部參數傳給該方法作為參數 (a,b,...) -> 特定對象.實例方法(a,b,...)
引用某類對象的實例方法 類名::實例方法 函數式接口中被實現的方法的第一個參數作為調用者,後面的參數全部傳給該方法作為參數 (a,b,...)->a.實例方法(b,...)
引用構造器 類名::new 函數式接口中被實現方法的全部參數傳給該構造器作為參數 (a,b,...)->new 類名(a,b,...)
@FunctionalInterface
interface Converter {
    Integer convert(String from);
}

@FunctionalInterface
interface MyTest {
    String test(String a, int b, int c);
}

@FunctionalInterface
interface YourTest {
    // 抽象方法負責根據 String 參數生成一個 JFrame 返回值
    JFrame win(String title);
}


public class LambdaRef {
    public static void main(String[] args) {
        // 1 引用類方法
        // 下面使用 Lambda 表達式創建 Converter 對象
        Converter converter1 = from -> Integer.valueOf(from);
        Integer val = converter1.convert("99");

        // 函數式接口中被實現方法的全部參數傳給該類方法作為參數
        Converter converter2 = Integer::valueOf;
        Integer val2 = converter2.convert("100");

        // 2 引用特定對象的實例方法
        // 使用 Lmabda 表達式創建 Converter 對象
        Converter converter3 = from -> "hello michael翔".indexOf(from);

        // 調用 "hello michael翔"的indexOf()實例方法
        // 函數式接口中被實現的全部參數傳給該方法作為參數
        Converter converter4 = "hello michael翔"::indexOf;

        // 3 引用某類對象的實例方法
        // 使用 Lambda 表達式創建 MyTest 對象
        MyTest mt = (a, b, c) -> a.substring(b, c);
        String  str = mt.test("Hello World, Hello Michael翔", 2,9);

        // 上面 Lambda 表達式只有一行,因此可以使用如下引用進行替換
        // 函數式接口中被實現方法的第一個參數作為調用者
        // 後面的參數全部傳給該方法作為參數
        MyTest str2 = String::substring;

        // 4 引用構造器
        // 使用 Lambda 表達式創建 YourTest 對象
        YourTest yt = a -> new JFrame(a);
        JFrame jf = yt.win("窗口");

        // 使用構造器引用進行替換
        // 函數式接口中被實現方法的全部參數傳給該構造器作為參數
        YourTest yt2 = JFrame::new;
        JFrame jf2 = yt.win("窗口2");
    }
}

Lambda 表達式與匿名內部類的聯繫與區別

Lambda 表達式與匿名內部類存在如下相同點:

  • Lambda 表達式與匿名內部類一樣,都可以直接訪問 effectively final 的局部變量,以及外部類的成員變量(包括示例變量和類變量);
  • Lambda 表達式創建的對象與匿名內部類生成的對象一樣,都可以直接調用從接口中繼承的默認方法;

Lambda 表達式與匿名內部類的區別:

  • 匿名內部類可以為任意接口創建實例,不管接口包含多少個抽象方法,只要匿名內部類實現所有抽象方法即可;但是 Lambda 表達式只能為函數式接口創建實例;
  • 匿名內部類可以為抽象類甚至普通類創建實例,但是 Lambda 表達式只能為函數式接口創建實例;
  • 匿名內部類實現的抽象方法體允許調用接口中定義的默認方法,但是 Lambda 表達式的代碼塊不允許調用接口中定義的默認方法;
@FunctionalInterface
interface Converter {
    Integer convert(String from);
}

@FunctionalInterface
interface MyTest {
    String test(String a, int b, int c);
}

@FunctionalInterface
interface YourTest {
    // 抽象方法負責根據 String 參數生成一個 JFrame 返回值
    JFrame win(String title);
}


public class LambdaRef {
    public static void main(String[] args) {
        // 1 引用類方法
        // 下面使用 Lambda 表達式創建 Converter 對象
        Converter converter1 = from -> Integer.valueOf(from);
        Integer val = converter1.convert("99");

        // 函數式接口中被實現方法的全部參數傳給該類方法作為參數
        Converter converter2 = Integer::valueOf;
        Integer val2 = converter2.convert("100");

        // 2 引用特定對象的實例方法
        // 使用 Lmabda 表達式創建 Converter 對象
        Converter converter3 = from -> "hello michael翔".indexOf(from);

        // 調用 "hello michael翔"的indexOf()實例方法
        // 函數式接口中被實現的全部參數傳給該方法作為參數
        Converter converter4 = "hello michael翔"::indexOf;

        // 3 引用某類對象的實例方法
        // 使用 Lambda 表達式創建 MyTest 對象
        MyTest mt = (a, b, c) -> a.substring(b, c);
        String  str = mt.test("Hello World, Hello Michael翔", 2,9);

        // 上面 Lambda 表達式只有一行,因此可以使用如下引用進行替換
        // 函數式接口中被實現方法的第一個參數作為調用者
        // 後面的參數全部傳給該方法作為參數
        MyTest str2 = String::substring;

        // 4 引用構造器
        // 使用 Lambda 表達式創建 YourTest 對象
        YourTest yt = a -> new JFrame(a);
        JFrame jf = yt.win("窗口");

        // 使用構造器引用進行替換
        // 函數式接口中被實現方法的全部參數傳給該構造器作為參數
        YourTest yt2 = JFrame::new;
        JFrame jf2 = yt.win("窗口2");
    }
}

Lambda 表達式調用 Arrays 的類方法

Arrays 類的有些方法需要 Comparator、XxxOperator、XxxFunction 等接口的實例,這些接口都是函數式接口。因此,可以使用 Lambda 表達式來調用 Arrays 的方法。

public class LambdaArrays {
    public static void main(String[] args) {
        String[] arr1 = new String[]{"java", "python", "rust", "go"};
        Arrays.parallelSort(arr1, (o1, o2) -> o1.length() - o2.length());
        System.out.println(Arrays.toString(arr1));
        int[] arr2 = {3, -4, 25, 16, 30, 18};
        // left 代表數組中前一個索引處的元素,計算第一個元素時,left 為 1;
        // right 代表數組中的當前索引處的元素
        Arrays.parallelPrefix(arr2, (left, right) -> left * right);
        System.out.println(Arrays.toString(arr2));
        long[] arr3 = new long[5];
        // a 代表正在計算的元素索引
        Arrays.parallelSetAll(arr3, a -> a * 5);
        System.out.println(Arrays.toString(arr3));
        
        // 等價於用匿名內部類重寫 applyAsLong 抽象方法
        Arrays.parallelSetAll(arr3, new IntToLongFunction() {
            @Override
            public long applyAsLong(int value) {
                return value * 5;
            }
        });
        System.out.println(Arrays.toString(arr3));
    }
}

輸出:

[go, java, rust, python]
[3, -12, -300, -4800, -144000, -2592000]
[0, 5, 10, 15, 20]
[0, 5, 10, 15, 20]

因為這些要出入 Comparator、XxxOperator、XxxFunction 等接口的實例往往都是一次性的,使用 Lambda 表達式也不用考慮重用等,反而讓程序更加簡潔了。

總結

本文主要參考的是 《瘋狂 Java 講義第 5 版》的第 6 章的面向對象下,通過實際的示例 demo 應該可以將 Lambda 的常用場景和用法掌握了。這樣,看項目代碼或者源碼的話,會更加易於理解!基本功紮實,才能走得更快!

參考

公眾號-二維碼-截圖

Tags: