天天看點

java:lambda表達式

1.為什麼引入lambda表達式?

lambda表達式是一個可傳遞的代碼塊,可以在以後執行一次或者多次。其實在我們日常代碼的編寫中,已經不止一次的使用過這種代碼塊。

列如:考慮如何用一個定制比較器完成排序。如果我們想要按照長度而不是字典序對字元串進行排序,可以像sort方法傳入一個Comparator對象:

class LengthComparator implements Comparator<String>{

		@Override
		public int compare(String o1, String o2) {
			// TODO 自動生成的方法存根
			return o1.length()-o2.length();
		}
	}
	
	...Arrays.sory(strings,new LengthComparator());           

compare方法不是立即調用。實際上,在數組完成排序之前,sort方法會一直調用compare方法,隻要元素的順序不正确就會重新排列元素。将比較元素所需的代碼段放在sort方法中,這個代碼将與其餘的排序邏輯內建(你可能并不打算重新實作其餘的這部分邏輯)。

這個例子是将一個代碼塊傳遞到某個對象,這個代碼塊會在将來的某個時間調用。

目前看來,在java中傳遞一個代碼段并不容易,不能直接傳遞代碼段。作為一種面向對象的程式設計語言,我們必須構造一個對象,這個對象的類需要有一個方法能包含所需的代碼塊。

2.lambda表達式的文法

再來考慮上面我們讨論的排序例子,我們傳入代碼來檢查一個字元串是否比另一個字元串段。這裡要計算:

o1.length()-o2.length();           

其中o1和o2都是字元串,java是一種強類型語言,是以我們還要指定他們的類型:

(String o1,String o2)->o1.length()-o2.length()           

這就是你看到的第一個lambda表達式。lambda表達式就是一個代碼塊,以及必須傳入代碼的變量規範。

lambda名字的具體由來這裡就不再細講了(應該不重要吧?),我們已經見過java中的一種lambda表達式形式:

參數,箭頭(->)以及一個表達式

如果代碼要完成的計算無法放在一個表達式中,就可以像寫方法一樣,把這些代碼放在{}中,并包含顯示的return語句。列如:

(String o1,String o2)->{
	if(o1.length()<o2.length()) return -1;
	else if(o1.length()>o2.length()) return 1;
	return 0;
}           

即使lambda表達式沒有參數,仍然要提供空括号,就像無參數方法一樣:

()->{for(int i=100;i>=0;i--) System.out.println(i);}           

如果可以推導出一個lambda表達式的參數類型,則可以忽略其類型。例如:

Comparator<String> comp=(o1,o2)->(o1.length()-o2.length());           

在這裡,編譯器可以推導出o1和o2必然是字元串,因為這個lambda表達式将賦給一個字元串比較器。

如果方法隻有一個參數,而且這個參數的類型可以推導得出,那麼甚至可以省略小括号:

Actionlistener listener=event->System.out.println("The time is "+new Date());           

無需指定lambda表達式的傳回類型。lambda表達式的傳回類型總是會由上下文推導得出。例如:

(String o1,String o2)->o1.length()-o2.length();           

可以在需要int類型結果的上下文中使用。

注意:如果一個lambda表達式隻在某些分支傳回一個值,而在另外一些分支不傳回值,這是不合法的。列如:

(int x)->{if(x>=0) return 1;}           

下面這個例子顯示了如何在一個比較器和一個動作監聽器中使用lambda表達式。

class Solution {
	public static void main(String[] args) {
		String[] planets=new String[] {"Mecury","Venus","Earth","Mars",
				"Jupiter","Saturn","Uranus","Meptune"};
		System.out.println(Arrays.deepToString(planets));
		System.out.println("Sorted in dictionary order:");
		Arrays.parallelSort(planets);
		System.out.println(Arrays.toString(planets));
		System.out.println("Sorted by length");
		Arrays.parallelSort(planets,(first,second)->(first.length()-second.length()));
		System.out.println(Arrays.toString(planets));
		
		Timer t=new Timer(1000,event->System.out.println("The time is "+new Date()));
		t.start();
		
		JOptionPane.showMessageDialog(null, "Quit program?");
		System.exit(0);
	}
}           

輸出為:

[Mecury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Meptune]
Sorted in dictionary order:
[Earth, Jupiter, Mars, Mecury, Meptune, Saturn, Uranus, Venus]
Sorted by length
[Mars, Earth, Venus, Mecury, Saturn, Uranus, Jupiter, Meptune]
The time is Mon Nov 18 19:01:22 CST 2019
The time is Mon Nov 18 19:01:23 CST 2019
The time is Mon Nov 18 19:01:24 CST 2019
The time is Mon Nov 18 19:01:25 CST 2019
The time is Mon Nov 18 19:01:26 CST 2019
The time is Mon Nov 18 19:01:27 CST 2019
The time is Mon Nov 18 19:01:28 CST 2019
The time is Mon Nov 18 19:01:29 CST 2019
           

3.函數式接口

前面已經讨論過,java中已經有很多封裝代碼塊的接口,例如:ActionListener或者Comparator。lambda表達式與這些接口是相容的。對于隻有一個抽象方法的接口,需要這種接口的對象時,就可以提供一個lambda表達式。這種接口稱為函數式接口。

注釋:按照常理來說,應該接口中所有方法都是抽象的才對,實際上,接口完全有可能重新聲明Object類的方法,如toString或者Clone,這些聲明有可能會讓方法不再是抽象的。

為了展示如何轉換為函數式接口,下面考慮Arrays.sort方法。它的第二個參數需要一個Comparator執行個體,Comparator就是隻有一個方法的接口,是以可以提供一個lambda表達式:

Arrays.parallelSort(words,(first,second)->first.length()-second.length());           

在底層,Arrays.sort方法會接收實作了Comparator<String>的某個類的對象。在這個對象上調用compare方法會執行這個lambda表達式的體。這些對象和類的管理完全取決于具體實作,與使用傳統的内聯類相比,這樣可能要高效很多。最好把lambda表達式看做是一個函數,而不是一個對象,另外要接受lambda表達式可以傳遞到函數式接口。

lambda表達式可以轉換為接口,這一點讓lambda表達式很有吸引力。具體的文法很簡短:

Timer t=new Timer(1000,event->
{
	System.out.println("At the tone,the time is "+new Date());
	Toolkit.getDefaultToolkit().beep();
});           

與使用實作了ActionListener接口的類相比,這個代碼的可讀性要好得多。

實際上,在java中,對lambda表達式所能做的也隻是能轉換為函數式接口。在其他支援函數字面量的程式設計語言中,可以聲明函數類型(如(String,String)->int)、聲明這些類型的變量,還可以使用變量儲存函數表達式。不過java的設計者并沒有為java語言增加函數類型。

注釋:甚至不能把lambda表達式賦給類型為Object的變量,因為Object不是一個函數式接口!

java API在java.util.function包中定義了很多非常通用的函數式接口,其中一個接口BiFunction<T,U,R>描述了參數類型為T和U而且傳回類型為R的函數。可以把我們的字元串比較lambda表達式儲存在這個類型的變量中:

BiFunction<String,String,Integer> comp=(first,second)->first.length()-second.length();           

不過,這對于排序并沒有幫助,沒有那個Arrays。sort方法想要接受一個BiFunction。

如果我們想要用lambda表達式做某些處理,需要謹記表達式的用途,為它建立一個特定的函數式接口。java.util.function包中有一個尤其有用的接口Predicate:

public interface Predicate<T>{
	boolean test(T t);
	//Additional default and static methods
}           

ArraysList類有一個removeIf方法,它的參數就是一個Predicate。這個接口專門用來傳遞lambda表達式,列如,下面的語句将從一個數組清單删除所有null值:

list.removeIf(e->e==null);           

4.方法引用

有時,可能已經有現成的方法可以完成你想要傳遞到其他代碼的某個動作。列如,假設你希望隻要出現一個定時器事件就列印這個事件對象。當然,為此也可以調用:

Timer t=new Timer(1000,event->System.out.println(event));           

但是,如果直接把println方法傳遞到Timer構造器就更好了,下面是具體做法:

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

表達式System.out::println是一個方法引用,它等價于lambda表達式->System.out.println(x)。

再來看一個例子,假設你想對字元串進行排序,而不考慮字母的大小寫,可以傳遞以下方法表達式:

Arrays.sort(strings,String::compareToIgnoreCase);           

從這些例子中可以看出,要用::操作符分隔方法名與對象或者類名。主要有三種情況:

object::instanceMethod
Class::staticMethod
Class::instanceMethod           

在前兩種情況中,方法引用等價于提供方法參數的lambda表達式。對于第三種情況,第一個參數會成為方法的目标。列如,String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y)

注意:如果有多個同名的重載方法,編譯器就會嘗試從上下文中找出你指的哪一個方法。例如,Math.max方法有兩個版本,一個用于整數,另一個用于double值,選擇哪一個版本取決于Math::max轉換為哪個函數式接口的方法參數。類似于lambda表達式,方法引用不能獨立存在,總是會轉換為函數式接口的執行個體。

可以在方法引用中使用this參數,列如,this::equals等同于x->this.equals(x)。使用super也是合法的。使用this作為目标,會調用給定方法的超類版本。我們看下面一個例子:

class Greeter{
	public void greet() {
		System.out.println("Hello World!");
	}
}
class TimedGreeter extends Greeter{
	public void greet() {
		Timer t=new Timer(1000,super::greet);
		t.start();
	}
}           

TimedGreeter.greet方法開始執行時,會構造一個Timer,它會在每次定時器滴答時執行super::greet方法。這個方法會調用超類的greet方法。

5.構造器引用

構造器引用與方法引用很類似,隻不過方法名為new,例如,Person::new是Person構造器的一個引用。具體是哪一個構造器呢?這取決于上下文。

可以用數組類型建立構造器引用。例如,int[]::new是一個構造器引用,它有一個參數:即數組的長度。這其實等價于lambda表達式x->new int[x];

java有一個限制,無法構造泛型類型T的數組。數組構造器引用對于客服這個限制很有用。表達式new T[n]會産生錯誤,因為這會改為new Object[n]。例如,假設我們需要一個Person對象數組。Stream接口有一個toArray方法可以傳回Object數組。

Object[] people=stream.toArray();           

不過, 這并不讓人滿意。使用者希望得到一個Person引用數組,而不是Object引用數組。流庫利用構造器引用解決了這個問題。可以把Person[]::new傳入toArray方法:

Person[] people=stream.toArray(Person[]::new);           

toArray方法調用這個構造器來得到一個正确類型的數組,然後填充這個數組并傳回。

6.變量作用域

通常,我們希望能夠在lambda表達式中通路外圍方法或類中的變量。考慮下面這個例子:

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

來看這樣一個調用:

repeatMessage("Hello",1000);           

該lambda表達式中的變量text并沒有在表達式中定義,事實上它是repeatMessage方法的一個參數變量。仔細想想看,這裡好像會有問題,盡管不那麼明顯。lambda表達式的代碼可能會在repeatMessage調用很久以後才運作,而那時這個參數變量已經不存在了。如何保留text變量呢?

要了解到底會發生什麼,下面來鞏固我們對lambda表達式的了解。lambda表達式有3個部分:

1)一個代碼塊         2)參數         3)自由變量的值,這是指非參數而且不在代碼中定義的變量

在上邊的例子中,這個lambda表達式有1個自由變量text。表示lambda表達式的資料結構必須存儲自由變量的值,在這裡就是字元串“Hello”。我們稱它被lambda表達式捕獲。

擴充:在java中,lambda表達式就是閉包

我們可以看到,lambda表達式可以捕獲外圍作用域中變量的值。但是有一條明文規定:lambda表達式中捕獲的變量必須實際上是最終變量,實際上的最終變量是指,這個變量初始化之後就不會再為它賦新值。上個例子中,text總是訓示同一個String對象,是以捕獲這個變量是合法的。

lambda表達式的體與嵌套塊有相同的作用域,這裡同樣适用命名沖突和遮蔽的有關規則。在lambda表達式中聲明與一個局部變量同名的參數或局部變量是不合法的。

class Solution {
    public static void main(String[] args) {
    	Path first=Paths.get("/usr/bin");
        Comparator<String> comp=(first,second)->first.length()-second.length();
    }
}           

在方法中,不能有兩個同名的局部變量,是以,lambda表達式中同樣也不能有同名的局部變量。

在一個lambda表達式中使用this關鍵字時,是指建立這個lambda表達式的方法的this參數,列如:

public class Application() {
    public void init() {
    	ActionListener listener=event->{
           System.out.println(this.toString());
    	}
    }
}           

表達式this.toString()會調用Application對象的toString方法,而不是ActionListener執行個體的方法。在lambda表達式中,this的使用并沒有任何特殊之處,lambda表達式的作用域嵌套在init方法中,與出現在這個方法中的其他位置一樣,lambda表達式中this的含義并沒有變化。

7.處理lambda表達式

使用lambda表達式的重點是延遲執行。畢竟,如果想要立即執行代碼,完全可以直接執行,而無需把它包裝在一個lambda表達式中。之是以希望以後再執行代碼,這裡有很多原因,如:

1)在一個單獨的線程中運作代碼         

2)多次運作代碼

3)在算法的适當位置運作代碼(例如:排序中的比較操作)

4)發生某種情況時執行代碼(例如:點選了一個按鈕,資料到達,等等)

5)隻在必要時才運作代碼

8.再談Comparator

Comparator接口包含很多友善的靜态方法來建立比較器,這些方法可以用于lambda表達式或方法引用。

靜态comparing方法取一個“鍵提取器”函數,它将類型T映射為一個可比較的類型(如String)。對要比較的對象應用這個函數,然後對傳回的鍵完成比較,例如,假設有一個Person對象數組,可以如下按名字對這些對象排序:

Arrays.sort(people,Comparator.comparing(Person::getName));           

也可以把比較器與thenComparing方法串起來,例如:

Arrays.sort(people,Comparator.comparing(Person::getName).thenComparing(Person::getFirstName));           

如果兩個人的姓相同,就會使用第二個比較器。

這些方法有很多變體形式。可以為comparing和thenComparing方法提取的鍵指定一個比較器。例如,可以如下根據人名長度完成排序:

Arrays.sort(people,Comparator.comparing(Person::getName,(s,t)->Integer.compare(s.length(),t.length())));           

另外,comparing和thenComparing方法都有變體形式,可以避免int、long或double值的裝箱。要完成前一個操作,還有一種更容易的做法:

Arrays.sort(people,Comparator.comparingInt(p->p.length()));           

如果鍵函數可以傳回nunll,可能就要用到nullsFirst和nullLast擴充卡。這些靜态方法會修改現有的比較器,進而在遇到null值時不會抛出異常,而是将這個值标記為小于或者大于正常值。列如,假設一個人沒有中名時getMiddleName會傳回一個null,就可以使用

Comparator.comparing(Person::getMiddleName(),Comparator.nullsFirst(...))           
Arrays.sort(people,comparing(Person::getMiddleName,nullsFirst(naturalOrder())));