lambda
表示式是 Java 8 的一個新特性,可以取代大部分的匿名內部類,簡化了匿名委託的使用,讓你讓程式碼更加簡潔,優雅。
比較官方的定義是這樣的:
lambda 表示式是一個可傳遞的程式碼塊(或者匿名函數),可以在以後執行一次或多次。
這個匿名函數沒有名稱,但它有參數列、函數主體、返回型別,可能還有一個可以丟擲的異常列表。lambda
表示式也可稱為閉包
。
在 Java 中傳遞一個程式碼段並不容易,你不能直接傳遞程式碼段。Java 是一種物件導向語言,所以必須構造一個物件,這個物件的類需要有一個方法包含所需的程式碼。接下來就看看 Java 是怎麼來處理程式碼塊的。
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 中有很多封裝程式碼塊的介面,比如上面的 Comparator
或 ActionListener
,lambda 表示式與這些介面是相容的。
但並不是所有的介面都可以使用 lambda 表示式來實現。lambda 規定介面中只能有一個需要被實現的方法(只包含一個抽象方法),不是規定介面中只能有一個方法。 這種介面就稱為函數式介面。
Java 8 中有另一個新特性:default, 被 default 修飾的方法會有預設實現,不是必須被實現的方法,所以不影響 Lambda 表示式的使用。
上面的 Comparator
和 ActionListener
,包括 Runnable
就是隻有一個需要被實現的方法的介面。即函數式介面。
@FunctionalInterface public interface Runnable { /** * When an object implementing interface <code>Runnable</code> is used... */ public abstract void run(); }
我們來觀察下 Runnable
介面,介面上面有一個註解 @FunctionalInterface
。
通過觀察 @FunctionalInterface
這個註解的原始碼,可以知道這個註解有以下特點:
該註解只能標記在有且僅有一個抽象方法的介面上。
JDK8 介面中的靜態方法和預設方法,都不算是抽象方法。
介面預設繼承 java.lang.Object,所以如果介面顯示宣告覆蓋了 Object 中方法,那麼也不算抽象方法。
該註解不是必須的,如果一個介面符合"函數式介面"定義,那麼加不加該註解都沒有影響。加上該註解能夠更好地讓編譯器進行檢查。如果編寫的不是函數式介面,但是加上了@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) 構造器。這裡的 stream
和 map
會在下一篇部落格中學習,這篇暫不討論。
看下面這個例子:
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 表示式中捕獲的變數必須實際上是事實最終變數。就是這個變數初始化之後就不會再為它賦新值。