java8 Lambda 表達式詳解

1.前言

在java8 以前,若我們想要把某些功能傳遞給某些方法,總要去寫匿名類。以前注冊事件監聽器的寫法與下面的示例代碼就很像:

        manager.addScheduleListener(new ScheduleListener() {
              @Override
              public void onSchedule(ScheduleEvent e) {
                 //Event listener implementation goes here...
              }
        });

我們添加了一些自定義代碼到Schedule監聽器中,需要先定義匿名內部類,然后傳遞一些功能到onSchedule方法中。

正是java在作為參數傳遞普通方法或功能的限制,java8增加了一個全新語言級別的功能,稱為lambda表達式。


2.為什么java需要lambda表達式

java是面向對象語言,除了原始數據類型之外,java中所有內容都是一個對象。而在函數式語言中,我們只需要給函數分配變量,并將這個函數作為參數傳遞給其他函數就可以實現特定的功能。javaScript就是函數式編程語言的典范(閉包)。

lambda表達式的加入,使得java擁有了函數式編程的能力。在其他語言中,lambda表達式的類型是一個函數;但在java中,lambda表達式被表示為對象,因此他們必須綁定到被稱為功能接口的特定對象類型。


3.lambda 表達式簡介

lambda表達式是一個匿名函數(對于java而言并不是很準確,但是這里我們不糾結這個問題)。簡單來說,這是一種沒有聲明的方法,即沒有訪問修飾符,返回值聲明和名稱。

在僅使用一次方法的地方特別有用,方法定義很短。它為我們節省了,如包含類聲明和編寫單獨方法的工作。

java中的lambda表達式通常使用語法是(argument) ->(body) 比如:

(arg1,arg2 ...) -> { body }
(type1 arg1,type2 arg2 ...) ->{ body }

以下是lambda表達式的一些示例

  (int a,int b)  -> {return a+b}
  () -> System.out.println("Hello World");
  (String s) -> {System.out.println(s);}
  () -> 42
  () -> {return 3.1415}

3.1 lambda 表達式的結構

lambda表達式的結構:

  • Lambda表達式可以具有零個,一個或多個參數。
  • 可以顯示聲明參數的類型,也可以由編譯器自動從上下文推斷參數的類型。例如(int a)也可以寫作(a)
  • 參數用小括號括起來,用逗號分隔。例如(a,b)(int a,int b)或(String a,int b,float c)
  • 空括號用于表示一組空的參數。例如() -> 42
  • 當有且僅有一個參數時,如果不顯示的指明類型,則不必使用小括號。例如 a -> return a*a
  • lambda表達式的正文可以包含零條,一條或多條語句。
  • 如果lambda表達式的正文只有一條語句,則大括號可不用寫,且表達式的返回值類型要與匿名函數的返回類型相同。
  • 如果lambda表達式的正文有一條以上的語句必須包含在大括號(代碼塊)中,且表達式的返回值類型要與匿名函數的返回值類型相同。

4.方法引用

4.1 從lambda 表達式到雙冒號操作符

例如,要穿件一個比較器,一下語法就夠了

Comparator c = (Person p1,Person p2) -> p1.getAge().compareTp(p2.getAge());

然后,使用類型推斷:

Comparator c = (p1,p2) -> p1.getAge().compareTo(p2.getAge());

我們可以使上面的代碼更具表現力和可讀性,我們來看一下:

Comparator c = Comparator.comparing(Person::getAge);

使用::運算符作為lambda調用特定方法的縮寫,并且擁有更好的可讀性。


4.2 使用方式

雙冒號(::)操作符是java中的方法引用。當使用一個方法的引用時,目標引用放在::之前,目標引用提供的方法名稱在::之后,即目標引用::方法。比如:

Person::getAge;

//獲取getAge方法的 Function 對象
Fuction<Person,Integer> getAge = Person::getAge
//傳參數調用getAge 方法
Integer age = getAge.apply(p);

我們引用getAge,然后將其應用于正確的參數。

目標引用的參數類型是Function<T,R> T表示傳入類型,R表示返回類型。比如 表達式 person -> person.getAge();,傳入參數是person,返回值是person.getAge(),那么方法引用Person::getAge 就對應著Function<Person,Integer>類型。


5.什么是功能接口(Functional interface)

在Java中,功能接口(Functional interface)指只有一個抽象方法的接口。

java.lang.Runnable是一個功能接口,在Runnable中只有一個方法的聲明void run()。我們使用匿名內部類實例化功能接口的對象。而使用lambda表達式,可以簡化寫法。

每個lambda表達式都可以隱式地分配給功能接口,例如,我們可以從lambda表達式創建Runnable接口的引用,如下所示。

Runnable r = () -> System.out.println("hello world");

當我們不指定功能接口時,這種類型的轉換會被編譯器自動處理,例如:

new Thread(
    () -> System.out.println("hello world")
).start();

在上面的代碼中,編譯器會自動推斷,lambda表達式可以從Thread類的構造函數簽名(public Thread(Runnable r) {})轉換為Runnable接口。

@FunctionalInterface是在java8 中添加的一個新注解,用于指示接口類型,聲明接口為java語言規范定義的功能接口。java8還聲明了lambda表達式可以使用的功能接口的數量。當您注釋的接口不是有效的功能接口時,@FunctionalInterface 會產生編譯器級錯誤。
以下是自定義功能接口的示例:

@FunctionalInterface
public interface WorkerInterface {
        public void doSomeWork();
}

正如其定義所述,功能接口只能有一個抽象方法。如果我們嘗試在其中添加一個抽象方法,則會拋出編譯時錯誤。例如:

@FunctionalInterface
public interface WorkerInterface{
      public void doWork();
      public void doMoreWork();
}
錯誤:意外的 @FunctionalInterface 注釋,WorkerInterface 不是函數接口,
      WorkerInterface 中找到多個非覆蓋抽象方法

一旦定義了功能接口,我們就可以利用lambda表達式調用。例如:

WorkerInterface work = () -> System.out.println("通過lambda表達式調用");
work.doWork();

6.lambda表達式的例子

6.1 線程初始化

new Thread(
      () -> System.out.println("hello world")
).start();

6.2 事件處理

事件處理可以用java8 使用lambda表達式來完成。以下代碼顯示了將ActionListener 添加到UI組件的新舊方式:

// 舊
button.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
            System.out.print("hello world");
});

//新
button.addActionListener( (e) ->
      System.out.println("hello world");
});

6.3 遍歷輸出(方法引用)

輸出給定數組的所有元素的簡單代碼。請注意,還有一種使用lambda表達式的方式。

//old way
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for( Integer i : list) {
      System.out.println(i);
}

//使用 -> 的lambda 表達式
list.forEach(n -> System.out.println(n));

//使用::的lambda 表達式
list.forEach(System.out::println)

6.4邏輯操作

輸出通過邏輯判斷的數據

public static void main(String args[]) {
    List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
    System.out.print("輸出所有數字:");
    evaluate(list,(n) -> true);

    System.out.print("不輸出:");
    evaluate(list,(n) -> true);

    System.out.print("輸出偶數:");
    evaluate(list,(n) -> true);

    System.out.print("輸出奇數:");
    evaluate(list,(n) -> true);

    System.out.print("輸出大于5的數字:");
    evaluate(list,(n) -> true);
}

public static void evaluate(List<Integer> list,Predicate<Integer> predicate){
    for( Integer n : list) {
        if(predicate.test(n)) {
        System.out.print( n + " ");
        }
    }
    System.out.println();
}    

6.4 Stream API 示例

java.util.stream.Stream 接口和lambda 表達式一樣,都是java8 新引入的。所有Stream的操作必須以lambda表達式為參數。Stream接口中帶有大量有用的方法,比如map() 的左右就是將input Stream的每個元素,映射成output Stream的另外一個元素。
下面的例子,我們將lambda 表達式 x -> x * x 傳遞給map()方法,將其應用于流的所有元素。之后,我們使用forEach打印列表的所有元素。

//old way
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer i : list) {
    int x = i * i;
    System.out.println(x);
}

//new way
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map(n -> n * n).forEach(System.out::println)

7.lambda 表達式和匿名類之間的區別

  • this關鍵字:對于匿名類this關鍵字解析為匿名類,而對于lambda表達式,this關鍵字解析為包含寫入lambda的類。
  • 編譯方式:java編譯器編譯Lambda表達式時,會將其轉換為類的私有方法,再進行動態綁定。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容