Java在JDK1.8之後增加一個新鮮物種,叫做lambda表示式,實際的寫法是一個小箭頭:->。它是什麼東西呢?我們可以很容易地在各大技術社群查到,這東西叫函式。那函式又是什麼東西?這裡不得不吐槽一下最開始翻譯英文文件的那批人,給後世引入的名詞讓很多人摸不著頭腦,什麼套接字,控制代碼等等。函式的英文是Function,最早是由清朝的數學家李善蘭翻譯,他在他的著作《代數學》解釋說:
凡此變數中函彼變數者,則此為彼之函式
意思就是一個量隨著另一個量的變化而變化。其實理解起來並不困難,但把這東西突然說給寫了很多年的面向物件的Java的人,似乎確實有點懵。儘管函數語言程式設計在很早之前就廣泛應用了,但在很多Java開發者心裡,函式似乎跟Java從來不沾邊。
追根溯源匿名函式首先不要神化所謂的lambda表示式,它還有個名字叫匿名函式,也不要被這個名字嚇到。函式就可以理解為方法,一個方法得有方法名、引數、返回值型別、return語句,匿名函式就只要一個非常簡單的宣告就可以實現需要的功能,比如計算兩數之和(以Java舉例):
(a, b) -> a + b
上古時代的程式設計師們使用它的理由是,很多時候只是一個小功能不會再複用的(肯定不是兩數之和,這裡可能特定場景的業務功能),沒必要完整的去宣告一個函式。它沒有實際的方法論,更多的是一個程式設計的風格。匿名函式這個特性最早是1958年就被加入到LISP中,後面誕生很多語言都借鑑了這個特性,LISP永遠滴神!
Java lambda表示式的幾種寫法各個語言對於lambda的寫法五花八門,這裡列舉Java的幾種寫法。
無引數,且返回值為voidRunnable run = () -> System.out.println("Hello, world.")
只包含一個引數,可以引數的省略括號int res = param -> param + 1
包含多個引數的,箭頭後為一個表示式(a, b) -> a + b
包含多個引數,但有明確的型別(long id, String name) -> "id: " + id + ", name:" + name
包含程式碼塊(a, b) -> { return a + b; }
方法引用IntBinaryOperator sum = Integer::sum;
以上所有的引數型別都是由Java的編譯器推斷出來的,當然也可以指定明確的型別。對於方法引用這種寫法也一樣,根據函式式介面的型別在指定的類中自動匹配引數型別和返回型別一致的方法。可以認為這是Java擁抱函式式的第一步,使用了匿名函式的外形,讓程式碼看起來更加整潔精煉。但這個->的運算子不是憑空而來,它的背後是一個個的函式式介面。
函式式介面函式式介面其實也沒什麼唬人的地方,首先看一下oracle官方的定義:
Conceptually, a functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract.
However, the compiler will treat any interface meeting the definition of a functional interface as a functional interface regardless of whether or not a FunctionalInterface annotation is present on the interface declaration.
說的意思就是函式式介面只有一個抽象方法的介面。Java 8引入了介面的預設方法機制,就是介面中的方法標記了default就可以直接在介面中寫實現。介面中未實現的方法都是抽象方法,abstract關鍵字可以省略。有且只有一個抽象方法的介面就可以稱之為函式式介面。為了更明確的宣告,jdk提供了一個註解:FunctionalInterface。這個註解只是個標記,沒有其他額外的處理和含義。如果你不用@FunctionalInterface標記但是符合定義的描述,編譯器依然會認為這是個函式式介面,都可以建立lambada表示式,方法引用。
函式式介面跟lambda表示式的關係又是什麼呢?下面舉個例子:
@FunctionalInterfaceinterface Square { int calculate(int x); }
這是一個計算的數值的函式式介面,輸入一個引數,返回計算完成的值,很簡單。下面我們來使用它:
public static void main(String args[]) { // 定義這個函式 Square s = (int x)->x*x; // 只有一個引數,也可以寫成 x -> x*x int a = 5; // 使用這個函式 int ans = s.calculate(a); System.out.println(ans); }
我們沒有給Square指定範型,因為這裡編譯器可以型別推斷出這個方法會返回int。下面我換一種寫法,你一定非常熟悉:
public static void main(String args[]) { // 匿名內部類實現calculate方法 Square s = new Square() { @Override public int calculate(int x) { return x*x; } }; int a = 5; // 使用這個介面 int ans = s.calculate(a); System.out.println(ans); }
是不是一下子就沒有秘密了?是的。上下兩個寫法幾乎是等價的。把原來冗長的匿名內部類方法實現改成一行的‘函式’,結合編譯器的型別推斷,可以讓程式碼看起來高大上一些。如果感覺calculate我需要更復雜一些的實現,也是可以的,稍微複雜點的邏輯就得用括號括起來,並且需要一個return語句,如下:
Square s = x -> { int a = 3; x = a + x; // something complicate return x * x;}
另一個常見的例子是:
@FunctionalInterfacepublic interface Runnable { public abstract void run();}
你幾乎可以在任何地方使用它,就像這樣:
() -> System.out.println("Hello, world.");
這個run方法沒有引數,返回值也是void。任何返回void的方法都可以這麼執行,但你總這樣無意義的用是不是就有點缺心眼。
總結起來,只要符合函式式介面的定義,並且引數和返回值都符合抽象方法的定義,就可以用這個->表示這個抽象方法的實現。有了這個認知的基礎,我們就可以任意發揮了,其實官方早就定義好了一些預置的函式式介面,能覆蓋大多數場景。
使用官方API下面簡單介紹一下官方的幾個介面和使用場景。
Function@FunctionalInterfacepublic interface Function<T, R> { R apply(T t); }
輸入一個引數T,返回另一個物件R。標準的輸入輸出,用途廣泛,所以叫function。
Consumer@FunctionalInterfacepublic interface Consumer<T> { void accept(T t);}
輸入一個引數T,返回void。消費資源,類似於mq消費訊息,你用給mq返回任何東西。
Supplier@FunctionalInterfacepublic interface Supplier<T> { T get();}
無引數,返回一個物件T。索取資源,無需引數,每呼叫一次即可獲取某個資源。
Predicate@FunctionalInterfacepublic interface Predicate<T> { boolean test(T t);}
輸入一個引數T,返回一個布林。俗稱斷言,輸入一個值,判斷對錯。
以上幾個官方定義的函式式介面看著名字挺唬人的,第一看看過去以為又有什麼高深的實現要去記去背,其實沒有,就是一些介面定義,只不過在特定場合給起了特定的名字。只要入參和返回值符合定義,就可以隨便實現,隨便用,就是這麼簡單。其實以上幾個介面在Stream的使用非常廣泛,不過大多數人可能先記住了一些常用寫法,但沒有深究為什麼可以這麼寫。
值得說明的一點是,函式式介面可以用來實現惰性求值。傳統的傳值會立刻計算出所有引數的值,但如果把函式式介面當引數傳遞,在呼叫實現方法之前是不會求值的。舉個例子:
public static String getName() { System.out.print("method called\n"); return "called"; } public static void main(String[] args) { String name1 = Optional.of("String").orElse(getName()); //output: method called String name2 = Optional.of("String").orElseGet(()->getName());// output: } ... public T orElse(T other) { return value != null ? value : other; } public T orElseGet(Supplier<? extends T> other) { return value != null ? value : other.get(); }
在Optional中使用orElse時,無論前面的是否為空都會執行引數。而orElseGet接收一個Supplier,當value不為空的時候是不會執行other.get()的,也就是不會求值,相對效能要好一些。
總結Java 8釋出時至今日已有8年多了,很多人在寫Java的過程中可能對於這個->的使用並不多,更多可能也是來自IDE的寫法轉換建議。初學或者非初學往往會被函式、lambda這樣的詞彙“心生畏懼”,感覺是個特別高階的理論,然後一直半懂不懂。希望透過這篇文章讓更多的人明白,lambda只是個名詞罷了,包裝的背後還是Java爛熟的那套機制。