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
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() |
本文由樊兔教育圖二UR整理髮布,樊兔教育是一個泛網際網路職業教育平臺,官網地址:http://ftuedu.com/