轉(zhuǎn):Java 8 重要語(yǔ)言特性:lambda 表達(dá)式

轉(zhuǎn)載自:《深入理解Java 8 Lambda(語(yǔ)言篇——lambda,方法引用,目標(biāo)類(lèi)型和默認(rèn)方法)》——Lucida

:本文是筆者在上述地址學(xué)習(xí) Java SE 8 Lambda 表達(dá)式的筆記。筆者的學(xué)習(xí)習(xí)慣,是在學(xué)習(xí)過(guò)程中將內(nèi)容敲打一遍,記憶會(huì)更加深刻。本文只節(jié)選了原文一部分,更多內(nèi)容詳見(jiàn)原文。


一. 背景

不過(guò)有些 Java 對(duì)象只是對(duì)單個(gè)函數(shù)的封裝。例如下面這個(gè)典型用例:Java API 中定義了一個(gè)接口(一般被稱(chēng)為回調(diào)接口),用戶通過(guò)提供這個(gè)接口的實(shí)例來(lái)傳入指定行為,例如:

public interface ActionListener {
    void actionPerformed(ActionEvent e);
}

這里并不需要專(zhuān)門(mén)定義一個(gè)類(lèi)來(lái)實(shí)現(xiàn) ActionListener 接口,因?yàn)樗粫?huì)在調(diào)用處被使用一次。用戶一般會(huì)使用匿名類(lèi)型把行為內(nèi)聯(lián)(inline):

button.addActionListener(new ActionListener) {
    public void actionPerformed(ActionEvent e) {
        ui.dazzle(e.getModifiers());
    }
}

很多庫(kù)都依賴(lài)于上面的模式。對(duì)于并行 API 更是如此,因?yàn)槲覀冃枰汛龍?zhí)行的代碼提供給并行API,并行編程是一個(gè)非常值得研究的領(lǐng)域,因?yàn)樵谶@里摩爾定律得到了重生:盡管我們沒(méi)有更快的 CPU,但是我們有更多的 CPU。
隨著回調(diào)模式和函數(shù)式編程風(fēng)格的日益流行,我們需要在 Java 中提供一種盡可能輕量級(jí)的將代碼封裝為數(shù)據(jù)的方法。但匿名內(nèi)部類(lèi)并不是一個(gè)好的選擇,因?yàn)椋?/p>

  1. 語(yǔ)法過(guò)于冗余
  2. 匿名類(lèi)中的this和變量名容易使人產(chǎn)生誤解;
  3. 類(lèi)型載入和實(shí)例創(chuàng)建語(yǔ)義不夠靈活;
  4. 無(wú)法捕獲非final的局部變量;
  5. 無(wú)法對(duì)控制流進(jìn)行抽象;

對(duì)于上述問(wèn)題,在 Java 8 中大多都被解決:

  • 提供更簡(jiǎn)潔的語(yǔ)法和局部作用域規(guī)則 -> 解決了問(wèn)題 1 和問(wèn)題 2
  • 提供更加靈活而且便于優(yōu)化的表達(dá)式語(yǔ)義 -> 繞開(kāi)了問(wèn)題 3
  • 允許編譯器推斷變量的“常量性” -> 減輕了問(wèn)題 4

二. 函數(shù)式接口

上面提到的 ActionListener 接口只有一個(gè)方法,大多數(shù)回調(diào)接口都擁有這個(gè)特征。比如 Runnable 接口和 Comparator 接口。我們把這些只擁有一個(gè)方法的接口稱(chēng)為函數(shù)式接口。編譯器會(huì)根據(jù)接口的結(jié)構(gòu)自行判斷。

  • 判斷過(guò)程并非簡(jiǎn)單的對(duì)接口方法計(jì)數(shù);
  • API 作者們可以通過(guò) @FunctionalInterface 注解來(lái)顯式指定一個(gè)接口是函數(shù)式接口,加上這個(gè)注解之后,編譯器就會(huì)驗(yàn)證該接口是否滿足函數(shù)式接口的要求。

函數(shù)式類(lèi)型的另一種方式,是引入一個(gè)全新的結(jié)構(gòu)化函數(shù)類(lèi)型:“箭頭”類(lèi)型。例如,一個(gè)接收 String 和 Object 并返回 int 的函數(shù)類(lèi)型可以被表示為:

(String, Object) -> int

但 Sun 公司最終出于下面的原因?qū)⑵浞穸ǎ?/p>

  • 它會(huì)為Java類(lèi)型系統(tǒng)引入額外的復(fù)雜度,并帶來(lái)結(jié)構(gòu)類(lèi)型指名類(lèi)型的混用。而 Java 幾乎全部使用指名類(lèi)型;
  • 它會(huì)導(dǎo)致類(lèi)庫(kù)風(fēng)格的分歧——一些類(lèi)庫(kù)會(huì)繼續(xù)使用回調(diào)接口,而另一些類(lèi)庫(kù)會(huì)使用結(jié)構(gòu)化函數(shù)類(lèi)型;
  • 它的語(yǔ)法會(huì)變得十分笨拙;
  • 每個(gè)函數(shù)類(lèi)型很難擁有其運(yùn)行時(shí)表示,使開(kāi)發(fā)者受到類(lèi)型擦除 (erasure) 的困擾和局限。例如:我們無(wú)法對(duì)方法 m(T->U) 和 m(X->Y) 進(jìn)行重載;

所以 Sun 公司最終選擇了“使用已知類(lèi)型”這種方法。因?yàn)楝F(xiàn)有的類(lèi)庫(kù)大量使用了函數(shù)式接口,通過(guò)沿用這種模式,我們使得現(xiàn)有類(lèi)庫(kù)能夠直接使用 lambda 表達(dá)式。

Java SE 7 中已經(jīng)存在的函數(shù)式接口如下:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.beans.PropertyChangeListener

除此之外,Java SE 8 中增加了一個(gè)新的包:java.util.function。它里面包含了常用的函數(shù)式接口,例如:

  • Predicate<T>: 接收 T 對(duì)象并返回 boolean;
  • Consume<T>: 接收 T 對(duì)象,不返回值;
  • Functio<T, R>: 接收 T 對(duì)象,返回 R 對(duì)象;
  • Supplie<T>: 提供 T 對(duì)象(例如工廠),不接收值;
  • UnaryOperato<T>: 接收 T 對(duì)象,返回 T 對(duì)象;
  • BinaryOperator<T>: 接收兩個(gè) T 對(duì)象,返回 T 對(duì)象;

除了上面的這些基本的函數(shù)式接口,還有一些針對(duì)原始類(lèi)型的特化函數(shù)式接口,例如 IntSupplier 和 LongBinaryOperator。(只為 int, long, double 提供了特化函數(shù)式接口,如果需要使用其它原始類(lèi)型則需要進(jìn)行類(lèi)型轉(zhuǎn)換)
同樣,還有一些針對(duì)多個(gè)參數(shù)的函數(shù)式接口,例如 BiFunction<T, U, R>,它接收 T 對(duì)象和 U 對(duì)象,返回 R 對(duì)象。

三. lambda 表達(dá)式

lambda 表達(dá)式是匿名方法,它提供了輕量級(jí)的語(yǔ)法,從而解決了匿名內(nèi)部類(lèi)帶來(lái)的冗余語(yǔ)法問(wèn)題(又被稱(chēng)為“高度問(wèn)題”)。下面是一些lambda表達(dá)式:

(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }

這幾個(gè)表達(dá)式的意義如下:

  • 第一個(gè):lambda 表達(dá)式接收 x 和 y 這兩個(gè)整形參數(shù)并返回它們的和;
  • 第二個(gè):lambda 表達(dá)式不接收參數(shù),返回整數(shù)'42';
  • 第三個(gè):lambda 表達(dá)式接收一個(gè)字符串并把它打印到控制臺(tái),不返回值。

lambda 表達(dá)式的語(yǔ)法由參數(shù)列表、箭頭符號(hào)->和函數(shù)體組成。其中函數(shù)體既可以是一個(gè)表達(dá)式,也可以是一個(gè)語(yǔ)句塊:

  • 表達(dá)式:表達(dá)式會(huì)被執(zhí)行然后返回執(zhí)行結(jié)果;
  • 語(yǔ)句塊:語(yǔ)句塊中的語(yǔ)句會(huì)被依次執(zhí)行,就像方法中的語(yǔ)句一樣;
    • return語(yǔ)句會(huì)把控制權(quán)交給匿名方法的調(diào)用者;
    • break和continue只能在循環(huán)中使用;
    • 如果函數(shù)體有返回值,那么函數(shù)體內(nèi)部的每一條路徑都必須返回值;

lambda 表達(dá)式也會(huì)經(jīng)常出現(xiàn)在嵌套環(huán)境中,比如說(shuō)作為方法的參數(shù)。為了使 lambda 表達(dá)式在這些場(chǎng)景下盡可能簡(jiǎn)潔,我們?nèi)コ瞬槐匾姆指舴?。不過(guò)在某些情況下我們也可以把它分為多行,然后用括號(hào)包起來(lái),就像其它普通表達(dá)式一樣。

下面是一些出現(xiàn)在語(yǔ)句中的lambda表達(dá)式:

FileFilter java = (File f) -> f.getName().endsWith("*.java");

String user = doPrivileged(() -> System.getProperty("user.name"));

new Thread(() -> {
    connectToService();
    sendNotification();
}).start();

四. 目標(biāo)類(lèi)型

對(duì)于給定的 lambda 表達(dá)式,它的類(lèi)型是由其上下文推導(dǎo)而來(lái)。例如,下面代碼中的 lambda 表達(dá)式類(lèi)型是 ActionListener:

ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());

這就意味著,同樣的 lambda 表達(dá)式在不同上下文里可以擁有不同的類(lèi)型。例如第一個(gè) lambda 表達(dá)式 () -> "done" 是 Callable 的實(shí)例,而第二個(gè) lambda 表達(dá)式則是 PrivilegedAction 的實(shí)例。

Callable<String> c = () -> "done";
PrivilegedAction<String> a = () -> "done";

編譯器負(fù)責(zé)推導(dǎo) lambda 表達(dá)式的類(lèi)型。它利用 lambda 表達(dá)式所在上下文所期待的類(lèi)型進(jìn)行推導(dǎo),這個(gè)被期待的類(lèi)型被稱(chēng)為目標(biāo)類(lèi)型。lambda 表達(dá)式只能出現(xiàn)在目標(biāo)類(lèi)型為函數(shù)式接口的上下文中。

當(dāng)然,lambda 表達(dá)式對(duì)目標(biāo)類(lèi)型也是有要求的。編譯器會(huì)檢查 lambda 表達(dá)式的類(lèi)型和目標(biāo)類(lèi)型的方法簽名是否一致。當(dāng)且僅當(dāng)下面所有條件均滿足時(shí),lambda 表達(dá)式才可以被賦給目標(biāo)類(lèi)型 T:

  • T 是一個(gè)函數(shù)式接口;
  • lambda 表達(dá)式的參數(shù)和 T 的方法參數(shù)在數(shù)量和類(lèi)型上一一對(duì)應(yīng)
  • lambda 表達(dá)式的返回值和 T 的方法返回值相兼容;
  • lambda 表達(dá)式內(nèi)所拋出的異常和 T 的方法 throws 類(lèi)型相兼容;

由于函數(shù)式接口的目標(biāo)類(lèi)型已經(jīng)了解 lambda 表達(dá)式的形式參數(shù)類(lèi)型,所以我們沒(méi)有必要把已知類(lèi)型再重復(fù)一遍,即 lambda 表達(dá)式的參數(shù)類(lèi)型可以從目標(biāo)類(lèi)型中得出。例如:

Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);

編譯器可以推導(dǎo)出 s1 和 s2 的類(lèi)型是 String。此外,當(dāng) lambda 的參數(shù)只有一個(gè)而且它的類(lèi)型可以被推導(dǎo)得知時(shí),該參數(shù)列表外面的括號(hào)可以被省略。例如:

FileFilter java = f -> f.getName().endsWith(".java");
button.addActionListener(e -> ui.dazzle(e.getModifiers()));

這些改進(jìn)展示了我們的設(shè)計(jì)目標(biāo):“不要把高度問(wèn)題轉(zhuǎn)化成寬度問(wèn)題?!闭Z(yǔ)法元素能夠盡可能的少,以便代碼的讀者能夠直達(dá) lambda 表達(dá)式的核心部分。

五. 目標(biāo)類(lèi)型的上下文

前文提到,lambda 表達(dá)式只能出現(xiàn)在擁有目標(biāo)類(lèi)型的上下文中。這些帶有目標(biāo)類(lèi)型的上下文有:

  • 變量聲明
  • 賦值
  • 返回語(yǔ)句
  • 數(shù)組初始化器
  • 方法和構(gòu)造方法的參數(shù)
  • lambda 表達(dá)式函數(shù)體
  • 條件表達(dá)式(? :)
  • 轉(zhuǎn)型(Cast)表達(dá)式

變量聲明、賦值、返回語(yǔ)句里,目標(biāo)類(lèi)型即是被賦值或被返回的類(lèi)型:

Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);

public Runnable toDoLater() {
    return () -> {
        System.out.println("later");
    }
}

數(shù)組初始化器和賦值類(lèi)似,只是這里的“變量”變成了數(shù)組元素,而類(lèi)型是從數(shù)組類(lèi)型中推導(dǎo)得知的:

filterFiles(new FileFilter[] {
    f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q")
});

方法參數(shù)的類(lèi)型推導(dǎo)要相對(duì)復(fù)雜,涉及到其它兩個(gè)語(yǔ)言特性重載解析參數(shù)類(lèi)型推導(dǎo)
重載解析會(huì)為一個(gè)給定的方法調(diào)用尋找最合適的方法聲明。由于不同的聲明具有不同的簽名,當(dāng) lambda 表達(dá)式作為方法參數(shù)時(shí),重載解析就會(huì)影響到 lambda 表達(dá)式的目標(biāo)類(lèi)型。編譯器會(huì)通過(guò)它所得之的信息來(lái)做出決定。如果 lambda 表達(dá)式具有顯式類(lèi)型(參數(shù)類(lèi)型被顯式指定),編譯器就可以直接使用 lambda 表達(dá)式的返回類(lèi)型;如果 lambda 表達(dá)式具有隱式類(lèi)型(參數(shù)類(lèi)型被推導(dǎo)而知),重載解析則會(huì)忽略 lambda 表達(dá)式函數(shù)體而只依賴(lài) lambda 表達(dá)式參數(shù)的數(shù)量。
如果在解析方法聲明時(shí)存在二義性,我們就需要利用轉(zhuǎn)型 (cast) 或顯式 lambda 表達(dá)式來(lái)提供更多的類(lèi)型信息。如果 lambda 表達(dá)式的返回類(lèi)型依賴(lài)于其參數(shù)的類(lèi)型,那么 lambda 表達(dá)式函數(shù)體有可能可以給編譯器提供額外的信息,以便其推導(dǎo)參數(shù)類(lèi)型。例如:

List<Person> ps = ...
Stream<String> names = ps.stream().map(p -> p.getName());

在上面的代碼中,ps 的類(lèi)型是 List<Person>,所以 ps.stream() 的返回類(lèi)型是 Stream<Person>。map() 方法接收一個(gè)類(lèi)型為 Function<T, R> 的函數(shù)式接口,這里 T 的類(lèi)型即是 Stream 元素的類(lèi)型,也就是 Person,而 R 的類(lèi)型未知。由于在重載解析之后 lambda 表達(dá)式的目標(biāo)類(lèi)型仍然未知,我們就需要推導(dǎo) R 的類(lèi)型:通過(guò)對(duì) lambda 表達(dá)式函數(shù)體進(jìn)行類(lèi)型檢查,我們發(fā)現(xiàn)函數(shù)體返回 String,因此 R 的類(lèi)型是 String,因而 map() 返回 Stream<String>。絕大多數(shù)情況下編譯器都能解析出正確的類(lèi)型,但如果碰到無(wú)法解析的情況,我們則需要:

  • 使用顯式 lambda 表達(dá)式(為參數(shù) p 提供顯式類(lèi)型)以提供額外的類(lèi)型信息;
  • 把 lambda 表達(dá)式轉(zhuǎn)型為 Function<Person, String>;
  • 為泛型參數(shù) R 提供一個(gè)實(shí)際類(lèi)型。(Stream<String> names = ps.stream().<String>map(p -> p.getName()))

lambda 表達(dá)式本身也可以為它自己的函數(shù)體提供目標(biāo)類(lèi)型,也就是說(shuō) lambda 表達(dá)式可以通過(guò)外部目標(biāo)類(lèi)型推導(dǎo)出其內(nèi)部的返回類(lèi)型,這意味著我們可以方便的編寫(xiě)一個(gè)返回函數(shù)的函數(shù):

Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };

類(lèi)似的,條件表達(dá)式可以把目標(biāo)類(lèi)型“分發(fā)”給其子表達(dá)式:

Callable<Integer> c = flag ? (() -> 23) : (() -> 42);

轉(zhuǎn)型表達(dá)式 (Cast expression) 可以顯式提供 lambda 表達(dá)式的類(lèi)型,這個(gè)特性在無(wú)法確認(rèn)目標(biāo)類(lèi)型時(shí)非常有用:

// 非法代碼
// Object o = () -> { System.out.println("hi"); }; 
// 有效代碼
Object o = (Runnable) () -> { System.out.println("hi"); };

六. 方法引用

lambda 表達(dá)式允許我們定義一個(gè)匿名方法,并允許我們以函數(shù)式接口的方式使用它。我們也希望能夠在已有的方法上實(shí)現(xiàn)同樣的特性。方法引用和 lambda 表達(dá)式擁有相同的特性,例如,它們都需要一個(gè)目標(biāo)類(lèi)型,并需要被轉(zhuǎn)化為函數(shù)式接口的實(shí)例。不過(guò)我們并不需要為方法引用提供方法體,我們可以直接通過(guò)方法名稱(chēng)引用已有方法。

以下面的代碼為例,假設(shè)我們要按照 name 或 age 為 Person 數(shù)組進(jìn)行排序:

class Person {
    private final String name;
    private final int age;
    
    public int getAge() { return age; }
    public String getName() {return name; }
    ...
}

Person[] people = ...
Comparator<Person> byName = Comparator.comparing(p -> p.getName());
Arrays.sort(people, byName);

這段代碼可以用方法引用代替 lambda 表達(dá)式:

Comparator<Person> byName = Comparator.comparing(Person::getName);

這里的 Person::getName 可以被看作為 lambda 表達(dá)式的簡(jiǎn)寫(xiě)形式。盡管方法引用不一定會(huì)把語(yǔ)法變的更緊湊,但它擁有更明確的語(yǔ)義:如果我們想要調(diào)用的方法擁有一個(gè)名字,我們就可以通過(guò)它的名字直接調(diào)用它。
因?yàn)楹瘮?shù)式接口的方法參數(shù)對(duì)應(yīng)于隱式方法調(diào)用時(shí)的參數(shù),所以被引用方法簽名可以通過(guò)放寬類(lèi)型,裝箱以及組織到參數(shù)數(shù)組中的方式對(duì)其參數(shù)進(jìn)行操作,就像在調(diào)用實(shí)際方法一樣:

// void exit(int status)
Consumer<Integer> b1 = System::exit;    
// void sort(Object[] a)
Consumer<String[]> b2 = Arrays:sort;    
// void main(String... args)
Consumer<String> b3 = MyProgram::main;  
// void main(String... args)
Runnable r = Myprogram::mapToInt        

七. 方法引用的種類(lèi)

方法引用有很多種,它們的語(yǔ)法如下:

  • 靜態(tài)方法引用:ClassName::methodName
  • 實(shí)例上的實(shí)例方法引用:instanceReference::methodName
  • 超類(lèi)上的實(shí)例方法引用:super::methodName
  • 類(lèi)型上的實(shí)例方法引用:ClassName::methodName
  • 構(gòu)造方法引用:Class::new
  • 數(shù)組構(gòu)造方法引用:TypeName[]::new

對(duì)于靜態(tài)方法引用,我們需要在類(lèi)名和方法名之間加入 "::" 分隔符,例如 Integer::sum。
對(duì)于具體對(duì)象上的實(shí)例方法引用,我們則需要在對(duì)象名和方法名之間加入分隔符:

Set<String> knownNames = ...
Predicate<String> isKnown = knownNames::contains;

這里的隱式 lambda 表達(dá)式會(huì)從 knownNames 中捕獲 String 對(duì)象,而它的方法體則會(huì)通過(guò) Set.contains 使用該 String 對(duì)象。有了實(shí)例方法引用,在不同函數(shù)式接口之間進(jìn)行類(lèi)型轉(zhuǎn)換就變的很方便:

Callable<Path> c = ...
Privileged<Path> a = c::call;

引用任意對(duì)象的實(shí)例方法,都需要在實(shí)例方法名稱(chēng)和其所屬類(lèi)型名稱(chēng)間加上分隔符:

Function<String, String> upperfier = String::toUpperCase;

如果類(lèi)型的實(shí)例方法是泛型的,那么我們就需要在 "::" 分隔符前提供類(lèi)型參數(shù),或者利用目標(biāo)類(lèi)型推導(dǎo)出其類(lèi)型。

需要注意的是,靜態(tài)方法引用和類(lèi)型上的實(shí)例方法引用擁有一樣的語(yǔ)法。編譯器會(huì)根據(jù)實(shí)際情況做出決定。一般我們不需要指定方法引用中的參數(shù)類(lèi)型,因?yàn)榫幾g器往往可以推導(dǎo)出結(jié)果,但如果需要我們也可以顯式在 :: 分隔符之前提供參數(shù)類(lèi)型信息。
和靜態(tài)方法引用類(lèi)似,構(gòu)造方法也可以通過(guò) new 關(guān)鍵字被直接引用:

SocketImplFactory factory = MySocketImpl::new;

如果類(lèi)型擁有多個(gè)構(gòu)造方法,那么我們就會(huì)通過(guò)目標(biāo)類(lèi)型的方法參數(shù)來(lái)選擇最佳匹配,這里的選擇過(guò)程和調(diào)用構(gòu)造方法時(shí)的選擇過(guò)程是一樣的。
如果待實(shí)例化的類(lèi)型是泛型的,那么我們可以在類(lèi)型名稱(chēng)之后提供類(lèi)型參數(shù),否則編譯器則會(huì)依照"菱形"構(gòu)造方法調(diào)用時(shí)的方式進(jìn)行推導(dǎo)。
數(shù)組的構(gòu)造方法引用的語(yǔ)法則比較特殊,為了便于理解,你可以假想存在一個(gè)接收int參數(shù)的數(shù)組構(gòu)造方法。參考下面的代碼:

IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 創(chuàng)建數(shù)組 int[10]
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容