函數式編程——Java中的lambda表達式

背景

在JDK1.8之前,我們經常會遇到下面這幾種場景:

無法傳入方法,只能傳入對象
	Thread thread = new Thread(new Runnable() {
	    @Override
	    public void run() {
	        System.out.println(Thread.currentThread());
	    }
	});
即使是簡單方法,也仍然需要創建完整函數體
    public int add(String s) {
        return a + b;
    }

這兩種場景的缺點是:

  • 代碼冗餘嚴重
  • 使用不靈活

然而,在JDK1.8中,迎來了lambda表達式,上述兩種代碼段可以改造爲下面的形式:

	Thread thread = new Thread(() -> System.out.println(Thread.currentThread()));
	(c1, c2) -> c1 + c2

具體是怎麼操作的,我們接下來慢慢分析

介紹

函數式編程的一個特點就是函數可以作爲參數和返回值,可以把函數當成一個字段值來進行使用,在JDK1.8之前,我們是沒辦法傳入一個方法的,只能將方法封裝到對象中,這樣就帶來了很大的不便,於是在JDK1.8中,引入了以Function爲首的一批爲函數式編程服務的接口

lambda表達式

lambda表達式的形式通常爲:

(T t, V v) -> { /* code */ }

通常我們不能單獨聲明一個lambda表達式,就像我們不能單獨聲明一個字面值常量卻不將它賦給任何變量,我們通常會手動約束需要使用lambda表達式參數的格式,所以可以簡寫成以下形式:

(t, v) -> { /* code */ }

再進一步化簡

t, v -> /* code */

如果沒有參數,則形式可以爲:

() -> /* code */

@FunctionalInterface註解

在JDK1.8中,很多類(如Comparator)上都新增了一個@FunctionalInterface的註解,這個註解的原理在本篇中不深究,我們通過舉一個例子來讓大家瞭解這個註解的用處

這個註解是註解在接口上的,如下:

@FunctionalInterface
public interface Movable {

    void move(String person);
}

如果是在JDK1.7中,假如我們想臨時創建一個實現Movable接口的對象,一般採用以下方式:

        Movable movable = new Movable() {
            
            @Override
            public void move(String person) {
            	// 這裏我們就簡單地打印下內容
                System.out.println(person);
            }
        };

但是現在,我們可以採用以下的方式:

	Movable movable = person -> System.out.println(person);

因爲我們直接將參數原樣輸出,所以有以下的簡化方式:

	Movable movable = System.out::println;

是不是簡潔度一下子就上去了

我們把註解去掉,發現依然能編譯通過,甚至也能正常運行,那可能就要懷疑了,這個註解豈不是沒用?其實Java提供的很多原生註解基本都是用來約束開發的,也就是讓你在碼代碼的時候就能發現錯誤,總比寫完了編譯運行的時候報錯要好

JDK1.8中,把lambda表達式叫做SAM類型(Single Abstract Method),即單一抽象方法類型,從字面上來看是說一個接口中只有一個抽象方法

@FunctionalInterface作爲一個編譯級錯誤檢查註解,當你想用一個lambda表達式來代替一個接口(更準確地說是接口中的方法)時,就需要滿足以下規範,否則就會在編譯時報錯:

  • 註解的類型必須是一個接口
  • 接口中只能有一個抽象方法(靜態方法、默認方法和重寫Object的方法除外)

滿足這兩個條件的就可以用以上形式來方便地進行編碼

Function系列接口

剛剛我們只說了怎麼通過lambda表達式方便地創建一個實現接口的對象,現在我們就要來了解JDK1.8中提供的最重要的功能,將lambda表達式作爲參數和返回值使用

既然是Function系列接口,那麼肯定不止Function接口這一個,不過我們先把重點放在Function上,如下

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

	/* 在本方法執行前執行另一個方法 */
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

	/* 在本方法執行後執行另一個方法 */
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
    
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

可以看出,Function接口中只有一個抽象方法apply(),負責接收T參數,然後返回R參數,剩下的compose()方法可以在執行本方法前執行傳入Function接口的apply()方法,andThen()則是與之相反,在本身方法之後執行

說了那麼多,Function始終還是一個接口,到底怎麼實例化?這時候又得說回lambda表達式了,Function系列接口都是通過一個lambda表達式來進行實例化的,如下:

        Function<Integer, String> func = num -> {
            System.out.println("Run Success...");
            return "[num]" + num;
        };

        String out = func.apply(20);
        System.out.println("out: " + out);

輸出結果:

out: [num]20

還可以使用compose()方法在方法執行前進行其他操作,使用方法如下:

        Function<Integer, String> func = num -> "[num]" + num;
        Function<Integer, Integer> before = num -> num + 100;

        String out = func.compose(before).apply(20);
        System.out.println("out: " + out);

輸出結果:

out: [num]120

這裏一定要注意一點,如果我們把原Function接口聲明爲傳入T參數輸出R類型的話,before接口對象傳入的參數一定要是最終的apply()方法傳入參數的類型或其父類,返回對象一定要是T或T的子類,這裏留一個簡單的問題,建議自己思考一下andThen()方法傳入的Function接口,它的參數和返回值需要滿足需要滿足什麼約束

既然說了是Function接口系列,那自然有一系列類似的接口,總不能都是隻傳一個參數,返回一個參數的這種簡單形式

這裏我借鑑了一下java.util.function-Function接口這篇博文的表格,如下

結尾

JDK1.8中的函數式編程的特性基本就是這樣了,在開發中雖然不推薦過度使用,不過很多時候合理地運用可以減少很多重複的代碼,lambda表達式還是很值得學習的

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