Java 8 之 Lambda 表達式

Lambda

在介紹 Lambda 表達式之前乒融,我們先來看只有單個方法的 Interface(通常我們稱之為回調接口):

public interface OnClickListener {
    void onClick(View v);
}

我們是這樣使用它的:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        v.setText("lalala");
    }
  });

這種回調模式在 Android 中非常流行,但是像上面這樣的匿名內部類并不是一個好的選擇赞季,因為:

  • 語法冗余
  • 匿名內部類中的 this 指針和變量容易產生誤解
  • 無法捕獲非 final 局部變量
  • 非靜態(tài)內部類默認持有外部類的引用奢驯,部分情況下會導致外部類無法被 GC 回收,導致內存泄露

而 Java 8 中引入的 Lambda 表達式就解決了以上的問題撒遣,先來看一個簡單的 Lambda 用法:

 //多行注釋中為不使用Lambda的寫法
        
/**
Runnable run = new Runnable() {
    @Override
    public void run() {
        System.out.println("Test")
    }
};
*/

Runnable run = () -> System.out.println("Test");

我們來看一下Runnable接口:

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

函數(shù)式接口

Java 8 引入了 FunctionalInterface 注解來表明一個接口打算成為一個函數(shù)式接口。

函數(shù)式接口: 只定義了一個抽象方法的接口稱之為函數(shù)式接口

在實際使用中不管 FunctionalInterface 注解是否存在禾进,Java 編譯器都會將所有滿足該定義的接口看作是函數(shù)式接口廉涕。

下面我們自己寫一個接口看一下能否如Runnable一般:

public interface TestLambdaInterface {
    void testInterface();
}

我們如下使用該接口狐蜕,程序能正常編譯并輸出了結果,說明 Java 編譯器都會將所有滿足該定義的接口看作是函數(shù)式接口:

TestLambdaInterface test = ()->System.out.println("Test");

test.testInterface();

當我們將方法的返回值改成String時婆瓜,程序也能正常編譯并輸出了結果:

TestLambdaInterface test = ()->"test";

test.testInterface();

說明函數(shù)式接口與接口中定義方法的返回值無關

下面我們在接口中增加一個方法

public interface TestLambdaInterface {
    void testInterface();
    void testInterface2();
}

程序不能正常通過編譯

D:\Work\LearnJavaFX\src>javac -encoding utf-8 LambdaTest.java
LambdaTest.java:20: 錯誤: 不兼容的類型: TestLambdaInterface 不是函數(shù)接口
        TestLambdaInterface test = () -> System.out.println("Test");
                                   ^
    在 接口 TestLambdaInterface 中找到多個非覆蓋抽象方法
1 個錯誤

說明函數(shù)式接口只能有一個抽象方法

我們再來看看Java中提供的BinaryOperator接口:

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {

    public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }

    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
}

其實現(xiàn)的接口 BiFunction 的代碼如下:

@FunctionalInterface
public interface BiFunction<T, U, R> {

    R apply(T t, U u);

    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}

該接口可以使用如下方法生成實例:

BinaryOperator<Long> add = (x, y) -> x + y;

說明函數(shù)式接口與接口中是否存在 default 與 static 方法無關

以上充分說明能成為函數(shù)式接口【可以使用 Lambda 來簡化接口操作】的條件為:只定義了一個抽象方法的接口勃救。

Lambda 書寫方式


        /**
         * 對于只有一個抽象方法且返回值為void可以使用下面的方法快捷的生成接口的實例
         */
        Runnable voidFunc = () -> System.out.println("沒有參數(shù)的函數(shù)式接口");
        /**
         * 上面的聲明其實完整寫法應該為:
         *  Runnable voidFunc = () -> {System.out.println("沒有參數(shù)的函數(shù)式接口")};
         * 對于只有一行語句時治力,可以省略{}宵统,
         * 下面是多行語句的情況,{}不能省略
         */
        Runnable muitiStatement = () -> {
            System.out.println("第一行");
            System.out.println("第二行");
            //多行語句....
        };

        /**
         * 對于有接口中有參數(shù)哦的方法我們可以使用
         * ActionListener oneParam = (event) -> System.out.println("一個參數(shù)");
         * 當只有一個參數(shù)時瓢省,可以省略()如下:
         */
        ActionListener oneParam = event -> System.out.println("一個參數(shù)");

        /**
         * 多參數(shù)就必須將參數(shù)放入到()中調用痊班,按照參數(shù)順序寫入
         */
        BinaryOperator<Long> add = (x, y) -> x + y;

        /**
         * 上面幾種情況中,接口的參數(shù)在編譯時進行類型的推斷馒胆,我們亦可以顯式聲明一下【這樣可以避免很多錯誤】
         */
        BinaryOperator<Long> add2 = (Long x, Long y) -> x + y;

Lambda 表達式語法由參數(shù)列表凝果、->函數(shù)體組成。函數(shù)體既可以是一個表達式也可以是一個代碼塊型雳。

  • 表達式:表達式會被執(zhí)行然后返回結果。它簡化掉了 return 關鍵字
  • 代碼塊:顧名思義就是一坨代碼沿量,和普通方法中的語句一樣

目標類型

通過前面的例子我們可以看到冤荆,lambda 表達式?jīng)]有名字,那我們怎么知道它的類型呢佛掖?答案是通過上下文推導而來的涌庭。例如,下面的表達式的類型是 OnClickListener

ClickListener listener = (View v) -> {v.setText("lalala");};

這就意味著同樣的lambda表達式在不同的上下文里有不同的類型

Runnable runnable = () -> doSomething();  //這個表達式是 Runnable 類型的
Callback callback = () -> doSomething();  //這個表達式是 Callback 類型的

編譯器利用 lambda 表達式所在的上下文所期待的類型來推導表達式的類型拴魄,這個被期待的類型被稱為目標類型席镀。lambda 表達式只能出現(xiàn)在目標類型為函數(shù)式接口的上下文中。

Lambda 表達式的類型和目標類型的方法簽名必須一致顶捷,編譯器會對此做檢查屎篱,一個 lambda 表達式要想賦值給目標類型 T 則必須滿足下面所有的條件:

  • T 是一個函數(shù)式接口
  • lambda 表達式的參數(shù)必須和 T 的方法參數(shù)在數(shù)量交播、類型和順序上一致(一一對應)
  • lambda 表達式的返回值必須和 T 的方法的返回值一致或者是它的子類
  • lambda 表達式拋出的異常和 T 的方法的異常一致或者是它的子類

由于目標類型是知道 lambda 表達式的參數(shù)類型,所以我們沒必要把已知的類型重復一遍秦士。也就是說 lambda 表達式的參數(shù)類型可以從目標類型獲人硗痢:

//編譯器可以推導出s1和s2是String類型
Comparator<String> c = (s1, s2) -> s1.compareTo(s2);

button.setOnClickListener(v -> v.setText("lalala"));

作用域

在內部類中使用變量名和this非常容易出錯。內部類通過繼承得到的成員變量(包括來說 object 的)可能會把外部類的成員變量覆蓋掉关贵,未做限制的 this 引用會指向內部類自己而非外部類卖毁。

而 lambda 表達式的語義就十分簡單:它不會從父類中繼承任何變量,也不用引入新的作用域炭剪。lambda 表達式的參數(shù)及函數(shù)體里面的變量和它外部環(huán)境的變量具有相同的語義(this 關鍵字也是一樣)翔脱。

public class HelloLambda {

    Runnable r1 = () -> System.out.println(this);
    Runnable r2 = () -> System.out.println(toString());

    @Override
    public String toString() {
        return "Hello, lambda!";
    }

    public static void main(String[] args) {
        new HelloLambda().r1.run();  
        new HelloLambda().r2.run();
    }
}

上面的代碼最終會打印兩個 Hello, lambda!,與之相類似的內部類則會打印出類似 HelloLambda$1@32a890HelloLambda$1@6b32098 這種出乎意料的字符串错妖。

基于詞法作用域的理念疚沐,lambda表達式不可以掩蓋任何其所在上下文的局部變量。

變量

在 JDK8 對匿名內部類中調用類之外變量必須為final的限制進行了一定的放寬:

String name = "123";

Runnable run = new Runnable() {
    @Override
    public void run() {
      //在JDK8之前下面的語句是會報錯的痴施,只有name是final時才可以使用究流,但是JDK8中,這樣的語句就不會報錯
       System.out.println(name);
    }
};

雖然 JDK8 中上面的語句時可以的神得,但這只是減少了我們代碼中的編寫偷仿,編譯時還是會將 name 當做是 final 所以當有以下幾種情況時炎疆,編譯還是會報錯:

          
        //1.對name進行了第二次賦值,這樣編譯器就會認為name不是final類型
        String name = "123";
        name="";
        Runnable run = () -> {
            System.out.println(name);
        };
        
        //2.在匿名類方法中對變量進行賦值也是不允許的
        String name = "123";  
        Runnable run = () -> {
            name="";
            System.out.println(name);
        };

如果真的需要在Lambda中或者匿名中對變量賦值全跨,那么應該將其放入數(shù)組亿遂,或者當做一個類的屬性來傳遞對象final時可以設置其屬性,不能設置其引用

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末挪钓,一起剝皮案震驚了整個濱河市耳舅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌馏予,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呢岗,死亡現(xiàn)場離奇詭異后豫,居然都是意外死亡突那,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門饭豹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來务漩,“玉大人,你說我怎么就攤上這事翘悉【哟ィ” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵制市,是天一觀的道長弊予。 經(jīng)常有香客問我汉柒,道長,這世上最難降的妖魔是什么碾褂? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任正塌,我火速辦了婚禮恤溶,結果婚禮上屎鳍,老公的妹妹穿的比我還像新娘问裕。我一直安慰自己粮宛,他們只是感情好,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布忧饭。 她就那樣靜靜地躺著筷畦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吼砂。 梳的紋絲不亂的頭發(fā)上鼎文,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機與錄音周偎,去河邊找鬼撑帖。 笑死,一個胖子當著我的面吹牛蛉艾,可吹牛的內容都是我干的灶平。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼罐监,長吁一口氣:“原來是場噩夢啊……” “哼瞒爬!你這毒婦竟也來了沟堡?” 一聲冷哼從身側響起航罗,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤屁药,失蹤者是張志新(化名)和其女友劉穎酿箭,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缭嫡,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡妇蛀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了眷茁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片古程。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡挣磨,死狀恐怖,靈堂內的尸體忽然破棺而出茁裙,到底是詐尸還是另有隱情,我是刑警寧澤掉蔬,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布女轿,位于F島的核電站壕翩,受9級特大地震影響,放射性物質發(fā)生泄漏放妈。R本人自食惡果不足惜荐操,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一托启、第九天 我趴在偏房一處隱蔽的房頂上張望攘宙。 院中可真熱鬧,春花似錦肩民、人聲如沸链方。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鹏溯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肺孵,已是汗流浹背颜阐。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工凳怨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肤舞。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓李剖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親敲董。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內容