在2013年將發布的 JavaSE8 中將包含一個叫做 Lambda Project 的計劃,在今年6月份的 JSR-335 草案中有描述。
JSR-335 將閉包引入了 Java 。閉包在現在的很多流行的語言中都存在,例如 C++、C# 。閉包允許我們建立函數指標,並把它們作為參數傳遞。在這篇文章中,我們將粗略的看一遍Java8的特性,並介紹Lambda運算式。而且我將試著放一些範例程式來解釋一些概念和文法。
Java 程式設計語言給我們提供了介面的概念,介面裡可以定義抽象的方法。介面定義了 API,並希望使用者或者供應商來實現這些方法。很多時候,我們並不為一些介面建立獨立的實作類別,我們通過寫一個匿名內部類來寫一個內聯的介面實現。
匿名類使用的非常廣泛。匿名內部類使用的最常見的情境就是事件處理器了。其次匿名內部類還常被用在多線程的程式中,我們通常寫匿名內部類,而不是建立 Runnable/Callable 介面的實作類別。
就像我們討論的一樣,一個匿名類就是一個內聯的給定的介面的實現。通常我們將這個實作類別的對象作為參數傳遞給一個方法,然後這個方法將在內部調用傳遞過來的實作類別的方法。故這種介面叫做回調介面,這些方法叫做回調方法。
雖然匿名類到處都在使用,但是他們還是有很多問題。第一個主要問題是複雜。這些類讓代碼的層級看起來很亂很複雜,也稱作 Vertical Problem 。第二,他們不能訪問封裝類的非 final 成員。this 這個關鍵字將變得很有迷惑性。如果一個匿名類有一個與其封裝類相同的成員名稱,內部變數將會覆蓋外部的成員變數,在這種情況下,外部的成員在匿名類內部將是不可見的,甚至不能通過 this 關鍵字來訪問。因為 this 關鍵字值得是匿名類對象本身而不是他的封裝類的對象。
public void anonymousExample() { String nonFinalVariable = "Non Final Example"; String variable = "Outer Method Variable"; new Thread(new Runnable() { String variable = "Runnable Class Member"; public void run() { String variable = "Run Method Variable"; //Below line gives compilation error. //System.out.println("->" + nonFinalVariable); System.out.println("->" + variable); System.out.println("->" + this.variable); } }).start();}
輸出是:
->Run Method Variable->Runnable Class Member
這個例子很好的說明了我上面所說的這個問題,而 Lambda 運算式幾乎解決了匿名內部類帶來的所有問題。在我們進一步探討 lambda 運算式之前,讓我們來看一看 Functional Interfaces。
Functional Interfaces
Functional Interfaces 是一個只有單個方法的介面,這代表了這個方法契約。
上面的定義中的只有一個實際上並沒有那麼簡單。這段有些不懂,請讀者查看原文(The ‘Single' method can exist in the form of multiple abstract methods that are inherited from superinterfaces. But in that case the inherited methods should logically represent a single method or it might redundantly declare a method that is provided by classes like Object, e.g. toString.)
下面的例子清楚的展示了怎樣理解 Functional Interfaces 的概念。
interface Runnable { void run(); }// Functionalinterface Foo { boolean equals(Object obj); }// Not functional; equals is already an implicit memberinterface Bar extends Foo {int compare(String o1, String o2); }// Functional; Bar has one abstract non-Object methodinterface Comparator { boolean equals(Object obj); int compare(T o1, T o2);}// Functional; Comparator has one abstract non-Object methodinterface Foo {int m(); Object clone(); }// Not functional; method Object.clone is not publicinterface X { int m(Iterable arg); }interface Y { int m(Iterable arg); }interface Z extends X, Y {}// Functional: two methods, but they have the same signature
大多數回調介面都是 Functional Interfaces。例如 Runnable,Callable,Comparator 等等。以前被稱作 SAM(Single Abstract Method)
Lambda 運算式
我們上邊說過,匿名類的一個主要問題是是代碼的層級看起來很亂,也就是 Vertical Problem 了,Lamdba 運算式實際上就是匿名類,只不過他們的結構更輕量,更短。Lambda 運算式看起來像方法。他們有一個正式的參數列表和這些參數的塊體表達。
(String s)-> s.lengh;() -> 43;(int x, int y) -> x + y;
上面的例子的意思是,第一個運算式接收一個 String 變數作為參數,然後返回字串的長度。第二個不帶任何參數,並返回43。最後,第三個接受兩個整數 x 和 y ,並返回其和。
在看了許多文字後,終於,我可以給出第一個 Lambda 運算式的例子了,這個例子運行在 JavaSE8 的預覽版下:
public class FirstLambdaExpression { public String variable = "Class Level Variable"; public static void main(String[] arg) { new FirstLambdaExpression().lambdaExpression(); } public void lambdaExpression(){ String variable = "Method Local Variable"; String nonFinalVariable = "This is non final variable"; new Thread (() -> { //Below line gives compilation error //String variable = "Run Method Variable" System.out.println("->" + variable); System.out.println("->" + this.variable); }).start(); }}
輸出是:
->Method Local Variable->Class Level Variable
你可以比較一些使用 Lambda 運算式和使用匿名內部類的區別。我們可以清楚的說,使用 Lambda 運算式的方式寫匿名類解決了變數可見度的問題。你可以看一下代碼中的注釋, Lambda 運算式不允許建立覆蓋變數。
通常的 Lambda 運算式的文法包括一個參數列表,箭頭關鍵字"->"最後是主體。主體可以是運算式(單行語句)也可以是多行語句塊。如果是運算式,將被計算後返回,如果是多行的語句塊,就看起來跟方法的語句塊很相似了,可以使用 return 來指定傳回值。break 和 continue 只能用在迴圈內部。
為什麼選擇這個特殊的文法形式呢,因為目前 C# 和 Scala 中通常都是這種樣式,也算是 Lambda 運算式的通用寫法。這樣的文法設計基本上解決了匿名類的複雜性。但是與此同時他也是非常靈活的,例如,如果方法體是單個運算式,大括弧和 return 語句都是不需要的。運算式的結果就是作為他自己的傳回值。這種靈活性可以保持代碼簡潔。
Lambda 運算式用作匿名類,因此他們可以靈活運用在其他模組或在其他 Lambda 運算式(嵌套的 Lambda 運算式)。
//Lambda expression is enclosed within methods parameter block.//Target interface type is the methods parameter type.String user = doSomething(() -> list.getProperty(“propName”);//Lambda expression is enclosed within a thread constructor//target interface type is contructors paramter i.e. Runnablenew Thread (() -> { System.out.println("Running in different thread");}).start();
如果你仔細看看 lambda 運算式,您將看到,目標介面類型不是一個運算式的一部分。編譯器會協助推斷 lambda 運算式的類型與周圍環境。
Lambda 運算式必須有一個目標類型,而他們可以適配任意可能的目標類型。當目標類型是一個介面的時候,下面的條件必須滿足,才能編譯正確:
- 介面應該是一個 functional interface
- 運算式的參數數量和類型必須與 functional interface 中聲明的一致
- 傳回值類型必須相容 functional interface 中方法的傳回值類型
- 拋出的異常運算式必須相容 functional interface 中方法的拋出異常聲明
由於編譯器可以通過目標類型的聲明中得知參數類型和個數,所以在 Lambda 運算式中,可以省略參數型別宣告。
Comparator c = (s1, s2) -> s1.compareToIgnoreCase(s2);
而且,如果目標類型中聲明的方法只接收一個參數(很多時候都是這樣的),那麼參數的小括弧也是可以不寫的,例如:
ActionListenr listenr = event -> event.getWhen();
一個很明顯的問題來了,為什麼 Lambda 運算式不需要一個指定的方法名呢?
答案是:Lambda 運算式只能用於 functional interface ,而 functional interface 只有一個方法。
當我們確定一個 functional interface 來建立 Lambda 運算式的時候,編譯器可以感知 functional interface 中方法的簽名,並且檢查給定的運算式是否匹配。
這種靈活的文法協助我們避免了使用匿名類的 Vertical Problem ,而且不會帶來 Horizontal Problem(單行語句非常長)。
Lambda 運算式的文法是上下文相關的,但是這些並不是第一次出現。Java SE 7添加的diamond operators 也有這個概念,通過上下文推斷類型。
void invoke(Runnable r) {r.run()}void Future invoke(Callable r) {return c.compute()}//above are two methods, both takes parameter of type functional interfaceFuture s = invoke(() ->"Done"); //Which invoke will be called?
上面問題的答案是調用接收Callable參數的方法。在這種情況下編譯器會通過不同參數類型的重載解決。當有不止一個適用的重載方法,編譯器也檢查lambda運算式與相應的目標類型的相容性。簡單的說,上面的invoke方法期望一個返回,但是只有一個invoke方法具有傳回值。
Lambda運算式可以顯式的轉換為指定的目標類型,只要跟對應的類型相容。看一下下面的程式,我實現了三種Callable,而且都將其轉換為Callable類型。
public class FirstSightWithLambdaExpressions { public static void main(String[] args) { List list = Arrays.asList( (Callable)()->"callable 1", (Callable) ()->"callable 2", (Callable) ()->"callable 3"); ExecutorService e = Executors.newFixedThreadPool(2); List futures = null; try { futures = e.invokeAll(list); new FirstSightWithLambdaExpressions().dumpList(futures); } catch (InterruptedException | ExecutionException e1) { e1.printStackTrace(); } e.shutdown(); } public void dumpList(List list) throws InterruptedException, ExecutionException { for (Future future : list) { System.out.println(future.get()); } }}
正如我們前面討論的一樣,匿名類不能訪問周圍環境中非final的變數。但是Lambda運算式裡就沒有這個限制。
目前,該定義的 functional interfaces 只適用於介面。我試著對一個只有一個抽象方法的抽象類別建立一個 lambda 運算式,但出了一個編譯錯誤。按照 jsr - 335,未來版本的 lambda 運算式可能支援 Functional Classes。
方法引用
方法引用被用作引用一個方法而不調用它。
Lambda 運算式允許我們定義一個匿名的方法,並將它作為 Functional interface 的一個執行個體。方法引用跟 Lambda 運算式很像,他們都需要一個目標類型,但是不同的是方法引用不提供方法的實現,他們引用一個已經存在的類或者對象的方法。
System::getProperty"abc"::lengthString::lengthsuper::toStringArrayList::new
上面的語句展示了方法和建構函式的引用的通用文法。這裡我們看到引入了一個新的操作符“::'(雙冒號)。我尚不清楚確切名稱為這個操作符,但是 JSR 指它作為分隔字元,維基百科頁面是指它作為一個範圍解析操作符。作為我們的參考,本教程的範圍內,我們將簡單地將它作為分隔字元。
目標引用或者說接收者被放在提供者和分隔字元的後面。這形成了一個運算式,它能夠引用一個方法。在最後聲明上述代碼,該方法的名字是“new”。這個運算式引用的是 ArrayList 類的構造方法(下一節再討論構造方法的引用)
再進一步瞭解這個之前,我想讓你看一看方法引用的強大之處,我建立了一個簡單的 Employee 數組的排序程式。
import java.util.Arrays;import java.util.List;import java.util.concurrent.ExecutionException;import java.util.concurrent.Future;public class MethodReference { public static void main (String[] ar){ Employee[] employees = {new Employee("Nick"), new Employee("Robin"), new Employee("Josh"), new Employee("Andy"), new Employee("Mark")}; System.out.println("Before Sort:"); dumpEmployee(employees); Arrays.sort(employees, Employee::myCompare); System.out.println("After Sort:"); dumpEmployee(employees); } public static void dumpEmployee(Employee[] employees){ for(Employee emp : Arrays.asList(employees)){ System.out.print(emp.name+", "); } System.out.println(); }}class Employee { String name; Employee(String name) { this.name = name; } public static int myCompare(Employee emp1, Employee emp2) { return emp1.name.compareTo(emp2.name); }}
輸出是:
Before Sort: Nick, Robin, Josh, Andy, Mark, After Sort: Andy, Josh, Mark, Nick, Robin,
輸出沒什麼特別,Employee 是一個非常簡單的類,只有一個 name 屬性。靜態方法 myCompare 接收兩個 Employee 對象,返回他們名字的比較。
在 main 方法中我建立了一個不同的 employee 的數組,並且將它連同一個方法參考資料表達式( Employee::myCompare )傳遞給了 Arrays.sort 方法。
等一下,如果我們看 Javadoc 你會發現 sort 方法的第二個參數是 Comparator 類型的,但是我們卻傳遞了 Employee 的一個靜態方法引用。重要的問題就在這了,我既沒有讓 Employee 實現 Comparable 介面,也沒有寫一個獨立的 Comparator 類,但是輸出確實沒有任何問題。
讓我們來看一看這是為什麼。 Arrays.sort 方法期望一個 Comparator 的執行個體,而這個 Comparator 是一個 functional interface ,這就意味著他只有一個方法,就是 compare 了。這裡我們同樣惡意傳一個 Lambda 運算式,在這個運算式中提供 compare 方法的實現。但是在我們的裡中,我們的 Employee 類已經有了一個自己的比較方法。只是他們的名字是不一樣的,參數的類型、數量,傳回值都是相同的,這裡我們就可以建立一個方法引用,並將它傳遞給 sort 作為第二個參數。
當有多個相同的名稱的方法的時候,編譯器會根據目標類型選擇最佳的匹配。為了搞明白,來看一個例子:
public static int myCompare(Employee emp1, Employee emp2) { return emp1.name.compareTo(emp2.name);}//Another method with the same name as of the above.public static int myCompare(Integer int1, Integer int2) { return int1.compareTo(int2);}
我建立了兩個不同的數組,用作排序。
Employee[] employees = {new Employee("Nick"), new Employee("Robin"), new Employee("Josh"), new Employee("Andy"), new Employee("Mark")};Integer[] ints = {1 , 4, 8, 2, 3, 8, 6};
現在,我執行下面的兩行代碼
Arrays.sort(employees, Employee::myCompare);Arrays.sort(ints, Employee::myCompare);
這裡,兩行代碼中的方法引用聲明都是相同的(Employee::myCompare),唯一不同的是我們傳入的數組,我們不需要傳遞一個含糊不清的標記用以知名那個方法作為方法引用,編譯器會協助我們檢查第一個參數,並且智能的找到合適的方法。
不要被靜態方法誤導了哦,我們還可以建立執行個體方法的引用。對於靜態方法我們使用類名::方法名來寫方法引用,如果是執行個體方法的引用,則是對象::方法名。
上面的例子已經是相當不錯的了,但是我們不必為整型的比較單獨寫一個方法,因為Integer已經實現了Comparable並且提供了實現方法compareTo。所以我們直接使用下面這一行就行了:
Arrays.sort(ints, Integer::compareTo);
看到這裡,你是否覺得有點迷惑?沒有?那我來讓你迷惑一下
這裡, Integer 是一個類名(而不是一個像 new Integer() 一樣的執行個體),而 compareTo 方法卻是 Integer 類的成員方法(非靜態).如果你仔細看了我上面的描述就會知道,成員方法的方法引用::之前應該是對象,但是為什麼這裡的語句確實合法的。
答案是:這種類型的語句允許使用在一些特定的類型中。Integer是一個資料類型,而對於資料類型來說,這種語句是允許的。
如果我們將 Employee 的方法 myCompare 變成非靜態,然後這樣使用:Employee::myCompare,就會出編譯錯誤:No Suitable Method Found。
構造方法引用
構造方法引用被用作引用一個構造方法而不執行個體化指定的類。
構造方法引用是 JavaSE 8 的一個新的特性。我們可以構造一個構造方法的引用,並且將它作為參數傳遞給目標類型。
當我們使用方法引用的時候,我們引用一個已有的方法使用他們。同樣的,在使用構造方法引用的時候,我們建立一個已有的構造方法的引用。
上一節中我們已經看到了構造方法引用的文法類名::new,這看起來很像方法引用。這種構造方法的引用可以分配給目標 functional interfaces 的執行個體。一個類可能有多個構造方法,在這種情況下,編譯器會檢查 functional interfaces 的類型,最終找到最好的匹配。
對我來說寫出第一個構造方法引用的程式有些困難,雖然我理解了他的文法,但是我卻不知道怎麼使用它,以及它有什麼用。最後,我花費了很久的努力,終於“啊,找到了...”,看看下面的程式吧。
public class ConstructorReference { public static void main(String[] ar){ MyInterface in = MyClass::new; System.out.println("->"+in.getMeMyObject()); }}interface MyInterface{ MyClass getMeMyObject();}class MyClass{ MyClass(){}}
輸出是:
這看起來有點神奇是吧,這個介面和這個類除了介面中聲明的方法的傳回值是 MyClass 類型的,沒有任何關係。
這個例子又激起了我心中的另一個問題:怎樣執行個體化一個帶參數的構造方法引用?看看下面的程式:
public class ConstructorReference { public static void main(String[] ar){ EmlpoyeeProvider provider = Employee::new; Employee emp = provider.getMeEmployee("John", 30); System.out.println("->Employee Name: "+emp.name); System.out.println("->Employee Age: "+emp.age); }}interface EmlpoyeeProvider{ Employee getMeEmployee(String s, Integer i);}class Employee{ String name; Integer age; Employee (String name, Integer age){ this.name = name; this.age = age; }}
輸出是:
->Employee Name: John->Employee Age: 30
在看完這篇文章之前,讓我們再來看一看JavaSE8中的最酷的一個特性--預設方法(Default Methods)
預設方法(Default Methods)
JavaSE8 中將會引入一個叫做預設方法的概念。早起的 Java 版本的介面擁有非常嚴格的介面,介面包含了一些抽象方法的聲明,所有非抽象的實作類別必須要提供所有這些抽象方法的實現,甚至是這些方法沒有用或者不合適出現在一些特殊的實作類別中。在即將到來的Java 版本中,允許我們在介面中定義方法的預設實現。廢話不多說,看下面:
public class DefaultMethods { public static void main(String[] ar){ NormalInterface instance = new NormalInterfaceImpl(); instance.myNormalMethod(); instance.myDefaultMethod(); }}interface NormalInterface{ void myNormalMethod(); void myDefaultMethod () default{ System.out.println("-> myDefaultMethod"); }}class NormalInterfaceImpl implements NormalInterface{ @Override public void myNormalMethod() { System.out.println("-> myNormalMethod"); }}
輸出是:
上面的介面中聲明了兩個方法,但是這個介面的實作類別只實現了其中一個,因為 myDefaultMethod 使用 default 修飾符標記了,而且提供了一個方法塊用作預設實現。通用的重載規則在這裡仍然生效。如果實作類別實現了介面中的方法,調用的時候將是調用類中的方法,否則,預設實現將被調用。
整合父介面的介面可以增加、改變、移除父介面中的預設實現。
interface ParentInterface{ void initiallyNormal(); void initiallyDefault () default{ System.out.println("-> myDefaultMethod"); }}interface ChildInterface extends ParentInterface{ void initiallyNormal() default{ System.out.println("now default - > initiallyNormal"); } void initiallyDefault (); //Now a normal method}
在這個例子中,ParentInterface 定義了兩個方法,一個是正常的,一個是有預設實現的,子介面只是簡單的反了過來,給第一個方法添加了預設實現,給第二個方法移除了預設實現。
設想一個類繼承了類 C ,實現了介面 I ,而且 C 有一個方法,而且跟I中的一個提供預設方法的方法是重載相容的。在這種情況下,C中的方法會優先於I中的預設方法,甚至C中的方法是抽象的時候,仍然是優先的。
public class DefaultMethods { public static void main(String[] ar){ Interfaxe impl = new NormalInterfaceImpl(); impl.defaultMethod(); }}class ParentClass{ public void defaultMethod() { System.out.println("->ParentClass"); }}interface Interfaxe{ public void defaultMethod() default{ System.out.println("->Interfaxe"); }}class NormalInterfaceImpl extends ParentClass implements Interfaxe{}
輸出是:
第二個例子是,我的類實現了兩個不同的介面,但是兩個介面中都提供了相同的具有預設實現的方法的聲明。在這種情況下,編譯器將會搞不清楚怎麼回事,實作類別必須選擇兩個的其中一個實現。這可以通過如下的方式來使用 super 來搞定。
public class DefaultMethods { public static void main(String[] ar){ FirstInterface impl = new NormalInterfaceImpl(); impl.defaultMethod(); }}interface FirstInterface{ public void defaultMethod() default{ System.out.println("->FirstInterface"); }}interface SecondInterface{ public void defaultMethod() default{ System.out.println("->SecondInterface"); }}class NormalInterfaceImpl implements FirstInterface, SecondInterface{ public void defaultMethod(){ SecondInterface.super.defaultMethod(); }}
輸出是:
現在,我們已經看完了 Java 閉包的介紹。這個文章中,我們接觸到了 Functional Interfaces 和 Java Closure ,理解了 Java 的 Lambda 運算式,方法引用和構造方法引用。而且我們也寫出了 Lambda 運算式的 Hello World 例子。
JavaSE8 很快就要到來了,我將很高興的擁抱這些新特性,也許這些新特性還是有些迷惑不清,但是我相信,隨著時間的推移,會變得越來越好。