Java8新特性: lambda 表示式介紹

2022-08-02 18:01:23

 

 

 

一、lambda 表示式介紹

lambda 表示式是 Java 8 的一個新特性,可以取代大部分的匿名內部類,簡化了匿名委託的使用,讓你讓程式碼更加簡潔,優雅。

比較官方的定義是這樣的:

lambda 表示式是一個可傳遞的程式碼塊(或者匿名函數),可以在以後執行一次或多次。

這個匿名函數沒有名稱,但它有參數列、函數主體、返回型別,可能還有一個可以丟擲的異常列表。lambda 表示式也可稱為閉包

在 Java 中傳遞一個程式碼段並不容易,你不能直接傳遞程式碼段。Java 是一種物件導向語言,所以必須構造一個物件,這個物件的類需要有一個方法包含所需的程式碼。接下來就看看 Java 是怎麼來處理程式碼塊的。

 

二、lambda 表示式的語法

ava 中有一個 Comparator 介面用來排序。這是 Java 8 以前的程式碼形式:

public class LengthComparator implements Comparator<String> {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
}
String[] strArr = new String[]{"abcde", "qwer"};
Arrays.sort(strArr, new LengthComparator());

我們需要定義一個實現了 Comparator 介面的類,並實現裡面的 compare() 方法,然後把這個類當做引數傳給 sort 方法。

而我們使用 lambda 表示式就可以這樣來寫:

Arrays.sort(strArr, (String a, String b) -> a.length() - b.length());

其中的 (String a, String b) -> a.length() - b.length() 就是一個 lambda 表示式。

lambda 表示式就是一個程式碼塊,以及必須傳入程式碼的變數規範

lambda 表示式的一些例子:

// 1. 不需要引數,返回值為 5  
() -> 5  
  
// 2. 接收一個引數(數位型別),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2個引數(數位),並返回他們的差值  
(x, y) -> x – y  
  
// 4. 接收2個int型整數,返回他們的和  
(int x, int y) -> x + y  
  
// 5. 接受一個 string 物件,並在控制檯列印,不返回任何值(看起來像是返回void)  
(String s) -> System.out.print(s)

再看一個例子加深理解:

// 用匿名內部類的方式來建立執行緒 
new Thread(new Runnable() { 
    @Override 
    public void run() { 
        System.out.println("hello world"); 
    } 
});


// 使用Lambda來建立執行緒
new Thread(() -> System.out.println("hello world"));

  

注意:

如果一個 lambda 表示式只在某些分支返回一個值,而另外一些分支不返回值,這是不合法的。
例如,(int x) -> { if (x>= 0) return 1; } 就不合法

 

三、函數式介面

Java 中有很多封裝程式碼塊的介面,比如上面的 ComparatorActionListener,lambda 表示式與這些介面是相容的。

但並不是所有的介面都可以使用 lambda 表示式來實現。lambda 規定介面中只能有一個需要被實現的方法(只包含一個抽象方法),不是規定介面中只能有一個方法。 這種介面就稱為函數式介面

Java 8 中有另一個新特性:default, 被 default 修飾的方法會有預設實現,不是必須被實現的方法,所以不影響 Lambda 表示式的使用。

上面的 ComparatorActionListener,包括 Runnable 就是隻有一個需要被實現的方法的介面。即函數式介面

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used...
     */
    public abstract void run();
}

我們來觀察下 Runnable 介面,介面上面有一個註解 @FunctionalInterface

通過觀察 @FunctionalInterface 這個註解的原始碼,可以知道這個註解有以下特點:

  1. 該註解只能標記在有且僅有一個抽象方法的介面上。

  2. JDK8 介面中的靜態方法和預設方法,都不算是抽象方法。

  3. 介面預設繼承 java.lang.Object,所以如果介面顯示宣告覆蓋了 Object 中方法,那麼也不算抽象方法。

  4. 該註解不是必須的,如果一個介面符合"函數式介面"定義,那麼加不加該註解都沒有影響。加上該註解能夠更好地讓編譯器進行檢查。如果編寫的不是函數式介面,但是加上了@FunctionInterface,那麼編譯器會報錯。

我們再來看一下 Comparator 介面的原始碼:

@FunctionalInterface
public interface Comparator<T> {
    
    int compare(T o1, T o2);

    boolean equals(Object obj);

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    default Comparator<T> thenComparing(Comparator<? super T> other) {
        Objects.requireNonNull(other);
        return (Comparator<T> & Serializable) (c1, c2) -> {
            int res = compare(c1, c2);
            return (res != 0) ? res : other.compare(c1, c2);
        };
    }

    public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
        return new Comparators.NullComparator<>(true, comparator);
    }
}

  

這裡只貼出來了部分程式碼,可以看到排除掉介面的中的靜態方法、預設方法和覆蓋的 Object 中的方法之後,就剩下一個抽象方法 int compare(T o1, T o2); 符合 lambda 函數式介面的規範。

JDK 中提供一些其他的函數介面如下:

 

 

四、方法參照

Java awt 包中有一個 Timer 類,作用是經過一段時間就執行一次。 用 lambda 表示式來處理:

Timer timer = new Timer(1000, event -> System.out.println("this time is " + new Date()));

這裡面的 lambda 表示式可以這樣表示:

Timer timer = new Timer(1000, System.out::println);

表示式 System.out::println 就是一個方法參照(method reference),它指示編譯器生成一個函數式介面的範例,覆蓋這個介面的抽象方法來呼叫給定的方法。

方法參照需要用 ::運運算元分隔方法名與物件或類名。主要有3種情況:

1. object::instanceMethod
2. Class::instanceMethod
3. Class::staticMethod

具體解釋這裡不再敘述,有興趣的可以看看《Java 核心技術卷1》。

注意:

只有當 lambda 表示式的體只呼叫一個方法而不做其他操作時,才能把 lambda 表示式重寫為方法參照

 

五、構造器參照

構造器參照與方法參照很類似,只不過方法名 new。例如,Person::new 是 Person 構造器的一個參照。

假如有一個字串列表。可以把它轉換為一個 Person 物件陣列,為此要在各個字串上呼叫構造器:

ArrayList<String> names = ... ;
Stream<Persion> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());

其中,map 方法會為各個列表元素呼叫 Person(String) 構造器。這裡的 streammap 會在下一篇部落格中學習,這篇暫不討論。

六、變數作用域

看下面這個例子:

public static void repeatMessage(String text, int delay){
    ActionListener listener = event -> 
        {
            System.out.printLn(text);
        };
    new Timer(delay, listener).start();
}

// 呼叫
repeatMessage("Hello", 1000);

可以看到, lambda 表示式可以捕獲外圍作用域中變數的值。在 Java 中,要確保所捕獲的值是明確定義的,這裡有一個重要的限制。在 lambda 表示式中,只能參照值不會改變的變數。這是為了保證並行執行過程的安全。

lambda 表示式中捕獲的變數必須實際上是事實最終變數。就是這個變數初始化之後就不會再為它賦新值。

 

參考資料:
  1.《Java 核心技術卷1-2》
  2.https://juejin.cn/post/7035997996936331294