1.前言
在java8 以前,若我們想要把某些功能傳遞給某些方法,總要去寫匿名類。以前注冊事件監聽器的寫法與下面的示例代碼就很像:
manager.addScheduleListener(new ScheduleListener() {
@Override
public void onSchedule(ScheduleEvent e) {
//Event listener implementation goes here...
}
});
我們添加了一些自定義代碼到Schedule監聽器中,需要先定義匿名內部類,然后傳遞一些功能到onSchedule方法中。
正是java在作為參數傳遞普通方法或功能的限制,java8增加了一個全新語言級別的功能,稱為lambda表達式。
2.為什么java需要lambda表達式
java是面向對象語言,除了原始數據類型之外,java中所有內容都是一個對象。而在函數式語言中,我們只需要給函數分配變量,并將這個函數作為參數傳遞給其他函數就可以實現特定的功能。javaScript就是函數式編程語言的典范(閉包)。
lambda表達式的加入,使得java擁有了函數式編程的能力。在其他語言中,lambda表達式的類型是一個函數;但在java中,lambda表達式被表示為對象,因此他們必須綁定到被稱為功能接口的特定對象類型。
3.lambda 表達式簡介
lambda表達式是一個匿名函數(對于java而言并不是很準確,但是這里我們不糾結這個問題)。簡單來說,這是一種沒有聲明的方法,即沒有訪問修飾符,返回值聲明和名稱。
在僅使用一次方法的地方特別有用,方法定義很短。它為我們節省了,如包含類聲明和編寫單獨方法的工作。
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);}
() -> 42
() -> {return 3.1415}
3.1 lambda 表達式的結構
lambda表達式的結構:
- Lambda表達式可以具有零個,一個或多個參數。
- 可以顯示聲明參數的類型,也可以由編譯器自動從上下文推斷參數的類型。例如
(int a)
也可以寫作(a)
。 - 參數用小括號括起來,用逗號分隔。例如
(a,b)
或(int a,int b)或(String a,int b,float c)
。 - 空括號用于表示一組空的參數。例如
() -> 42
。 - 當有且僅有一個參數時,如果不顯示的指明類型,則不必使用小括號。例如
a -> return a*a
。 - lambda表達式的正文可以包含零條,一條或多條語句。
- 如果lambda表達式的正文只有一條語句,則大括號可不用寫,且表達式的返回值類型要與匿名函數的返回類型相同。
- 如果lambda表達式的正文有一條以上的語句必須包含在大括號(代碼塊)中,且表達式的返回值類型要與匿名函數的返回值類型相同。
4.方法引用
4.1 從lambda 表達式到雙冒號操作符
例如,要穿件一個比較器,一下語法就夠了
Comparator c = (Person p1,Person p2) -> p1.getAge().compareTp(p2.getAge());
然后,使用類型推斷:
Comparator c = (p1,p2) -> p1.getAge().compareTo(p2.getAge());
我們可以使上面的代碼更具表現力和可讀性,我們來看一下:
Comparator c = Comparator.comparing(Person::getAge);
使用::
運算符作為lambda調用特定方法的縮寫,并且擁有更好的可讀性。
4.2 使用方式
雙冒號(::)
操作符是java中的方法引用。當使用一個方法的引用時,目標引用放在::
之前,目標引用提供的方法名稱在::
之后,即目標引用::方法
。比如:
Person::getAge;
//獲取getAge方法的 Function 對象
Fuction<Person,Integer> getAge = Person::getAge
//傳參數調用getAge 方法
Integer age = getAge.apply(p);
我們引用getAge,然后將其應用于正確的參數。
目標引用的參數類型是Function<T,R>
T
表示傳入類型,R
表示返回類型。比如 表達式 person -> person.getAge();
,傳入參數是person,返回值是person.getAge(),那么方法引用Person::getAge 就對應著Function<Person,Integer>
類型。
5.什么是功能接口(Functional interface)
在Java中,功能接口(Functional interface)指只有一個抽象方法的接口。
java.lang.Runnable
是一個功能接口,在Runnable
中只有一個方法的聲明void run()
。我們使用匿名內部類實例化功能接口的對象。而使用lambda表達式,可以簡化寫法。
每個lambda表達式都可以隱式地分配給功能接口,例如,我們可以從lambda表達式創建Runnable接口的引用,如下所示。
Runnable r = () -> System.out.println("hello world");
當我們不指定功能接口時,這種類型的轉換會被編譯器自動處理,例如:
new Thread(
() -> System.out.println("hello world")
).start();
在上面的代碼中,編譯器會自動推斷,lambda表達式可以從Thread類的構造函數簽名(public Thread(Runnable r) {}
)轉換為Runnable接口。
@FunctionalInterface
是在java8 中添加的一個新注解,用于指示接口類型,聲明接口為java語言規范定義的功能接口。java8還聲明了lambda表達式可以使用的功能接口的數量。當您注釋的接口不是有效的功能接口時,@FunctionalInterface
會產生編譯器級錯誤。
以下是自定義功能接口的示例:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
正如其定義所述,功能接口只能有一個抽象方法。如果我們嘗試在其中添加一個抽象方法,則會拋出編譯時錯誤。例如:
@FunctionalInterface
public interface WorkerInterface{
public void doWork();
public void doMoreWork();
}
錯誤:意外的 @FunctionalInterface 注釋,WorkerInterface 不是函數接口,
WorkerInterface 中找到多個非覆蓋抽象方法
一旦定義了功能接口,我們就可以利用lambda表達式調用。例如:
WorkerInterface work = () -> System.out.println("通過lambda表達式調用");
work.doWork();
6.lambda表達式的例子
6.1 線程初始化
new Thread(
() -> System.out.println("hello world")
).start();
6.2 事件處理
事件處理可以用java8 使用lambda表達式來完成。以下代碼顯示了將ActionListener 添加到UI組件的新舊方式:
// 舊
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.print("hello world");
});
//新
button.addActionListener( (e) ->
System.out.println("hello world");
});
6.3 遍歷輸出(方法引用)
輸出給定數組的所有元素的簡單代碼。請注意,還有一種使用lambda表達式的方式。
//old way
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for( Integer i : list) {
System.out.println(i);
}
//使用 -> 的lambda 表達式
list.forEach(n -> System.out.println(n));
//使用::的lambda 表達式
list.forEach(System.out::println)
6.4邏輯操作
輸出通過邏輯判斷的數據
public static void main(String args[]) {
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
System.out.print("輸出所有數字:");
evaluate(list,(n) -> true);
System.out.print("不輸出:");
evaluate(list,(n) -> true);
System.out.print("輸出偶數:");
evaluate(list,(n) -> true);
System.out.print("輸出奇數:");
evaluate(list,(n) -> true);
System.out.print("輸出大于5的數字:");
evaluate(list,(n) -> true);
}
public static void evaluate(List<Integer> list,Predicate<Integer> predicate){
for( Integer n : list) {
if(predicate.test(n)) {
System.out.print( n + " ");
}
}
System.out.println();
}
6.4 Stream API 示例
java.util.stream.Stream 接口和lambda 表達式一樣,都是java8 新引入的。所有Stream的操作必須以lambda表達式為參數。Stream接口中帶有大量有用的方法,比如map() 的左右就是將input Stream的每個元素,映射成output Stream的另外一個元素。
下面的例子,我們將lambda 表達式 x -> x * x
傳遞給map()
方法,將其應用于流的所有元素。之后,我們使用forEach打印列表的所有元素。
//old way
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer i : list) {
int x = i * i;
System.out.println(x);
}
//new way
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map(n -> n * n).forEach(System.out::println)
7.lambda 表達式和匿名類之間的區別
-
this
關鍵字:對于匿名類this
關鍵字解析為匿名類,而對于lambda表達式,this
關鍵字解析為包含寫入lambda的類。 - 編譯方式:java編譯器編譯Lambda表達式時,會將其轉換為類的私有方法,再進行動態綁定。