Lambda 是啥玩意
簡單來說,Lambda 就是一個匿名的方法,就這樣,沒啥特別的。它采用一種非常簡潔的方式來定義方法。當你想傳遞可復用的方法片段時,匿名方法非常有用。例如,將一個方法傳遞給另外一個方法。
Tips
其實很多主流語言早已支持 lambda 表達式,例如,Scala,C#,Objective-C,Ruby,C++(11), Python等等。所以也不是啥新玩意兒。
匿名方法 VS 匿名類
需要謹記一點,在 Java 里,匿名方法和匿名類並不是相同的。匿名類仍然需要實例化對象,匿名類雖然沒有明確的名字,但它只有是一個對象時才能夠使用。
而匿名方法並不需要給它分配實例,方法與作用的數據分離,而對象與它所作用的數據密切相關。
Java 中的 Lambda 表達式
在 Java 8之前,一個實現了只有一個抽象方法的接口的匿名類看起來更像Lambda 表達式。下面的代碼中,anonymousClass
方法調用waitFor
方法,參數是一個實現接口的Condition
類,實現的功能為,當滿足某些條件,Server 就會關閉。
下面的代碼是典型的匿名類的使用。
void anonymousClass() {
final Server server = new HttpServer();
waitFor(new Condition() {
@Override
public Boolean isSatisfied() {
return !server.isRunning();
}
}
下面的代碼用 Lambda 表達式實現相同的功能:
void closure() {
Server server = new HttpServer();
waitFor(() -> !server.isRunning());
}
其實,上面的waitFor
方法,更接近於下面的代碼的描述:
class WaitFor {
static void waitFor(Condition condition) throws
InterruptedException {
while (!condition.isSatisfied())
Thread.sleep(250);
}
}
一些理論上的區別
實際上,上面的兩種方法的實現都是閉包,后者的實現就是Lambda 表示式。這就意味着兩者都需要持有運行時的環境。在 Java 8 之前,這就需要把匿名類所需要的一切復制給它。在上面的例子中,就需要把 server
屬性復制給匿名類。
因為是復制,變量必須聲明為 final 類型,以保證在獲取和使用時不會被改變。Java 使用了優雅的方式保證了變量不會被更新,所以我們不用顯式地把變量加上 final 修飾。
Lambda 表達式則不需要拷貝變量到它的運行環境中,從而 Lambda 表達式被當做是一個真正的方法來對待,而不是一個類的實例。
Lambda 表達式不需要每次都要被實例化,對於 Java 來說,帶來巨大的好處。不像實例化匿名類,對內存的影響可以降到最小。
總體來說,匿名方法和匿名類存在以下區別:
- 類必須實例化,而方法不必;
- 當一個類被新建時,需要給對象分配內存;
- 方法只需要分配一次內存,它被存儲在堆的永久區內;
- 對象作用於它自己的數據,而方法不會;
- 靜態類里的方法類似於匿名方法的功能。
一些具體的區別
匿名方法和匿名類有一些具體的區別,主要包括獲取語義和覆蓋變量。
獲取語義
this 關鍵字是其中的一個語義上的區別。在匿名類中,this 指的是匿名類的實例,例如有了內部類為 Foo$InnerClass
,當你引用內部類閉包的作用域時,像Foo.this.x
的代碼看起來就有些奇怪。
在 Lambda 表達式中,this 指的就是閉包作用域,事實上,Lambda 表達式就是一個作用域,這就意味着你不需要從超類那里繼承任何名字,或是引入作用域的層級。你可以在作用域里直接訪問屬性,方法和局部變量。
例如,下面的代碼中,Lambda 表達式可以直接訪問firstName
變量。
public class Example {
private String firstName = "Tom";
public void example() {
Function<String, String> addSurname = surname -> {
// equivalent to this.firstName
return firstName + " " + surname; // or even,
};
}
}
這里的firstName
就是this.firstName
的簡寫。
但是在匿名類中,你必須顯式地調用firstName
,
public class Example {
private String firstName = "Jerry";
public void anotherExample() {
Function<String, String> addSurname = new Function<String,
String>() {
@Override
public String apply(String surname) {
return Example.this.firstName + " " + surname;
}
};
}
}
覆蓋變量
在 Lambda 表達式中,
public class ShadowingExample {
private String firstName = " Tim";
public void shadowingExample(String firstName) {
Function<String, String> addSurname = surname -> {
return this.firstName + " " + surname;
};
}
}
因為 this 在Lambda 表達式中,它指向的是一個封閉的作用域,所以this.firstName
對應的值是“Tim”,而不是跟它同名的參數的值。如果去掉this,那么引用的則是方法的參數。
在上面的例子中,如果用匿名類來實現的話,firstName
指的就是方法的參數;如果想訪問最外面的firstName
,則使用Example.this.firstName
。
public class ShadowingExample {
private String firstName = "King";
public void anotherShadowingExample(String firstName) {
Function<String, String> addSurname = new Function<String,
String>() {
@Override
public String apply(String surname) {
return firstName + " " + surname;
}
};
}
}
Lambda 表達式基本語法
Lambda 表達式基本上就是匿名函數塊。它更像是內部類的實例。例如,我們想對一個數組進行排序,我們可以使用Arrays.sort
方法,它的參數是Comparator
接口,類似於下面的代碼。
Arrays.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first.compareTo(second);
}
});
參數里的Comparator
實例就是一個抽象片段,本身沒有別的。在這里只有在 sort
方法中被使用。
如果我們用新的語法來替換,用 Lambda 表達式的方式來實現:
Arrays.sort(numbers, (first, second) -> first.compareTo(second));
這種方式更加簡潔,實際上,Java 把它當做Comparator
類的實例來對待。如果我們把 sort
的第二個參數從 Lambda 表達式中抽取出來,它的類型為Comparator<Integer>
。
Comparator<Integer> ascending = (first, second) -> first.compareTo(second);
Arrays.sort(numbers, ascending);
語法分解
你可以把單一的抽象方法轉換成 Lambda 表達式。
舉例,如果我們有一個接口名為Example
,里面只有一個抽象方法apply
,該抽象方法返回某一類型。
interface Example {
R apply(A args);
}
我們可以匿名實現此接口里的方法:
new Example() {
@Override
public R apply(A args) {
body
}
};
轉換成 Lambda 表達式的話,我們去掉實例和聲明,去掉方法的細節,只保留方法的參數列表和方法體。
(args) {
body
}
我們引入新的符號(->)來表示 Lambda 表達式。
(args) -> {
body
}
拿之前排序的方法為例,首先我們用匿名類來實現:
Arrays.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first.compareTo(second);
}
});
下一步,去掉實例和方法簽名:
Arrays.sort(numbers, (Integer first, Integer second) {
return first.compareTo(second);
});
引用 Lambda 表達式:
Arrays.sort(numbers, (Integer first, Integer second) -> {
return first.compareTo(second);
});
完成!但有些地方可以進一步優化。你可以去掉參數的類型,編譯器已經足夠聰明知道參數的類型。
Arrays.sort(numbers, (first, second) -> {
return first.compareTo(second);
});
如果是一個簡單的表達式的話,例如只有一行代碼,你可以去掉方法體的大括號,如果有返回值的話,return 關鍵字也可以去掉。
Arrays.sort(numbers, (first, second) -> first.compareTo(second));
如果Lambda 只有一個參數的話,參數外面的小括號也可以去掉。
(x) -> x + 1
去掉小括號后,
x -> x + 1
下一步我們做下總結,
(int x, int y) -> { return x + y; }
(x, y) -> { return x + y; }
(x, y) -> x + y; x -> x * 2
() -> System.out.println("Hello");
System.out::println;
第一個方式是完整的 Lambda 的聲明和使用的方式,不過有些冗余,其實,參數的類型可以省略;
第二個方式是去掉參數類型的 Lambda 表達式;
第三個方式是,如果你的方法體只有一行語句,你可以直接省略掉大括號和 return 關鍵字;
第四個方式是沒有參數的 Lambda 表達式;
第五個方式是Lambda 表達式的變種:是Lambda 表達式的一種簡寫,稱為方法引用。例如:
System.out::println;
實際上它是下面Lambda 表達式的一種簡寫:
(value -> System.out.prinltn(value)
深入 Lambda表達式
函數式接口
Java 把 Lambda表達式當作是一個接口類型的實例。它把這種形式被稱之為函數式接口。一個函數式接口就是一個只有單一方法的接口,Java把這種方法稱之為“函數式方法”,但更常用的名字為單一抽象方法(single abstract method" 或 SAM)。例如JDK中存在的接口例如Runnable
和Callable
。
@FunctionalInterface
Oracle 引入了一個新的注解為@FunctionalInterface
, 用來標識一個接口為函數式接口。它基本上是用來傳達這一用途,除此而外,編輯器還會做一些額外的檢查。
比如,下面的接口:
public interface FunctionalInterfaceExample {
// compiles ok
}
如果加上@FunctionalInterface
注解,則會編譯錯誤:
@FunctionalInterface // <- error here
public interface FunctionalInterfaceExample {
// doesn't compile
}
編譯器就會報錯,錯誤的詳細信息為“Invalid '@FunctionalInterface' annotation; FunctionalInterfaceExample is not a functional interface”。意思是沒有定義一個單一的抽象方法。
而如果我們定義了兩個抽象方法會如何?
@FunctionalInterface
public interface FunctionalInterfaceExample {
void apply();
void illegal(); // <- error here
}
編譯器再次報錯,提示為"multiple, non-overriding abstract methods were found"。所以,一旦使用了此注解,則在接口里只能定義一個抽象方法。
而現在有這樣一種情況,如歌一個接口繼承了另一個接口,會怎么辦?我們創建一個新的函數式接口為A,定義了另一個接口B,B繼承A,則B仍然是一個函數式接口,它繼承了A的apply
方法。
@FunctionalInterface
interface A {
abstract void apply();
}
interface B extends A {
如果你想看起來更加清晰,可以復寫父類的方法:
@FunctionalInterface
interface A {
abstract void apply();
}
interface B extends A {
@Override
abstract void apply();
}
我們可以用下面的代碼來測試一下上面的兩個接口是否為函數式接口:
@FunctionalInterface
public interface A {
void apply();
}
public interface B extends A {
@Override
void apply();
}
public static void main(String... args) {
A a = () -> System.out.println("A");
B b = () -> System.out.println("B");
a.apply(); // 打印:A
b.apply(); // 打印:B
}
如果B接口繼承了A接口,那么在B接口中就不能定義新的方法了,否則編譯器會報錯。
除了這些,在Java 8 中接口有了一些新的改進:
- 可以添加默認方法;
- 可以包含靜態接口方法;
- 在
java.util.function
包中增加了一些新的接口,例如,Function
和Predicate
。
方法引用
簡單來說,方法引用就是 Lambda 表達式的一種簡寫。當你創建一個 Lambda 表達式時,你創建了一個匿名方法並提供方法體,但你使用方法引用時,你只需要提供已經存在的方法的名字,它本身已經包含方法體。
它的基本語法如下;
Class::method
或一個更加簡潔明了的例子:
String::valueOf
"::"符號前面表示的是目標引用,后面表示方法的名字。所以,在上面的例子,String 類作為目標類,用來尋找它的方法valueOf
,我們指的就是 String 類上的靜態方法。
public static String valueOf(Object obj) { ... }
"::"稱之為定界符,當我們使用它的時候,只是用來引用要使用的方法,而不是調用方法,所以不能在方法后面加()。
String::valueOf(); // error
你不能直接調用方法引用,只是用來替代 Lambda 表達式,所以,哪里使用 Lambda 表達式了,哪里就可以使用方法引用了。
所以,下面的代碼並不能運行:
public static void main(String... args) {
String::valueOf;
}
這是因為該方法引用不能轉化為Lambda 表達式,因為編譯器沒有上下文來推斷要創建哪種類型的Lambda。
我們知道這個引用其實是等同於下面的代碼:
(x) -> String.valueOf(x)
但編譯器還不知道。雖然它可以知道一些事情。它知道,作為一個Lambda,返回值應該是字符串類型,因為valueOf方法的返回值為字符串類型。但它不知道作為論據需要提供什么信息。我們需要給它一點幫助,給它更多的上下文信息。
下面我們創建一個函數式接口Conversion
,
@FunctionalInterface
interface Conversion {
String convert(Integer number);
}
接下來我們需要創建一個場景去使用這個接口作為一個 Lambda,我們定義了下面的方法:
public static String convert(Integer number, Conversion function) {
return function.convert(number);
}
其實,我們已經給編譯器提供了足夠多的信息,可以把一個方法引用轉換成一個等同的 Lambda。當我們調用convert
方法時,我們可以把如下代碼傳遞給 Lambda。
convert(100, (number) -> String.valueOf(number));
我們可以用把上面的 Lambda 替換為方法引用,
convert(100, String::valueOf);
另一種方式是我們告訴編譯器,把引用分配給一個類型:
Conversion b = (number) -> String.valueOf(number);
用方法引用來表示:
Conversion b = String::valueOf
方法引用的種類
在 Java 中,有四種方法引用的類型:
- 構造方法引用;
- 靜態方法引用:
- 兩種實例方法引用。
最后兩個有點混亂。第一種是特定對象的方法引用,第二個是任意對象的方法引用,而是特定類型的方法引用。區別在於你想如何使用該方法,如果你事先並不知道有沒有實例。
構造方法引用
構造方法的基本引用如下:
String::new
它會創建一個 Lambda 表達式,然后調用String 無參的構造方法。
它實際上等同於:
() -> new String()
需要注意的是構造方法引用沒有括號,它只是引用,並不是調用,上面的例子只是引用了 String類的構造方法,並沒有真正去實例化一個字符串對象。
接下來我們看一個實際應用構造方法引用的例子。
看先的例子,循環十遍為 list 增加對象。
public void usage() {
List<Object> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new Object());
}
}
如果我們想復用實例化的功能,我們可以抽取出一個新的方法initialise
用factory
創建對象。
public void usage() {
List<Object> list = new ArrayList<>();
initialise(list, ...);
}
private void initialise(List<Object> list, Factory<Object> factory){
for (int i = 0; i < 10; i++) {
list.add(factory.create());
}
}
Factory
是一個函數式接口,包含一個create
方法,此方法返回 Object 對象,我們可以用 Lambda 的方式向 list 中添加對象。
public void usage() {
List<Object> list = new ArrayList<>();
initialise(list, () -> new Object());
}
或者我們用構造方法引用的方式來替換:
public void usage() {
List<Object> list = new ArrayList<>();
initialise(list, Object::new);
}
上面的方法其實還有待改進,上面只是創建 Object 類型的對象,我們可以增加泛型,實現可以創建更多類型的方法。
public void usage() {
List<String> list = new ArrayList<>();
initialise(list, String::new);
}
private <T> void initialise(List<T> list, Factory<T> factory) {
for (int i = 0; i < 10; i++) {
list.add(factory.create());
}
}
到現在為知,我們演示的都是無參的構造方法的引用,如果是帶有參數的構造方法的引用該如何處理呢?
當有多個構造函數時,使用相同的語法,但編譯器計算出哪個構造函數是最佳匹配。它基於目標類型和推斷功能接口,它可以用來創建該類型。
例如,我們有個 Person 類,它有一個多個參數的構造方法。
class Person {
public Person(String forename, String surname, LocalDate
birthday, Sex gender, String emailAddress, int age) {
// ...
}
回到上面的例子,我們可以如下使用:
initialise(people, () -> new Person(forename, surname, birthday,
gender, email, age));
但是如果想使用這個構造方法引用,則需要 Lambda 表達式提供如下參數:
initialise(people, () -> new Person(forename, surname, birthday,
gender, email, age));
特定對象的方法引用
下面是特定對象的方法引用的例子:
x::toString
x
就是我們想要得到的對象。它等同於下面的Lambda 表達式。
() -> x.toString()
這種方法引用可以為我們提供便利的方式在不同的函數式接口類型中進行切換。看例子:
Callable<String> c = () -> "Hello";
Callable
的方法為call
,當被調用時返回“Hello”。
如果我們有另外一個函數式接口Factory
,我們可以使用方法引用的方式來轉變Callable
這個函數式接口。
Factory<String> f = c::call;
我們可以重新創建一個 Lambda表達式,但是這個技巧是重用預定義的Lambda的一個有用的方式。 將它們分配給變量並重用它們以避免重復。
我們有下面一個例子:
public void example() {
String x = "hello";
function(x::toString);
}
這個例子中方法引用使用了閉包。他創建了一個 Lambda用來調用x
對象上的toString
方法。
上面function
方法的簽名和實現如下所示:
public static String function(Supplier<String> supplier) {
return supplier.get();
}
函數式接口Supplier
的定義如下:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
當使用此方法時,它通過get
方法返回一個字符串,而且這是唯一的在我們的結構中獲取字符串的方式。它等同於:
public void example() {
String x = "";
function(() -> x.toString());
}
需要注意的是,這里的 Lambda 表達式沒有參數。這表明x
變量在Lambda的局部作用域里是不可用的,如果可用必須要放在它的作用域之外。我們必須要掩蓋變量x
。
如果用匿名類來實現的話,應該是下面的樣子,這些需要主意,x
變量是如何傳遞的。
public void example() {
String x = "";
function(new Supplier<String>() {
@Override
public String get() {
return x.toString(); // <- closes over 'x'
}
});
}
任意對象的實例方法引用(實例隨后提供)
最后一種類型的實例方法引用的格式是這樣的:
Object::toString
盡管在“::”左邊指向的是一個類(有點類似於靜態方法引用),實際上它是指向一個對象,toString
方法是Object
類上的實例方法,不是靜態方法。您可能不使用常規實例方法語法的原因是,還沒有引用的實例。
在以前,當我們調用x::toString
時,我們是知道x
的類型,但是有些情況我們是不知道的,但你仍然可以傳遞一個方法引用,但是在后面使用此語法時需要提供對應的類型。
例如,下面的表達式等同於x
沒有限制的類型。
(x) -> x.toString()
有兩種不同的實例方法的引用基本是學術上的。有時候,你需要傳遞一些東西,其他時候,Lambda 的用法會為你提供。
這個例子類似於一個常規的方法引用;它這次調用String 對象的toString
方法,該字符串提供給使用 Lambda 的函數,而不是從外部作用域傳遞的函數。
public void lambdaExample() {
function("value", String::toString);
}
這個String
看起來像是引用一個類,其實是一個實例。是不是有些迷惑,為了能清晰一些,我們需要看一個使用 Lambda 表達式的方法,如下:
public static String function(String value, Function<String, String> function) {
return function.apply(value);
}
所以,這個 String 實例直接傳遞給了方法,它看起來像一個完全合格的Lambda。
public void lambdaExample() {
function("value", x -> x.toString());
}
上面的代碼可以簡寫成String::toString
, 它是在說在運行時給我提供對象實例。
如果你想用匿名類展開加以理解,它是這個樣子的。參數x
是可用的並沒有被遮蔽,所以它更像是Lambda 表達式而不是閉包。
public void lambdaExample() {
function("value", new Function<String, String>() {
@Override
// takes the argument as a parameter, doesn't need to close
over it
public String apply(String x) {
return x.toString();
}
});
}
方法引用的總結
Oracle描述了四種類型的方法引用,如下所示。
種類 | 舉例 |
---|---|
靜態方法引用 | ContainingClass::staticMethodName |
特定對象的實例方法引用 | ContainingObject::instanceMethodName |
特定類型的任意對象的實例方法引用 | ContainingType::methodName |
構造方法引用 | ClassName::new |
下面是方法引用的語法和具體的例子。
種類 | 語法 | 舉例 |
---|---|---|
靜態方法引用 | Class::staticMethodName | String::valueOf |
特定對象的實例方法引用 | object::instanceMethodName | x::toString |
特定類型的任意對象的實例方法引用 | Class::instanceMethodName | String::toString |
構造方法引用 | ClassName::new | String::new |
最后,上面的方法引用等同於下面對應的 Lambda 表達式。
種類 | 語法 | Lambda |
---|---|---|
靜態方法引用 | Class::staticMethodName | (s) -> String.valueOf(s) |
特定對象的實例方法引用 | object::instanceMethodName | () -> "hello".toString() |
特定類型的任意對象的實例方法引用 | Class::instanceMethodName | (s) -> s.toString() |
構造方法引用 | ClassName::new | () -> new String() |
目前為止,Labmbda 的主要內容已經介紹完畢。