Java - Lambda表達式(Java 8+)

在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是“拿什麼東西做什麼事情”。相對而言,面向對象過分強調“必須通過對象的形式來做事情”,而函數式思想則儘量忽略面向對象的複雜語法——強調做什麼,而不是以什麼形式做。

面向對象的思想:做一件事情,找一個能解決這個事情的對象,調用對象的方法完成事情。

函數式編程思想:只要能獲取到結果,誰去做的怎麼做的都不重要,重視的是結果,不重視過程。

一個栗子——冗餘的Runnable代碼:

package top.onefine.demo.lambda;

// 使用實現Runnable接口的方式實現多線程程序
public class Demo01Runnable {
    public static void main(String[] args) {
        new Thread(new Runnable() {  // 創建Runnable接口的實現類,重寫run方法設置線程任務
            @Override
            public void run() {
                System.out.println("新線程:" + Thread.currentThread().getName() + " 創建了!");
            }
        }).start();
    }
}

當需要啓動一個線程去完成任務時,通常會通過java.lang.Runnable接口來定義任務內容,並使用java.lang.Thread類來啓動該線程。本着“一切皆對象”的思想,這種做法是無可厚非的:首先創建一個 Runnable 接口的匿名內部類對象來指定任務內容,再將其交給一個匿名的線程來啓動。

代碼分析:

對於 Runnable 的匿名內部類用法,可以分析出幾點內容:

  • Thread 類需要 Runnable 接口作爲參數,其中的抽象 run 方法是用來指定線程任務內容的核心;
  • 爲了指定 run 的方法體,不得不需要 Runnable 接口的實現類;
  • 爲了省去定義一個 RunnableImpl 實現類的麻煩,不得不使用匿名內部類;
  • 必須覆蓋重寫抽象 run 方法,所以方法名稱、方法參數、方法返回值不得不再寫一遍,且不能寫錯;
  • 而實際上,似乎只有方法體纔是關鍵所在

編程思想轉換:

我們真的希望創建一個匿名內部類對象嗎?不。我們只是爲了做這件事情而不得不創建一個對象。我們真正希望做的事情是:將 run 方法體內的代碼傳遞給 Thread 類知曉。

傳遞一段代碼——這纔是我們真正的目的,而創建對象只是受限於面向對象語法而不得不採取的一種手段方式。

那,有沒有更加簡單的辦法?如果我們將關注點從“怎麼做”迴歸到“做什麼”的本質上,就會發現只要能夠更好地達到目的,過程與形式其實並不重要。

當然有,2014年3月Oracle所發佈的 Java 8(JDK 1.8) 中,加入了Lambda表達式的重量級新特性,爲我們打開了新世界的大門。

體驗Lambda的更優寫法:

藉助Java 8的全新語法,上述 Runnable 接口的匿名內部類寫法可以通過更簡單的Lambda表達式達到等效:

package top.onefine.demo.lambda;


public class Demo02LambdaRunnable {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("新線程:" + Thread.currentThread().getName() + " 創建了!")).start();
    }
}

這段代碼和剛纔的執行效果是完全一樣的,可以在1.8或更高的編譯級別下通過。從代碼的語義中可以看出:我們啓動了一個線程,而線程任務的內容以一種更加簡潔的形式被指定。不再有“不得不創建接口對象”的束縛,不再有“抽象方法覆蓋重寫”的負擔,就是這麼簡單!

1. Lambda標準格式

Lambda省去面向對象的條條框框,格式由3個部分組成:

  • 一些參數
  • 一個箭頭
  • 一段代碼

Lambda表達式的標準格式爲:

(參數類型 參數名稱)> { 代碼語句 }

格式說明:

  • 小括號()內的語法與傳統方法參數列表一致:無參數則留空;多個參數則用逗號分隔。
  • 箭頭-> 是新引入的語法格式,代表指向動作,可以理解爲將前面的參數傳遞給後面的方法體
  • 大括號{}內的語法與傳統方法體要求基本一致。

一個栗子——無參數無返回情況:

package top.onefine.demo.lambda;


public class Demo03LambdaDemo {
    public static void main(String[] args) {
        invokeCook(() -> System.out.println("喫飯啦"));
		
		// 對比匿名內部類
        invokeCook(new Cook() {
            @Override
            public void makeFood() {
                System.out.println("喫飯啦");
            }
        });
    }
	
	// 定義一個參數是接口的方法,方法體內部調用此參數接口的方法
    public static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}

// 定義接口
interface Cook {
	// 只含一個抽象方法,這裏無參數無返回值
    void makeFood();
}

2. Lambda的參數和返回值

栗子1——有參數有返回值:

package top.onefine.demo.lambda;


import lombok.*;

import java.util.Arrays;
import java.util.Comparator;

public class Demo04LambdaDemo {
    public static void main(String[] args) {
        Person[] array = {
                new Person("one", 18),
                new Person("fine", 19),
                new Person("o", 20)
        };
        // 對數組中的Person對象使用Arrays的sort()方法通過年齡進行升序(前邊-後邊)排序
        /*
        Arrays.sort(array, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();  // 升序
            }
        });*/
        // 使用Lambda表達式
        Arrays.sort(array, (Person o1, Person o2) -> {  // 有參數
            return o1.getAge() - o2.getAge();  // 有返回值(return)
        });

        // 遍歷數組
        for (Person p : array) {
            System.out.println(p);
        }
    }
}

@Getter
@Setter
@ToString
@AllArgsConstructor
class Person {
    private String name;
    private Integer age;

// 這裏使用lombok添加get/set方法、toString方法和全參構造器
}

結果:

Person(name=one, age=18)
Person(name=fine, age=19)
Person(name=o, age=20)

栗子2——有參數有返回值:

package top.onefine.demo.lambda;


public class Demo05LambdaDemo {
    public static void main(String[] args) {
        /*
        invokeCalc(10, 20, new Calculator() {
            @Override
            public int calc(int a, int b) {  // 接口回調
                return a + b;
            }
        });*/
        invokeCalc(10, 20, (int a, int b) -> {return a + b;});
    }

    private static void invokeCalc(int a, int b, Calculator calculator) {
        int result = calculator.calc(a, b);
        System.out.println("結果是:" + result);
    }
}

interface Calculator {
    public abstract int calc(int a, int b);  // 定義兩數相加
}

3. Lambda省略格式

可推導即可省略

Lambda強調的是“做什麼”而不是“怎麼做”,所以凡是可以根據上下文推導得知(即可推導可省略)的信息,都可以省略。例如上例還可以使用Lambda的省略寫法:

invokeCalc(10, 20, (a, b) -> a + b);

省略規則

在Lambda標準格式的基礎上,使用省略寫法的規則爲:

  1. 小括號內參數的類型可以省略;
  2. 如果小括號內有且僅有一個參數,則小括號可以省略;
  3. 如果大括號內有且僅有一個語句,則無論是否有返回值,都可以省略大括號、return關鍵字及語句分號。注,{}return;三個一起省略。

4. Lambda的使用前提

Lambda的語法非常簡潔,完全沒有面向對象複雜的束縛。但是使用時有幾個問題需要特別注意:

  1. 使用Lambda必須具有接口,且要求接口中有且僅有一個抽象方法。無論是JDK內置的 Runnable 、 Comparator 接口還是自定義的接口,只有當接口中的抽象方法存在且唯一時,纔可以使用Lambda。
  2. 使用Lambda必須具有上下文推斷。也就是方法的參數或局部變量類型必須爲Lambda對應的接口類型,才能使用Lambda作爲該接口的實例。

備註:有且僅有一個抽象方法的接口,稱爲“函數式接口”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章