Lambda表達式的簡單介紹
前言
Java 是一流的面向對象語言,除了部分簡單資料類型,Java 中的一切都是對象,即使數組也是一種對象,每個類建立的執行個體也是對象。在 Java 中定義的函數或方法不可能完全獨立,也不能将方法作為參數或傳回一個方法給執行個體。
我們總是通過匿名類給方法傳遞函數功能,以下是舊版的事件監聽代碼
someObject.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
//Event listener implementation goes here...
}
});
為了給 Mouse 監聽器添加自定義代碼,我們定義了一個匿名内部類 MouseAdapter 并建立了它的對象,通過這種方式,我們将一些函數功能傳給 addMouseListener 方法。
匿名類型最大的問題就在于其備援的文法。有人戲稱匿名類型導緻了“高度問題”(height problem):比如前面
MouseAdapter
的例子裡的五行代碼中僅有一行在做實際工作。
為什麼 Java 需要 Lambda 表達式?
在函數式語言中,我們隻需要給函數配置設定變量,并将這個函數作為參數傳遞給其它函數就可實作特定的功能。而java如前言中所述,不能直接将方法當作一個參數傳遞。同時匿名内部類又存在諸多不便:文法過于備援,匿名類中的
this
和變量名容易使人産生誤解,類型載入和執行個體建立語義不夠靈活,無法捕獲非
final
的局部變量等。
Lambda 表達式的出現為 Java 添加了缺失的函數式程式設計特點,使我們能将函數當做一等公民看待。
先說說函數式接口
我們将隻包含一個抽象方法聲明的接口稱為函數式接口。(之前它們被稱為SAM類型,即單抽象方法類型(Single Abstract Method))。Java SE7中就已存在函數式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.beans.PropertyChangeListener
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
@FunctionalInterface注解來顯式指定一個接口是函數式接口(以避免無意聲明了一個符合函數式标準的接口),加上這個注解之後,編譯器就會驗證該接口是否滿足函數式接口的要求。Java SE 8中增加了一個新的包
java.util.function
,它裡面包含了很多常用的函數式接口。
下面是stream流中常用到的一些新增的函數式接口:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Lambda 表達式
Lambda 表達式是一種匿名函數(雖然并不完全正确),簡單地說,它是沒有聲明的方法,也即沒有通路修飾符、傳回值聲明和名字。它提供了輕量級的文法,進而解決了匿名内部類帶來的“高度問題”。
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); }
() ->
() -> { return };
- 一個 Lambda 表達式可以有零個或多個參數
- 參數的類型既可以明确聲明,也可以根據上下文來推斷。例如:
與(int a)
效果相同(a)
- 所有參數需包含在圓括号内,參數之間用逗号相隔。例如:
或(a, b)
或(int a, int b)
(String a, int b, float c)
- 空圓括号代表參數集為空。例如:
() -> 42
- 當隻有一個參數,且其類型可推導時,圓括号()可省略。例如:
a -> return a*a
- Lambda 表達式的主體可包含零條或多條語句
- 如果 Lambda 表達式的主體隻有一條語句,花括号{}可省略。匿名函數的傳回類型與該主體表達式一緻
-
如果 Lambda 表達式的主體包含一條以上語句,則表達式必須包含在花括号{}中(形成代碼塊)。匿名函數的
傳回類型與代碼塊的傳回類型一緻,若沒有傳回則為空
目标類型?
函數式接口的名稱并不是lambda表達式的一部分。那麼對于給定的lambda表達式,它的類型是什麼?答案是:它的類型是由其上下文推導而來。
這就意味着同樣的lambda表達式在不同上下文裡可以擁有不同的類型:
FileFilter f = (t) -> true;
Predicate p = (t) -> true;
第一個lambda表達式
(t) -> true
是
FileFilter
的執行個體,而第二個lambda表達式則是
Predicate
的執行個體。
編譯器負責推導lambda表達式的類型。它利用lambda表達式所在上下文所期待的類型進行推導,這個被期待的類型被稱為目标類型。lambda表達式隻能出現在目标類型為函數式接口的上下文中。
來看幾個lambda例子
1.線程初始化 and 事件處理
//線程初始化
//舊方法:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from thread");
}
}).start();
//新方法:
new Thread(() -> System.out.println("Hello from thread")).start();
//1.2事件處理
//舊方法:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("The button was clicked using old fashion code!");
}
});
//新方法:
button.addActionListener( (e) -> {
System.out.println("The button was clicked. From Lambda expressions !");
});
2.數組列印
// forEach
//舊方法:
List<Integer> list = Arrays.asList(, , , , , , );
for(Integer n: list) {
System.out.println(n);
}
//新方法:
List<Integer> list = Arrays.asList(, , , , , , );
list.forEach(n -> System.out.println(n));
//使用方法引用
//使用 Java 8 全新的雙冒号(::)操作符将一個正常方法轉化為 Lambda 表達式
list.forEach(System.out::println);
3.stream流相關使用
//3.1 map
//舊方法:
List<Integer> list = Arrays.asList(,,,,,,);
for(Integer n : list) {
int x = n * n;
System.out.println(x);
}
//新方法:
List<Integer> list = Arrays.asList(,,,,,,);
list.stream().map((x) -> x*x).forEach(System.out::println);
//3.2 filter
//3.3 reduce
方法引用
有時候,我們的Lambda表達式可能僅僅調用一個已存在的方法,而不做任何其它事,對于這種情況,通過一個方法名字來引用這個已存在的方法會更加清晰,Java 8的方法引用允許我們這樣做。方法引用是一個更加緊湊,易讀的Lambda表達式,注意方法引用是一個Lambda表達式,其中方法引用的操作符是雙冒号”::”。
例如:
(n) -> System.out.println(n)
System.out::println
方法引用分為4類,常用的是前兩種。方法引用也受到通路控制權限的限制,可以通過在引用位置是否能夠調用被引用方法來判斷。具體分類資訊如下:
-
引用靜态方法
ContainingClass::staticMethodName
例子: String::valueOf,對應的Lambda:(s) -> String.valueOf(s)
比較容易了解,和靜态方法調用相比,隻是把.換為::
-
引用特定對象的執行個體方法
containingObject::instanceMethodName
例子: x::toString,對應的Lambda:() -> this.toString()
與引用靜态方法相比,都換為執行個體的而已
-
引用特定類型的任意對象的執行個體方法
ContainingType::methodName
例子: String::toString,對應的Lambda:(s) -> s.toString()
太難以了解了。難以了解的東西,也難以維護。建議還是不要用該種方法引用。
執行個體方法要通過對象來調用,方法引用對應Lambda,Lambda的第一個參數會成為調用執行個體方法的對象。
-
引用構造函數
ClassName::new
例子: String::new,對應的Lambda:() -> new String()
構造函數本質上是靜态方法,隻是方法名字比較特殊。
如何處理異常?
如果函數接口的方法本身沒有定義可以被抛出的受檢異常,那麼在使用該接口時是無法處理可能存在的受檢異常的,比如典型的IOException這類:
public static void main(String[] args) {
new Thread(() -> {
// 會提示有未處理異常
throw new Exception();
}).start();
}
我們有兩個選擇:
1.在Lambda表達式内處理受檢異常
2.捕獲該受檢異常并重新以非受檢異常(如RuntimeException)的形式抛出
public void notThrowExce() {
new Thread(() -> {
try {
throw new Exception();
} catch (Exception e) {
// 内部處理
// 抛出不受檢異常
throw new RuntimeException();
}
}).start();
}
如果函數接口的方法本身定義了可以被抛出的受檢異常
@FunctionalInterface
public interface WorkerInterface {
void doSomeWork() throws Exception;
}
public class Worker implements WorkerInterface{
private WorkerInterface workerInterface;
public Worker(WorkerInterface workerInterface) {
this.workerInterface = workerInterface;
}
@Override
public void doSomeWork() throws Exception {
workerInterface.doSomeWork();
}
}
public void throwExce() throws Exception {
// 可以将異常抛出
new Worker(() -> { throw new Exception(); }).doSomeWork();
}
Lambda 表達式與匿名類的差別
1.對于匿名類,關鍵詞
this
解讀為匿名類,而對于 Lambda 表達式,關鍵詞
this
解讀為寫就 Lambda 的外部類。
2.Java 編譯器編譯 Lambda 表達式時将他們轉化為類裡面的私有函數