Java 8 Lambda 表示式學習心得總結

renchx發表於2015-11-22

lambda表示式,是一段可以傳遞的程式碼,可以被多次執行。在 java8 之前,如果我們想寫一個簡單的比較器 Compartor ,我們需要建立一個實現類或者一個匿名內部類類傳入到需要比較的方法內當中。

在 java8 之前傳遞一段程式碼不是很容易,現在我們想要實現一個通過傳遞程式碼來檢查某個字串的長度是否小於另外一個字串的長度。

(String first, String second) -> Integer.compare(first.length(), second.length());

上面這段程式碼就是 lambda 表示式,這個表示式不僅僅是一個簡單的程式碼塊,還必須指定傳遞給程式碼的所有變數。

Java 當中 lambda 表示式的格式是:引數、箭頭(->)、以及一個表示式。如果負責計算的程式碼無法用一個表示式表示,可以使用 {} 括起來。

如果 lambda 沒有引數,可以使用 () 來表示,如果 lambda 表示式的引數型別可以被推導,那麼可以省略掉。

Comparator<String> comparator = (first, second) -> Integer.compare( first.length(), second.length());

上面的例子當中會推匯出 first 和 second 的型別是 String ,因為表示式賦值給了一個字串比較器。

注意,在 lambda 表示式當中只在某些分支有返回值是不合法的。

函式式介面

Java 當中有許多介面都需要封裝程式碼塊, Runnable 、 Compartor 等等。

對於只包含一個方法的介面,可以通過 lambda 表示式來建立該介面的物件,這種介面被稱為函式式介面。

在 java.util.function 包下面提供了許多通用的函式式介面。

可以在任意函式式介面上面使用 @FunctionalInterface 來標識它是一個函式式介面,但是該註解不是強制的。

當 lambda 表示式被轉換成一個函式式介面的例項時,需要注意處理檢查時異常,如下程式碼。

Runnable runnable = () -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

如果不加 try catch 語句的話,這個賦值語句就會編譯錯誤,因為 Runnable 的 run 方法是沒有異常丟擲的。

Callable 是可以丟擲任何異常,並且有返回值,但是我們不想返回任何資料的時候可以如下定義:

Callable<Void> callable = () -> {
            System.out.println("xxx");
            return null;
        };

方法引用

有時候,我們想傳遞的程式碼已經有現成的實現了。例如,我們僅僅想點選按鈕時候列印 event 物件,可以進行如下程式碼:

button.setOnAction(System.out::println);

表示式 System.out::println 是一個方法引用,等同於 lambda 表示式 x -> System.out.println(x);

:: 操作符將方法名和物件或類的名字分隔開。有以下三種主要的使用情況:

  • 物件 :: 例項方法
  • 類 :: 靜態方法
  • 類 :: 例項方法

前兩種情況,方法引用相當於提供方法引數的 lambda 表示式。 System.out::println 等同於 x -> System.out.println(x);

Math::pow 等同於 (x, y) -> Math.pow(x, y);

第三種情況,第一個引數會成為執行方法的物件。例如, Sting::compareToIgnoreCase 等同於 (x, y) -> x.compareToIgnoreCase(y);

Comparator<String> comparator = String::compareTo;

當然還可以捕獲 this 指標,this :: equals 相當於 x -> this.equals(x);

構造器引用

構造器引用與方法引類似,不同的是構造器引用使用的方法名是 new。例如,Buttton::new。

List<String> strings = new ArrayList<String>();
strings.add("a");
strings.add("b");
strings.add("c");
Stream<Button> stream = strings.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());

先不詳細介紹 stream map collect 方法,主要看對於每個列表元素會呼叫 Button 的構造方法。雖然 Button 有多個構造器,但是會選擇只有一個 String 引數的構造器。

陣列型別的構造器引用,int[]::new 是一個含有一個引數的構造器引用,這個引數就是陣列的長度,相當於 x -> new int[x]。

變數作用域

有如下程式碼:

public static void repeat(String string, int count) {
        Runnable runnable = () -> {
            for (int i = 0; i < count; i++) {
                System.out.println(this.toString());
                Thread.yield();
            }
        };
        new Thread(runnable).start();
    }

上面這段程式碼的兩個引數沒有設定成 final 的,這在 JDK7 之前是會編譯錯誤的,同樣在 java8 當中匿名內部類訪問外部也不需要 final 來修飾。

分析下上面的程式碼,由於有 Thread.yield 所以可能其他執行緒佔用 CPU 先執行,然後方法 repeat() 先反回了,才執行 runnable,那麼這個時候 string 和 count 這 2 個引數怎麼辦?

首先一個 lambda 表示式需要有三個部分:

  • 一段程式碼
  • 引數
  • 自由變數的值,這裡的“自由”指的是那些不是傳入表示式的引數並且沒有在程式碼中定義的變數。

上面的那個例子當中有兩個自由變數,string 和 count,lambad 表示式必須存放這兩個變數的值。並且含有自由變數的程式碼塊被稱為閉包

在 lambda 表示式當中被引用的變數的值不可以被更改,編譯器會檢查修改操作:

public void repeat(String string, int count) {
        Runnable runnable = () -> {
            for (int i = 0; i < count; i++) {
                string = string + "a";//編譯出錯
                System.out.println(this.toString());
            }
        };
        new Thread(runnable).start();
    }

在 lambda 表示式當中不允許宣告一個與區域性變數同名的引數或者區域性變數。

String first = "";
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(),//編譯會出錯
                second.length());

lambda 表示式中使用 this 會引用建立該 lambda 表示式的方法的 this 引數,

public class Testmain2 {
    public static void main(String[] args) {
        Testmain2 testmain2 = new Testmain2();
        testmain2.method();
    }

    @Override
    public String toString() {
        return "aaaa";
    }

    public void method() {
        Runnable runnable = () -> {
            System.out.println(this.toString());
        };
        new Thread(runnable).start();
    }
}

上面的例子執行後會輸出:aaaa。

預設方法

在集合庫當中提供了一些函式表示式,例如 forEach 方法:

list.forEach(System.out::println);

由於集合的介面是之前定義的,新新增一個 forEach 方法會導致老的程式碼不相容,但是 java8 當中是給介面設計成可以包含具體實現的預設方法來解決這個問題。

public interface Person {
    long getID();

    default String getName() {
        return "name";
    }
}

如果要實現 Person 介面,那麼必須實現 getID 方法,getName 方法可以不實現。

如果一個介面定義了一個預設方法,而另外一個父類中又定義了同名的方法,那麼如何選擇?有以下規則:

  • 選擇父類中的方法,如果父類提供了具體的實現方式,那麼介面中具有相同名稱和引數的預設方法會被忽略。
  • 介面衝突,如果需要實現兩個介面,並且這兩個介面有兩個相同簽名的預設方法,那麼子類就需要覆蓋重寫這個方法。

如果一個子類繼承了一個父類,並且實現了一個介面,並且父類和介面有相同簽名的預設方法,那麼之類繼承父類當中的實現,類優先可以保持 java7 的相容性。

注意,不能為 Object 中的方法重新定義個預設方法。

介面中的靜態方法

java8 可以在介面當中新增靜態方法,便於把一些工具方法加入到介面當中,所以類似一些, Collections 和 Paths 類比較尷尬。

相關文章