Lambda對比匿名內部類,Lambda是什麼,Lambda該怎麼用,Lambda使用過程中有什麼需要注意的?

ZhaoSimonone發表於2020-12-03

由匿名內部類到Lambda

看看遍歷一個List的不同方法

public class Main {
  public static List<String> myList;
  static {
    myList = new ArrayList<String>();
    myList.add("AAA");
    myList.add("BBB");
    myList.add("CCC");
    myList.add("DDD");
  }
  public static void main(String[] args) {
    
    //1.直接進行遍歷
    for (int i = 0; i < myList.size(); ++i) {
      System.out.println("myList: " + myList.get(i));
    }
      
    for (String tmp : myList) {
      System.out.println("myList: " + tmp);
    }
      
	/**************************************************************/
      
    //2.使用匿名內部類
    myList.forEach(new Consumer<String>() {
      @Override
      public void accept(String tmp) {
        System.out.println("myList: " + tmp);
      }
    });
     
    /**************************************************************/

    //3.使用Lambda
    //lambda完整版:(引數) -> {程式碼塊}
    myList.forEach((String s) -> System.out.println("myList: " + s));
    //由於myList裡面的引數肯定是String型別的,所以可以不用申明s為String型別的
    myList.forEach((s) -> System.out.println("myList: " + s));
    //由於只有一個引數,所以還可以不用加括號
    myList.forEach(s -> System.out.println("myList: " + s));
    //由於該引數其實是來自於myList,還可以這麼寫
    myList.forEach(System.out::println);
  }
}

首先,先看看forEach方法。

通過層層繼承,List獲得了Iterable介面中的forEach方法。

//List介面繼承了Collection介面
public interface List<E> extends Collection<E> {
    ......
}

//Collection介面繼承了Iterable介面
public interface Collection<E> extends Iterable<E> {
    ......
}

//在Iterable介面中定義了forEach的預設實現
public interface Iterable<T> {
    ......
	default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

分析forEach方法

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

forEach方法傳入的是一個Consumer型別的引數,Consumer是一個介面,介面不能直接例項化,但是介面的引用可以指向實現它的某個子類。也就是說,這裡傳入的引數實際上是一個繼承了Consumer介面的子類的例項,並且這個子類一定實現了Consumer中的抽象方法

Consumer是Java1.8新增的一個介面,裡面只有一個抽象方法accept()以及一個擁有預設實現的方法。

@FunctionalInterface
public interface Consumer<T> {
    
    void accept(T t);
    
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

再回到forEach方法,在forEach方法中遍歷List,並通過action物件來呼叫accept方法,action是一個例項,該例項對應的類實現了accept方法,而我們所要做的事情其實就是實現accept方法,在accept方法中指定在遍歷List的過程中,我們究竟要做些什麼操作。

for (T t : this) {
    action.accept(t);
}

回顧一下,這三種遍歷myList的不同方法。

  • 方法一是直接進行遍歷,在遍歷中操作myList物件。相當於是在方法中直接操作物件。

  • 方法二是使用匿名內部類建立一個Consumer型別的例項,然後在例項中實現accept方法,最後再將這個例項作為一個引數傳入forEach方法中。

我們習慣的程式設計思想是:如果我們要操作某個物件,我們會在程式碼塊裡直接操作,然後將其封裝成一個方法,然後呼叫該方法即可完成對該物件的操作

第二種遍歷的方法則是一種截然不同的思路。如果我們要操作某個物件,首先會在物件內部預留一個“空的方法”,這個“空的方法”就像是一個佔位符一樣,在該方法內部並沒有指定物件具體需要做什麼,當我們想對該物件進行操作的時候,就可以將相應的程式碼塊傳入,填在這個"空的方法"裡,然後物件呼叫這個方法就可以完成具體的操作了

可以對比這張圖來區分這兩種不同的程式設計思想。
在這裡插入圖片描述

在遍歷myList的第二種方法裡,我們使用了匿名內部類傳入了一個物件,在該物件內實現了accept方法,accept方法內的程式碼就是我們希望在遍歷myList的時候所要完成的操作但是實際上,我們根本不需要這個物件,我們所需要的僅僅只是這個物件裡的方法。但是Java是物件導向程式設計的,類才是第一等公民,任何操作都得依靠類來完成,我們不能直接呼叫方法,我們必須建立一個物件,然後通過Object.method()的形式來呼叫方法,如果是靜態方法,我們同樣也需要類名.method()的形式來進行呼叫。由於方法不能單獨存在,方法必須封裝在一個物件裡,因此我們要想往forEach方法中傳入程式碼塊,就必須得將方法封裝在一個物件裡。儘管這樣看起來有些臃腫,但是在Java裡卻不得不這麼做。

針對這樣的問題,我們再來看遍歷myList的第三種方法,這便是Lambda,Lambda是一種程式設計思想——函數語言程式設計。在函數語言程式設計的語言中,函式才是第一等公民,函式可以單獨存在,函式可以當作引數傳遞,也可以當作返回值。

//2.使用匿名內部類
myList.forEach(new Consumer<String>() {
    @Override
    public void accept(String tmp) {
        System.out.println("myList: " + tmp);
    }
});

//3.使用Lambda
//lambda完整版:(引數) -> {程式碼塊}
myList.forEach(s -> System.out.println("myList: " + s));

對比使用匿名內部類和Lambda表示式,Lambda表示式顯然更加簡潔,我們不需要建立一個物件,然後將方法封裝在物件裡來進行傳遞,我們可以直接將要執行的程式碼塊當作引數傳入。

Lambda的使用

單個引數的情況

還是以myList的遍歷為例

這裡的s其實就是從myList中依次取出的值,由於我們定義myList的時候就指定了它裡面所儲存的資料型別:List<String> myList。所以這個引數s就不需要定義它的型別,它一定和myList中儲存的引數的資料型別一樣。

String outside = "myList:";
myList.forEach((s) -> System.out.println(outside + s));

進一步,我們可以將 System.out.println("myList: " + s)封裝在一個方法裡,假如我們封裝在了public void process(String str)這個方法裡,且該方法與myList.forEach位於同一個類裡,則可以這麼呼叫:

String outside = "myList:";
myList.forEach(s -> process(outside + s));

在上面的例子中,我們將外部的變數outside傳入了Lambda表示式中,如果我們的process方法中不需要外部的變數,所需要的引數僅僅來自於myList中。我們還可以這麼簡寫

  1. 假設public void process(String str)位於LambdaTest這個類中,則可以這麼呼叫。
LambdaTest test = new LambdaTest();
myList.forEach(test::process);
  1. 若process是靜態方法,還能這麼呼叫
myList.forEach(LambdaTest::process);

兩個引數的情況

Map<String, String> myMap = new HashMap<>();
myMap.put("k1", "v1");
myMap.put("k2", "v2");
myMap.put("k3", "v3");
// 兩個引數也沒問題,把引數用括號擴起來,用逗號分開
myMap.forEach((k, v) -> processTwo(k, v));
// 省略也沒問題,這裡假設processTwo為LambdaTest中的一個靜態方法
myMap.forEach(LambdaTest::processTwo);

使用Stream進行流式處理

以上面的myList為例。

myList.stream().filter(s -> s.length() > 4).map(String::toUpperCase).forEach(System.out::println);

上面這段程式碼的意思就是:取出myList中的每一個元素,過濾掉length > 4的元素,然後將過濾後的元素轉化為全大寫,然後依次輸出每一個。這一氣呵成的處理便是Stream,就像是水流動一樣。

collect的作用則是將元素又收集起來,轉化為一個Collection集合。

List<String> longgerStrList = myList.stream().filter(s -> s.length() > 4)
    .map(String::toUpperCase).collect(Collectors.toList());

使用Lambda需要注意的點

  • Lambda可以有返回值和異常,得具體看對應的介面中的抽象方法有沒有返回值或者是有沒有丟擲異常。對應到myList.forEach,裡面所需要實現的方法其實是Consumer<T>介面中的void accept(T t);方法,該方法並沒有返回值,因此Lambda的表示式中就不能有表示式。該方法的簽名中也沒丟擲異常,因此不需要丟擲異常。
  • lambda 可以取代只有一個抽象方法的介面,因為在使用Lambda的時候,我們只管往裡面傳入要執行的程式碼,並沒有指定這段程式碼是要覆蓋介面中的哪個方法,如果介面中有多個抽象方法,則究竟覆蓋誰就無法抉擇了,因此只能在有一個抽象方法的介面中使用。

相關文章