1. 程式人生 > 其它 >Lambda-讓人又愛又恨的“->"

Lambda-讓人又愛又恨的“->"

聊到Java8新特性,我們第一反應想到的肯定是Lambda表示式和函式式介面的出現。要說ta到底有沒有在一定程度上“優化”了程式碼的簡潔性呢?抑或是ta在一定程度上給程式設計師增加了閱讀和debug的難度,讓不少程式設計師頭疼。這期來接著“聊聊Java”,新特性篇之**又愛又恨的Lambda**。

寫在前邊

  • 聊到Java8新特性,我們第一反應想到的肯定是Lambda表示式和函式式介面的出現。要說ta到底有沒有在一定程度上“優化”了程式碼的簡潔性呢?抑或是ta在一定程度上給程式設計師增加了閱讀和debug的難度,讓不少程式設計師頭疼。這期來接著“聊聊Java”,新特性篇只又愛又恨的Lambda

Lambda表示式

實質屬於函數語言程式設計的概念,可返回一個介面的實現

執行緒中的應用

傳統方式

建立一個一次性的類

//一次性的類,用在new Thread中充當Runnable對的實現類
class runnable implements Runnable{

    @Override
    public void run() {
        System.out.println("我在路上");
    }
}

public class lambdaTest {
    public static void main(String[] args) {
        runnable runnable = new runnable();
        Thread thread1 = new Thread(runnable);
    }
}

(稍微優化)匿名內部類

Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("我在路上");
            }
        });

作用

  • 避免匿名內部類定義過多
  • 使程式碼看起來簡潔
  • 簡化程式碼,只留下核心邏輯

函式式介面

定義:任何介面,如果只包含唯一一個抽象方法,那麼它就是一個函式式介面

public interface Runnable{
    void run();
}

為了避免後來人給這個介面新增函式後,導致該介面有多個函式,不再是函式式介面,我們可以在介面類的上方宣告@FunctionalInterface

所有的Lambda的型別都是一個介面

  • 而Lambda表示式本身,就是這個介面的實現

如果定義成實現類,就會報錯

  • 我們在程式編譯的時候並不知道我們最終創建出來的物件具體是哪個子類,直到執行時期才能得知,隨之我們可以讓這個like指向指向各種不同的類上,可以呼叫各種不同的子類方法,大大提高了程式的可擴充套件性

而這裡我們用lambda實際上是等價於匿名內部類(沒有類名),實際創建出來的類是什麼,我們不知道,所以我們會定義成介面,利用多型的向上轉型特性
關於多型的更多特性,在我的另一篇部落格中 : 傳送門->

多型


方法引用

Demo

 //介面定義
    interface parseIntNum{
        //定義一個String轉化成Integer的方法
        int pass(String s);
    }
    public static void main(String[] args) {
        parseIntNum parseIntNum1;
        parseIntNum parseIntNum2;
        //原始lambda
        parseIntNum1 = (str)-> Integer.parseInt(str);
        System.out.println(parseIntNum1.pass("1"));
        //方法引用改進版本
        parseIntNum2 = Integer::parseInt;
        System.out.println(parseIntNum2.pass("1"));
    }


所謂方法引用,是指如果某個已經存在的方法,他的簽名接口裡邊定義的函式恰好一致,就可以直接傳入方法引用。
因為parseIntNum介面定義的方法是int pass(String s),和Integer中的靜態方法int parseInt(String s)相比,除了方法名外,方法引數一致,返回型別相同,這就是我們說的方法簽名一致,可以直接傳入方法引用

方法引用的寫法

  • 其實很簡單,只需要使用操作符雙冒號** "::"**

常見的方法引用

常見的引用形式

類名::靜態方法名

呼叫類的靜態方法

  • 其實我們上邊使用Integer::parseInt 就等價於呼叫 Integer的靜態方法 parseInt

物件:例項方法

此處小小中二了一點hhhh

class Naruto{
    public static void Rasengan(){
        System.out.println("螺旋丸");
    }
}
//介面定義
interface Ninja{
    //定義一個奧義方法
    void aoyi();
}
public class methodQuote {
    //通過引用Naurto的螺旋丸
    Ninja ninja=Naruto::Rasengan;
    //再發動奧義
    ninjia.aoyi();
}

資料型別:new

 public static void main(String[] args) {
     //方式一
        IntFunction<int []> arr1 = new IntFunction<int[]>() {
            @Override
            public int[] apply(int num) {
                return new int[num];
            }
        };
        arr1.apply(10);
     //方式二(方法引用)
        IntFunction<int []> arr2 = int[]::new;
        arr2.apply(10);

    }
  • 除此之外還有很多種形式,這裡就不過多贅述了

開發常用

常用小技巧:遍歷陣列列印

對比三種方法,可以看出簡潔程度

ArrayList<Integer> arrayList=new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
//匿名內部類
        arrayList.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(integer);
            }
        });
//lambda優化
		arrayList.forEach((integer)-> System.out.println(integer));	
//方法引用列印,用方法引用替代了我們的匿名內部類(相當於替代了lambda)
        arrayList.forEach(System.out::println);

遍歷Map

		Map<Integer, String> map = new HashMap<>();
        map.forEach((k,v)->System.out.println(v));

Lambda表示式作用域

訪問區域性普通變數

  • 只能引用標記了final的外層區域性變數

即 : 不能在lambda 內部修改定義在域外的區域性變數,否則會編譯錯誤。

  • 特殊情況下,區域性變數也可以不用宣告為 final,但是必須不可被後面的程式碼修改(即隱性的具有 final 的語義)

破壞隱式final(再次修改)

package com.melo.notes.lambdaTest;

public class TestFinal {
    interface MeiYan{
        Integer change(String str);
    }

    public static void main(String[] args) {
        //定義區域性變數
        String temp = "222";
        //寫成一行可以不用return
        MeiYan meiYan = (str -> Integer.valueOf(str+temp));
        //再次修改,不符合隱式final定義
        temp = "333";
        Integer str =meiYan.change("111") ;
        System.out.println(str);
    }
}
  • 不允許宣告一個與區域性變數同名的引數或者區域性變數。

訪問區域性引用變數

import java.util.ArrayList;
public class TestArray {

    interface MeiYan{
        Integer change();
    }
    void testArrayList(){
        ArrayList<String> list = new ArrayList<>();
        list.add("111");
        //訪問外部引用區域性引用變數
        MeiYan meiYan = (() -> Integer.valueOf(list.get(0)));
        //修改區域性引用變數
        list.set(0,"222");
        Integer str =meiYan.change();
        System.out.println(str);
    }
    public static void main(String[] args) {
        new TestArray().testArrayList();
    }
}
  • 訪問引用變數的話,是沒有問題的。因為lambda可以感知到外部對該引用變數的改變,不會出現資料不同步問題

具體可以看下邊的"理解"有更詳細的解釋

訪問靜態變數和例項變數

都是可以的,再次修改也不會報錯

程式碼

public class TestFinal {
    //靜態變數
    static String StaticTemp;
    //例項變數
    String instanceTemp;

    interface MeiYan{
        Integer change(String str);
    }
    void testStatic(){
        StaticTemp="222";
        MeiYan meiYan = (str -> Integer.valueOf(str+StaticTemp));
        StaticTemp="333";
        Integer str =meiYan.change("111") ;
        System.out.println(str);
    }
    public static void main(String[] args) {
        new TestFinal().testStatic();
    }

理解

例項變數和區域性變數的區別

這裡的關鍵問題轉換到: 例項變數和區域性變數的區別是什麼?

  • 例項變數儲存在

堆是線上程之間共享的

  • 而區域性變數儲存在

有可能會有相應的執行緒問題(見下)

執行緒問題

比如A執行緒分配了一個變數temp

  • 有可能Lambda是在另一個執行緒B中使用的,使用Lambda的執行緒B,可能會在分配該變數的執行緒A將temp變數收回之後,還去訪問temp變數。

資料不同步問題

聯想一下我們普通的方法,方法的引數只是存活在方法棧這個空間裡,而我們lambda的{ }裡實際上也相當於一個方法塊。

  • 如果我們這裡的方法塊訪問了外部的變數,而這個變數只是一個普通資料型別的話,相當於只是訪問到了一份副本。當外部對這個變數進行修改時,lambda內部(只有副本)是無法感知到這個變數的修改的。

因此為了防止出現資料不同步的問題,java8就限制了:lambda訪問區域性普通資料型別變數時,需要用final修飾或使用隱式final的方法!

擴充套件--值傳遞還是引用傳遞?

  • 這裡只是提到了,對於普通資料型別的區域性變數會有限制,而對於引用型別的區域性變數呢?這就涉及到了Java的引數傳值究竟是值傳遞還是引用傳遞了。

先佔個坑位,以後還會在本專欄中更新一篇關於這方面的部落格!!!

疑問解決

一開始有個疑問,看起來方法引用省略了引數,那我們Intger.parseInt是去對誰操作?

  • 其實是自己混淆了lambda,lambda定義的時候那個引數,根本不是實際的引數

可以說那個引數,只是為方法體服務的,只是方法體裡邊會用到.
而我們都用了方法引用了,前提就是引數和返回值一樣,方法體也是我們想要實現的內容,這時自然而然都不用我們寫方法體了,那方法體所依賴的引數也自然不用派上用場了

寫在最後

  • 最近要開始忙活專案了,對於這些基礎知識的更深入掌握,也許會放在以後準備面試的時候,目前理解不深,如有錯誤之處還望指出!

本專欄還會斷斷續續更新一些Java基礎知識和麵經,提前為以後面試打下基礎!