Java8新特性: lambda 表達式介紹
一、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 中有很多封裝程式碼塊的介面,比如上面的 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 表達式中捕獲的變數必須實際上是事實最終變數。就是這個變數初始化之後就不會再為它賦新值。
參考資料: