lambda表達(dá)式

前段時(shí)間一直在看lambda表達(dá)式沾谓,但是總感覺吃不透,在深入了解lambda表達(dá)式的時(shí)候,需要很多基礎(chǔ)的知識(shí)棧盖灸。這里就偷一個(gè)懶,轉(zhuǎn)載一下個(gè)人認(rèn)為網(wǎng)上關(guān)于lambda表達(dá)式總結(jié)的最好的一篇文章磺芭。

注:原譯文見 http://lucida.me/blog/java-8-lambdas-insideout-language-features/

英語原版見:http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html

關(guān)于

本文介紹了 Java SE 8 中新引入的 lambda 語言特性以及這些特性背后的設(shè)計(jì)思想赁炎。這些特性包括:

  • lambda 表達(dá)式(又被成為“閉包”或“匿名方法”)
  • 方法引用和構(gòu)造方法引用
  • 擴(kuò)展的目標(biāo)類型和類型推導(dǎo)
  • 接口中的默認(rèn)方法和靜態(tài)方法

1. 背景

Java 是一門面向?qū)ο缶幊陶Z言。面向?qū)ο缶幊陶Z言和函數(shù)式編程語言中的基本元素(Basic Values)都可以動(dòng)態(tài)封裝程序行為:面向?qū)ο缶幊陶Z言使用帶有方法的對象封裝行為钾腺,函數(shù)式編程語言使用函數(shù)封裝行為徙垫。但這個(gè)相同點(diǎn)并不明顯,因?yàn)镴ava 對象往往比較“重量級(jí)”:實(shí)例化一個(gè)類型往往會(huì)涉及不同的類放棒,并需要初始化類里的字段和方法姻报。

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

public interface ActionListener {

  void actionPerformed(ActionEvent e);

}

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

button.addActionListener(new ActionListener() {

  public void actionPerformed(ActionEvent e) {

    ui.dazzle(e.getModifiers());

  }

});

很多庫都依賴于上面的模式邮府。對于并行 API 更是如此荧关,因?yàn)槲覀冃枰汛龍?zhí)行的代碼提供給并行 API,并行編程是一個(gè)非常值得研究的領(lǐng)域褂傀,因?yàn)樵谶@里摩爾定律得到了重生:盡管我們沒有更快的 CPU 核心(core)忍啤,但是我們有更多的 CPU 核心。而串行 API 就只能使用有限的計(jì)算能力仙辟。

隨著回調(diào)模式和函數(shù)式編程風(fēng)格的日益流行同波,我們需要在Java中提供一種盡可能輕量級(jí)的將代碼封裝為數(shù)據(jù)(Model code as data)的方法。匿名內(nèi)部類并不是一個(gè)好的 選擇叠国,因?yàn)椋?/p>

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

上面的多數(shù)問題均在Java SE 8中得以解決:

  • 通過提供更簡潔的語法和局部作用域規(guī)則未檩,Java SE 8 徹底解決了問題 1 和問題 2
  • 通過提供更加靈活而且便于優(yōu)化的表達(dá)式語義,Java SE 8 繞開了問題 3
  • 通過允許編譯器推斷變量的“常量性”(finality)粟焊,Java SE 8 減輕了問題 4 帶來的困擾

不過蜜自,Java SE 8 的目標(biāo)并非解決所有上述問題热凹。因此捕獲可變變量(問題 4)和非局部控制流(問題 5)并不在 Java SE 8的范疇之內(nèi)乡括。(盡管我們可能會(huì)在未來提供對這些特性的支持)

2. 函數(shù)式接口(Functional interfaces)

盡管匿名內(nèi)部類有著種種限制和問題蚀浆,但是它有一個(gè)良好的特性,它和Java類型系統(tǒng)結(jié)合的十分緊密:每一個(gè)函數(shù)對象都對應(yīng)一個(gè)接口類型香追。之所以說這個(gè)特性是良好的合瓢,是因?yàn)椋?/p>

  • 接口是 Java 類型系統(tǒng)的一部分
  • 接口天然就擁有其運(yùn)行時(shí)表示(Runtime representation)
  • 接口可以通過 Javadoc 注釋來表達(dá)一些非正式的協(xié)定(contract),例如透典,通過注釋說明該操作應(yīng)可交換(commutative)

上面提到的 ActionListener 接口只有一個(gè)方法晴楔,大多數(shù)回調(diào)接口都擁有這個(gè)特征:比如 Runnable 接口和 Comparator 接口。我們把這些只擁有一個(gè)方法的接口稱為 函數(shù)式接口峭咒。(之前它們被稱為 SAM類型滥崩,即 單抽象方法類型(Single Abstract Method))

我們并不需要額外的工作來聲明一個(gè)接口是函數(shù)式接口:編譯器會(huì)根據(jù)接口的結(jié)構(gòu)自行判斷(判斷過程并非簡單的對接口方法計(jì)數(shù):一個(gè)接口可能冗余的定義了一個(gè) Object已經(jīng)提供的方法,比如 toString()讹语,或者定義了靜態(tài)方法或默認(rèn)方法钙皮,這些都不屬于函數(shù)式接口方法的范疇)。不過API作者們可以通過 @FunctionalInterface 注解來顯式指定一個(gè)接口是函數(shù)式接口(以避免無意聲明了一個(gè)符合函數(shù)式標(biāo)準(zhǔn)的接口)顽决,加上這個(gè)注解之后短条,編譯器就會(huì)驗(yàn)證該接口是否滿足函數(shù)式接口的要求。

實(shí)現(xiàn)函數(shù)式類型的另一種方式是引入一個(gè)全新的 結(jié)構(gòu)化 函數(shù)類型才菠,我們也稱其為“箭頭”類型茸时。例如,一個(gè)接收 StringObject 并返回 int 的函數(shù)類型可以被表示為 (String, Object) -> int赋访。我們仔細(xì)考慮了這個(gè)方式可都,但出于下面的原因缓待,最終將其否定:

  • 它會(huì)為Java類型系統(tǒng)引入額外的復(fù)雜度,并帶來 結(jié)構(gòu)類型(Structural Type)指名類型(Nominal Type) 的混用渠牲。(Java 幾乎全部使用指名類型)
  • 它會(huì)導(dǎo)致類庫風(fēng)格的分歧——一些類庫會(huì)繼續(xù)使用回調(diào)接口旋炒,而另一些類庫會(huì)使用結(jié)構(gòu)化函數(shù)類型
  • 它的語法會(huì)變得十分笨拙,尤其在包含受檢異常(checked exception)之后
  • 每個(gè)函數(shù)類型很難擁有其運(yùn)行時(shí)表示签杈,這意味著開發(fā)者會(huì)受到 類型擦除(erasure) 的困擾和局限瘫镇。比如說,我們無法對方法 m(T->U)m(X->Y) 進(jìn)行重載(Overload)

所以我們選擇了“使用已知類型”這條路——因?yàn)楝F(xiàn)有的類庫大量使用了函數(shù)式接口答姥,通過沿用這種模式铣除,我們使得現(xiàn)有類庫能夠直接使用 lambda 表達(dá)式。例如下面是 Java SE 7 中已經(jīng)存在的函數(shù)式接口:

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

  • Predicate<T>——接收 T 并返回 boolean
  • Consumer<T>——接收 T敲长,不返回值
  • Function<T, R>——接收 T郎嫁,返回 R
  • Supplier<T>——提供 T 對象(例如工廠),不接收值
  • UnaryOperator<T>——接收 T 對象潘明,返回 T
  • BinaryOperator<T>——接收兩個(gè) T行剂,返回 T

除了上面的這些基本的函數(shù)式接口秕噪,我們還提供了一些針對原始類型(Primitive type)的特化(Specialization)函數(shù)式接口钳降,例如 IntSupplierLongBinaryOperator。(我們只為 int腌巾、longdouble 提供了特化函數(shù)式接口遂填,如果需要使用其它原始類型則需要進(jìn)行類型轉(zhuǎn)換)同樣的我們也提供了一些針對多個(gè)參數(shù)的函數(shù)式接口,例如 BiFunction<T, U, R>澈蝙,它接收 T 對象和 U 對象吓坚,返回 R 對象。

3. lambda表達(dá)式(lambda expressions)

匿名類型最大的問題就在于其冗余的語法灯荧。有人戲稱匿名類型導(dǎo)致了“高度問題”(height problem):比如前面 ActionListener 的例子里的五行代碼中僅有一行在做實(shí)際工作礁击。

lambda表達(dá)式是匿名方法,它提供了輕量級(jí)的語法逗载,從而解決了匿名內(nèi)部類帶來的“高度問題”哆窿。

下面是一些lambda表達(dá)式:

(int x, int y) -> x + y

() -> 42

(String s) -> { System.out.println(s); }

第一個(gè) lambda 表達(dá)式接收 xy 這兩個(gè)整形參數(shù)并返回它們的和;第二個(gè) lambda 表達(dá)式不接收參數(shù)厉斟,返回整數(shù) ‘42’挚躯;第三個(gè) lambda 表達(dá)式接收一個(gè)字符串并把它打印到控制臺(tái),不返回值擦秽。

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

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

表達(dá)式函數(shù)體適合小型 lambda 表達(dá)式誉己,它消除了 return 關(guān)鍵字眉尸,使得語法更加簡潔。

lambda 表達(dá)式也會(huì)經(jīng)常出現(xiàn)在嵌套環(huán)境中巨双,比如說作為方法的參數(shù)噪猾。為了使 lambda 表達(dá)式在這些場景下盡可能簡潔,我們?nèi)コ瞬槐匾姆指舴邸2贿^在某些情況下我們也可以把它分為多行袱蜡,然后用括號(hào)包起來,就像其它普通表達(dá)式一樣慢宗。

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


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

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

new Thread(() -> {

  connectToService();

  sendNotification();

}).start();

需要注意的是坪蚁,函數(shù)式接口的名稱并不是 lambda 表達(dá)式的一部分。那么問題來了镜沽,對于給定的 lambda 表達(dá)式敏晤,它的類型是什么?答案是:它的類型是由其上下文推導(dǎo)而來缅茉。例如嘴脾,下面代碼中的 lambda 表達(dá)式類型是 ActionListener

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

這就意味著同樣的 lambda 表達(dá)式在不同上下文里可以擁有不同的類型:

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

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

第一個(gè) lambda 表達(dá)式 () -> "done"Callable 的實(shí)例,而第二個(gè) lambda 表達(dá)式則是 PrivilegedAction 的實(shí)例蔬墩。

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

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

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

由于目標(biāo)類型(函數(shù)式接口)已經(jīng)“知道” lambda 表達(dá)式的形式參數(shù)(Formal parameter)類型黄锤,所以我們沒有必要把已知類型再重復(fù)一遍搪缨。也就是說,lambda 表達(dá)式的參數(shù)類型可以從目標(biāo)類型中得出:


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

在上面的例子里猜扮,編譯器可以推導(dǎo)出 s1s2 的類型是 String勉吻。此外,當(dāng) lambda 的參數(shù)只有一個(gè)而且它的類型可以被推導(dǎo)得知時(shí)旅赢,該參數(shù)列表外面的括號(hào)可以被省略:


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

button.addActionListener(e -> ui.dazzle(e.getModifiers()));

這些改進(jìn)進(jìn)一步展示了我們的設(shè)計(jì)目標(biāo):“不要把高度問題轉(zhuǎn)化成寬度問題齿桃』蠡蹋”我們希望語法元素能夠盡可能的少,以便代碼的讀者能夠直達(dá) lambda 表達(dá)式的核心部分短纵。

lambda 表達(dá)式并不是第一個(gè)擁有上下文相關(guān)類型的 Java 表達(dá)式:泛型方法調(diào)用和“菱形”構(gòu)造器調(diào)用也通過目標(biāo)類型來進(jìn)行類型推導(dǎo):


List<String> ls = Collections.emptyList();

List<Integer> li = Collections.emptyList();

Map<String, Integer> m1 = new HashMap<>();

Map<Integer, String> m2 = new HashMap<>();

5. 目標(biāo)類型的上下文(Contexts for target typing)

之前我們提到 lambda 表達(dá)式智能出現(xiàn)在擁有目標(biāo)類型的上下文中带污。下面給出了這些帶有目標(biāo)類型的上下文:

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

在前三個(gè)上下文(變量聲明、賦值和返回語句)里香到,目標(biāo)類型即是被賦值或被返回的類型:


Comparator<String> c;

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

public Runnable toDoLater() {

  return () -> {

    System.out.println("later");

  }

}

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

filterFiles(

  new FileFilter[] {

    f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q")

  });

方法參數(shù)的類型推導(dǎo)要相對復(fù)雜些:目標(biāo)類型的確認(rèn)會(huì)涉及到其它兩個(gè)語言特性:重載解析(Overload resolution)和參數(shù)類型推導(dǎo)(Type argument inference)悠就。

重載解析會(huì)為一個(gè)給定的方法調(diào)用(method invocation)尋找最合適的方法聲明(method declaration)千绪。由于不同的聲明具有不同的簽名,當(dāng) lambda 表達(dá)式作為方法參數(shù)時(shí)梗脾,重載解析就會(huì)影響到 lambda 表達(dá)式的目標(biāo)類型荸型。編譯器會(huì)通過它所得之的信息來做出決定。如果 lambda 表達(dá)式具有 顯式類型(參數(shù)類型被顯式指定)炸茧,編譯器就可以直接 使用lambda 表達(dá)式的返回類型瑞妇;如果lambda表達(dá)式具有 隱式類型(參數(shù)類型被推導(dǎo)而知),重載解析則會(huì)忽略 lambda 表達(dá)式函數(shù)體而只依賴 lambda 表達(dá)式參數(shù)的數(shù)量梭冠。

如果在解析方法聲明時(shí)存在二義性(ambiguous)辕狰,我們就需要利用轉(zhuǎn)型(cast)或顯式 lambda 表達(dá)式來提供更多的類型信息。如果 lambda 表達(dá)式的返回類型依賴于其參數(shù)的類型控漠,那么 lambda 表達(dá)式函數(shù)體有可能可以給編譯器提供額外的信息蔓倍,以便其推導(dǎo)參數(shù)類型。


List<Person> ps = ...

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

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

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

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

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

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


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

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


// Object o = () -> { System.out.println("hi"); }; 這段代碼是非法的

Object o = (Runnable) () -> { System.out.println("hi"); };

除此之外,當(dāng)重載的方法都擁有函數(shù)式接口時(shí)桐早,轉(zhuǎn)型可以幫助解決重載解析時(shí)出現(xiàn)的二義性癣缅。

目標(biāo)類型這個(gè)概念不僅僅適用于 lambda 表達(dá)式,泛型方法調(diào)用和“菱形”構(gòu)造方法調(diào)用也可以從目標(biāo)類型中受益哄酝,下面的代碼在 Java SE 7 是非法的友存,但在 Java SE 8 中是合法的:


List<String> ls = Collections.checkedList(new ArrayList<>(), String.class);

Set<Integer> si = flag ? Collections.singleton(23) : Collections.emptySet();

6. 詞法作用域(Lexical scoping)

在內(nèi)部類中使用變量名(以及 this)非常容易出錯(cuò)。內(nèi)部類中通過繼承得到的成員(包括來自 Object 的方法)可能會(huì)把外部類的成員掩蓋(shadow)陶衅,此外未限定(unqualified)的 this 引用會(huì)指向內(nèi)部類自己而非外部類爬立。

相對于內(nèi)部類,lambda 表達(dá)式的語義就十分簡單:它不會(huì)從超類(supertype)中繼承任何變量名万哪,也不會(huì)引入一個(gè)新的作用域侠驯。lambda 表達(dá)式基于詞法作用域,也就是說 lambda 表達(dá)式函數(shù)體里面的變量和它外部環(huán)境的變量具有相同的語義(也包括 lambda 表達(dá)式的形式參數(shù))奕巍。此外吟策,’this’ 關(guān)鍵字及其引用在 lambda 表達(dá)式內(nèi)部和外部也擁有相同的語義。

為了進(jìn)一步說明詞法作用域的優(yōu)點(diǎn)的止,請參考下面的代碼檩坚,它會(huì)把 "Hello, world!" 打印兩遍:

public class Hello {

  Runnable r1 = () -> { System.out.println(this); }

  Runnable r2 = () -> { System.out.println(toString()); }

  public String toString() {  return "Hello, world"; }

  public static void main(String... args) {

    new Hello().r1.run();

    new Hello().r2.run();

  }

}

與之相類似的內(nèi)部類實(shí)現(xiàn)則會(huì)打印出類似 Hello$1@5b89a773Hello$2@537a7706 之類的字符串,這往往會(huì)使開發(fā)者大吃一驚诅福。

基于詞法作用域的理念匾委,lambda 表達(dá)式不可以掩蓋任何其所在上下文中的局部變量,它的行為和那些擁有參數(shù)的控制流結(jié)構(gòu)(例如 for 循環(huán)和 catch 從句)一致氓润。

個(gè)人補(bǔ)充:這個(gè)說法很拗口赂乐,所以我在這里加一個(gè)例子以演示詞法作用域:


int i = 0;

int sum = 0;

for (int i = 1; i < 10; i += 1) { //這里會(huì)出現(xiàn)編譯錯(cuò)誤,因?yàn)閕已經(jīng)在for循環(huán)外部聲明過了

  sum += i;

}

7. 變量捕獲(Variable capture)

在 Java SE 7 中咖气,編譯器對內(nèi)部類中引用的外部變量(即捕獲的變量)要求非常嚴(yán)格:如果捕獲的變量沒有被聲明為 final 就會(huì)產(chǎn)生一個(gè)編譯錯(cuò)誤挨措。我們現(xiàn)在放寬了這個(gè)限制——對于 lambda 表達(dá)式和內(nèi)部類,我們允許在其中捕獲那些符合 有效只讀(Effectively final)的局部變量崩溪。

簡單的說浅役,如果一個(gè)局部變量在初始化后從未被修改過,那么它就符合有效只讀的要求伶唯,換句話說觉既,加上 final 后也不會(huì)導(dǎo)致編譯錯(cuò)誤的局部變量就是有效只讀變量。


Callable<String> helloCallable(String name) {

  String hello = "Hello";

  return () -> (hello + ", " + name);

}

this 的引用,以及通過 this 對未限定字段的引用和未限定方法的調(diào)用在本質(zhì)上都屬于使用 final 局部變量瞪讼。包含此類引用的 lambda 表達(dá)式相當(dāng)于捕獲了 this 實(shí)例岭参。在其它情況下,lambda 對象不會(huì)保留任何對 this 的引用尝艘。

這個(gè)特性對內(nèi)存管理是一件好事:內(nèi)部類實(shí)例會(huì)一直保留一個(gè)對其外部類實(shí)例的強(qiáng)引用演侯,而那些沒有捕獲外部類成員的 lambda 表達(dá)式則不會(huì)保留對外部類實(shí)例的引用。要知道內(nèi)部類的這個(gè)特性往往會(huì)造成內(nèi)存泄露背亥。

盡管我們放寬了對捕獲變量的語法限制秒际,但試圖修改捕獲變量的行為仍然會(huì)被禁止,比如下面這個(gè)例子就是非法的:


int sum = 0;

list.forEach(e -> { sum += e.size(); });

為什么要禁止這種行為呢狡汉?因?yàn)檫@樣的 lambda 表達(dá)式很容易引起 race condition娄徊。除非我們能夠強(qiáng)制(最好是在編譯時(shí))這樣的函數(shù)不能離開其當(dāng)前線程,但如果這么做了可能會(huì)導(dǎo)致更多的問題盾戴。簡而言之寄锐,lambda 表達(dá)式對 封閉,對 變量 開放尖啡。

個(gè)人補(bǔ)充:lambda 表達(dá)式對 封閉橄仆,對 變量 開放的原文是:lambda expressions close over values, not variables,我在這里增加一個(gè)例子以說明這個(gè)特性:

int sum = 0;

list.forEach(e -> { sum += e.size(); }); // Illegal, close over values

List<Integer> aList = new List<>();

list.forEach(e -> { aList.add(e); }); // Legal, open over variables

lambda 表達(dá)式不支持修改捕獲變量的另一個(gè)原因是我們可以使用更好的方式來實(shí)現(xiàn)同樣的效果:使用規(guī)約(reduction)衅斩。java.util.stream 包提供了各種通用的和專用的規(guī)約操作(例如 sum盆顾、minmax),就上面的例子而言畏梆,我們可以使用規(guī)約操作(在串行和并行下都是安全的)來代替 forEach


int sum =

    list.stream()

        .mapToInt(e -> e.size())

        .sum();

sum() 等價(jià)于下面的規(guī)約操作:

int sum =

    list.stream()

        .mapToInt(e -> e.size())

        .reduce(0 , (x, y) -> x + y);

規(guī)約需要一個(gè)初始值(以防輸入為空)和一個(gè)操作符(在這里是加號(hào))您宪,然后用下面的表達(dá)式計(jì)算結(jié)果:


0 + list[0] + list[1] + list[2] + ...

規(guī)約也可以完成其它操作,比如求最小值奠涌、最大值和乘積等等宪巨。如果操作符具有可結(jié)合性(associative),那么規(guī)約操作就可以容易的被并行化溜畅。所以捏卓,與其支持一個(gè)本質(zhì)上是并行而且容易導(dǎo)致 race condition 的操作,我們選擇在庫中提供一個(gè)更加并行友好且不容易出錯(cuò)的方式來進(jìn)行累積(accumulation)达皿。

8. 方法引用(Method references)

lambda 表達(dá)式允許我們定義一個(gè)匿名方法天吓,并允許我們以函數(shù)式接口的方式使用它贿肩。我們也希望能夠在 已有的 方法上實(shí)現(xiàn)同樣的特性峦椰。

方法引用和 lambda 表達(dá)式擁有相同的特性(例如,它們都需要一個(gè)目標(biāo)類型汰规,并需要被轉(zhuǎn)化為函數(shù)式接口的實(shí)例)汤功,不過我們并不需要為方法引用提供方法體,我們可以直接通過方法名稱引用已有方法溜哮。

以下面的代碼為例滔金,假設(shè)我們要按照 nameagePerson 數(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á)式的簡寫形式色解。盡管方法引用不一定(比如在這個(gè)例子里)會(huì)把語法變的更緊湊,但它擁有更明確的語義——如果我們想要調(diào)用的方法擁有一個(gè)名字餐茵,我們就可以通過它的名字直接調(diào)用它科阎。

因?yàn)楹瘮?shù)式接口的方法參數(shù)對應(yīng)于隱式方法調(diào)用時(shí)的參數(shù),所以被引用方法簽名可以通過放寬類型忿族,裝箱以及組織到參數(shù)數(shù)組中的方式對其參數(shù)進(jìn)行操作锣笨,就像在調(diào)用實(shí)際方法一樣:


Consumer<Integer> b1 = System::exit;    // void exit(int status)

Consumer<String[]> b2 = Arrays:sort;    // void sort(Object[] a)

Consumer<String> b3 = MyProgram::main;  // void main(String... args)

Runnable r = Myprogram::mapToInt        // void main(String... args)

9. 方法引用的種類(Kinds of method references)

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

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

對于靜態(tài)方法引用道批,我們需要在類名和方法名之間加入 :: 分隔符错英,例如 Integer::sum

對于具體對象上的實(shí)例方法引用,我們則需要在對象名和方法名之間加入分隔符:


Set<String> knownNames = ...

Predicate<String> isKnown = knownNames::contains;

這里的隱式 lambda 表達(dá)式(也就是實(shí)例方法引用)會(huì)從 knownNames 中捕獲 String 對象隆豹,而它的方法體則會(huì)通過Set.contains 使用該 String 對象椭岩。

有了實(shí)例方法引用,在不同函數(shù)式接口之間進(jìn)行類型轉(zhuǎn)換就變的很方便:


Callable<Path> c = ...

Privileged<Path> a = c::call;

引用任意對象的實(shí)例方法則需要在實(shí)例方法名稱和其所屬類型名稱間加上分隔符:


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

這里的隱式 lambda 表達(dá)式(即 String::toUpperCase 實(shí)例方法引用)有一個(gè) String 參數(shù)璃赡,這個(gè)參數(shù)會(huì)被 toUpperCase 方法使用判哥。

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

需要注意的是,靜態(tài)方法引用和類型上的實(shí)例方法引用擁有一樣的語法豆励。編譯器會(huì)根據(jù)實(shí)際情況做出決定夺荒。

一般我們不需要指定方法引用中的參數(shù)類型,因?yàn)榫幾g器往往可以推導(dǎo)出結(jié)果良蒸,但如果需要我們也可以顯式在 :: 分隔符之前提供參數(shù)類型信息技扼。

和靜態(tài)方法引用類似,構(gòu)造方法也可以通過 new 關(guān)鍵字被直接引用:


SocketImplFactory factory = MySocketImpl::new;

如果類型擁有多個(gè)構(gòu)造方法嫩痰,那么我們就會(huì)通過目標(biāo)類型的方法參數(shù)來選擇最佳匹配剿吻,這里的選擇過程和調(diào)用構(gòu)造方法時(shí)的選擇過程是一樣的。

如果待實(shí)例化的類型是泛型的串纺,那么我們可以在類型名稱之后提供類型參數(shù)丽旅,否則編譯器則會(huì)依照”菱形”構(gòu)造方法調(diào)用時(shí)的方式進(jìn)行推導(dǎo)。

數(shù)組的構(gòu)造方法引用的語法則比較特殊纺棺,為了便于理解榄笙,你可以假想存在一個(gè)接收 int參數(shù)的數(shù)組構(gòu)造方法。參考下面的代碼:


IntFunction<int[]> arrayMaker = int[]::new;

int[] array = arrayMaker.apply(10) // 創(chuàng)建數(shù)組 int[10]

10. 默認(rèn)方法和靜態(tài)接口方法(Default and static interface methods)

lambda 表達(dá)式和方法引用大大提升了 Java 的表達(dá)能力(expressiveness)祷蝌,不過為了使把 代碼即數(shù)據(jù) (code-as-data)變的更加容易茅撞,我們需要把這些特性融入到已有的庫之中,以便開發(fā)者使用。

Java SE 7 時(shí)代為一個(gè)已有的類庫增加功能是非常困難的米丘。具體的說剑令,接口在發(fā)布之后就已經(jīng)被定型,除非我們能夠一次性更新所有該接口的實(shí)現(xiàn)拄查,否則向接口添加方法就會(huì)破壞現(xiàn)有的接口實(shí)現(xiàn)吁津。默認(rèn)方法(之前被稱為 虛擬擴(kuò)展方法守護(hù)方法)的目標(biāo)即是解決這個(gè)問題,使得接口在發(fā)布之后仍能被逐步演化堕扶。

這里給出一個(gè)例子腺毫,我們需要在標(biāo)準(zhǔn)集合 API 中增加針對 lambda 的方法。例如 removeAll 方法應(yīng)該被泛化為接收一個(gè)函數(shù)式接口 Predicate挣柬,但這個(gè)新的方法應(yīng)該被放在哪里呢潮酒?我們無法直接在 Collection 接口上新增方法——不然就會(huì)破壞現(xiàn)有的 Collection 實(shí)現(xiàn)。我們倒是可以在 Collections 工具類中增加對應(yīng)的靜態(tài)方法邪蛔,但這樣就會(huì)把這個(gè)方法置于“二等公民”的境地急黎。

默認(rèn)方法 利用面向?qū)ο蟮姆绞较蚪涌谠黾有碌男袨椤K且环N新的方法:接口方法可以是 抽象的 或是 默認(rèn)的侧到。默認(rèn)方法擁有其默認(rèn)實(shí)現(xiàn)勃教,實(shí)現(xiàn)接口的類型通過繼承得到該默認(rèn)實(shí)現(xiàn)(如果類型沒有覆蓋該默認(rèn)實(shí)現(xiàn))。此外匠抗,默認(rèn)方法不是抽象方法故源,所以我們可以放心的向函數(shù)式接口里增加默認(rèn)方法,而不用擔(dān)心函數(shù)式接口的單抽象方法限制汞贸。

下面的例子展示了如何向 Iterator 接口增加默認(rèn)方法 skip


interface Iterator<E> {

  boolean hasNext();

  E next();

  void remove();

  default void skip(int i) {

    for ( ; i > 0 && hasNext(); i -= 1) next();

  }

}

根據(jù)上面的 Iterator 定義绳军,所有實(shí)現(xiàn) Iterator 的類型都會(huì)自動(dòng)繼承 skip 方法。在使用者的眼里矢腻,skip 不過是接口新增的一個(gè)虛擬方法门驾。在沒有覆蓋 skip 方法的 Iterator 子類實(shí)例上調(diào)用 skip 會(huì)執(zhí)行 skip 的默認(rèn)實(shí)現(xiàn):調(diào)用 hasNextnext 若干次。子類可以通過覆蓋 skip 來提供更好的實(shí)現(xiàn)——比如直接移動(dòng)游標(biāo)(cursor)多柑,或是提供為操作提供原子性(Atomicity)等奶是。

當(dāng)接口繼承其它接口時(shí),我們既可以為它所繼承而來的抽象方法提供一個(gè)默認(rèn)實(shí)現(xiàn)竣灌,也可以為它繼承而來的默認(rèn)方法提供一個(gè)新的實(shí)現(xiàn)聂沙,還可以把它繼承而來的默認(rèn)方法重新抽象化。

除了默認(rèn)方法初嘹,Java SE 8 還在允許在接口中定義 靜態(tài) 方法及汉。這使得我們可以從接口直接調(diào)用和它相關(guān)的輔助方法(Helper method),而不是從其它的類中調(diào)用(之前這樣的類往往以對應(yīng)接口的復(fù)數(shù)命名削樊,例如 Collections)豁生。比如兔毒,我們一般需要使用靜態(tài)輔助方法生成實(shí)現(xiàn) Comparator 的比較器漫贞,在Java SE 8中我們可以直接把該靜態(tài)方法定義在 Comparator 接口中:


public static <T, U extends Comparable<? super U>>

    Comparator<T> comparing(Function<T, U> keyExtractor) {

  return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));

}

11. 繼承默認(rèn)方法(Inheritance of default methods)

和其它方法一樣甸箱,默認(rèn)方法也可以被繼承,大多數(shù)情況下這種繼承行為和我們所期待的一致迅脐。不過芍殖,當(dāng)類型或者接口的超類擁有多個(gè)具有相同簽名的方法時(shí),我們就需要一套規(guī)則來解決這個(gè)沖突:

  • 類的方法(class method)聲明優(yōu)先于接口默認(rèn)方法谴蔑。無論該方法是具體的還是抽象的豌骏。
  • 被其它類型所覆蓋的方法會(huì)被忽略。這條規(guī)則適用于超類型共享一個(gè)公共祖先的情況隐锭。

為了演示第二條規(guī)則窃躲,我們假設(shè) CollectionList 接口均提供了 removeAll 的默認(rèn)實(shí)現(xiàn),然后 Queue 繼承并覆蓋了 Collection 中的默認(rèn)方法钦睡。在下面的 implement 從句中蒂窒,List 中的方法聲明會(huì)優(yōu)先于 Queue 中的方法聲明:


class LinkedList<E> implements List<E>, Queue<E> { ... }

當(dāng)兩個(gè)獨(dú)立的默認(rèn)方法相沖突或是默認(rèn)方法和抽象方法相沖突時(shí)會(huì)產(chǎn)生編譯錯(cuò)誤。這時(shí)程序員需要顯式覆蓋超類方法荞怒。一般來說我們會(huì)定義一個(gè)默認(rèn)方法洒琢,然后在其中顯式選擇超類方法:


interface Robot implements Artist, Gun {

  default void draw() { Artist.super.draw(); }

}

super 前面的類型必須是有定義或繼承默認(rèn)方法的類型。這種方法調(diào)用并不只限于消除命名沖突——我們也可以在其它場景中使用它褐桌。

最后衰抑,接口在 inheritsextends 從句中的聲明順序和它們被實(shí)現(xiàn)的順序無關(guān)。

12. 融會(huì)貫通(Putting it together)

我們在設(shè)計(jì)lambda時(shí)的一個(gè)重要目標(biāo)就是新增的語言特性和庫特性能夠無縫結(jié)合(designed to work together)荧嵌。接下來呛踊,我們通過一個(gè)實(shí)際例子(按照姓對名字列表進(jìn)行排序)來演示這一點(diǎn):

比如說下面的代碼:


List<Person> people = ...

Collections.sort(people, new Comparator<Person>() {

  public int compare(Person x, Person y) {

    return x.getLastName().compareTo(y.getLastName());

  }

})

冗余代碼實(shí)在太多了!

有了lambda表達(dá)式啦撮,我們可以去掉冗余的匿名類:


Collections.sort(

  people, (Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));

盡管代碼簡潔了很多恋技,但它的抽象程度依然很差:開發(fā)者仍然需要進(jìn)行實(shí)際的比較操作(而且如果比較的值是原始類型那么情況會(huì)更糟),所以我們要借助 Comparator 里的 comparing 方法實(shí)現(xiàn)比較操作:


Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));

在類型推導(dǎo)和靜態(tài)導(dǎo)入的幫助下逻族,我們可以進(jìn)一步簡化上面的代碼:


Collections.sort(people, comparing(p -> p.getLastName()));

我們注意到這里的 lambda 表達(dá)式實(shí)際上是 getLastName 的代理(forwarder)蜻底,于是我們可以用方法引用代替它:


Collections.sort(people, comparing(Person::getLastName));

最后,使用 Collections.sort 這樣的輔助方法并不是一個(gè)好主意:它不但使代碼變的冗余聘鳞,也無法為實(shí)現(xiàn) List 接口的數(shù)據(jù)結(jié)構(gòu)提供特定(specialized)的高效實(shí)現(xiàn)薄辅,而且由于 Collections.sort 方法不屬于 List 接口,用戶在閱讀 List 接口的文檔時(shí)不會(huì)察覺在另外的 Collections 類中還有一個(gè)針對 List 接口的排序(sort())方法抠璃。

默認(rèn)方法可以有效的解決這個(gè)問題站楚,我們?yōu)?List 增加默認(rèn)方法 sort(),然后就可以這樣調(diào)用:

people.sort(comparing(Person::getLastName));;

此外搏嗡,如果我們?yōu)?Comparator 接口增加一個(gè)默認(rèn)方法 reversed()(產(chǎn)生一個(gè)逆序比較器)窿春,我們就可以非常容易的在前面代碼的基礎(chǔ)上實(shí)現(xiàn)降序排序拉一。


people.sort(comparing(Person::getLastName).reversed());;

13. 小結(jié)(Summary)

Java SE 8 提供的新語言特性并不算多——lambda 表達(dá)式,方法引用旧乞,默認(rèn)方法和靜態(tài)接口方法蔚润,以及范圍更廣的類型推導(dǎo)。但是把它們結(jié)合在一起之后尺栖,開發(fā)者可以編寫出更加清晰簡潔的代碼嫡纠,類庫編寫者可以編寫更加強(qiáng)大易用的并行類庫。

今天寫了幾個(gè)小例子延赌,主要還是前端不是很擅長除盏,一些格式轉(zhuǎn)換就在后臺(tái)完成了。

    /**
     * 根據(jù)參數(shù)查詢list
     *
     * @param param
     * @return
     */
    @Override
    public PageBean<RzCheckSum> getCheckSumList(RzCheckSumQueryParam param) {
        param.reset();
        DBContextHolder.setDBType(Source.compare.toString());

        List<RzCheckSum>  list = rzCheckSumMapper.getListCheckSum(param);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        list.forEach(n -> n.setPaycheckDateString(sdf.format(new Date(Long.valueOf(n.getPaycheckDate() + "000")))));
        evaluate(list,(n) -> n == 1);

        DBContextHolder.clearDBType();
        return new PageBean(param.getPageNum(), param.getNumPerPage(), list.size(), list);
    }

    public static void evaluate(List<RzCheckSum> list, Predicate<Integer> predicate) {
        for(RzCheckSum n: list)  {
            if(predicate.test(Integer.valueOf(n.getDeleted()))) {
                n.setDeletedString("是");
            }else {
                n.setDeletedString("否");
            }
        }
    }

我在這個(gè)service的實(shí)現(xiàn)里 用到兩次lambda表達(dá)式挫以。一次是時(shí)間戳改成日期格式者蠕,第二次是把1轉(zhuǎn)換成是,0轉(zhuǎn)換成否掐松。感覺代碼看上去清爽多了踱侣。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市甩栈,隨后出現(xiàn)的幾起案子泻仙,更是在濱河造成了極大的恐慌,老刑警劉巖量没,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玉转,死亡現(xiàn)場離奇詭異,居然都是意外死亡殴蹄,警方通過查閱死者的電腦和手機(jī)究抓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來袭灯,“玉大人刺下,你說我怎么就攤上這事』” “怎么了橘茉?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長姨丈。 經(jīng)常有香客問我畅卓,道長,這世上最難降的妖魔是什么蟋恬? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任翁潘,我火速辦了婚禮,結(jié)果婚禮上歼争,老公的妹妹穿的比我還像新娘拜马。我一直安慰自己渗勘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布俩莽。 她就那樣靜靜地躺著旺坠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪豹绪。 梳的紋絲不亂的頭發(fā)上价淌,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天申眼,我揣著相機(jī)與錄音瞒津,去河邊找鬼。 笑死括尸,一個(gè)胖子當(dāng)著我的面吹牛巷蚪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播濒翻,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼屁柏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了有送?” 一聲冷哼從身側(cè)響起淌喻,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎雀摘,沒想到半個(gè)月后裸删,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阵赠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年涯塔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片清蚀。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡匕荸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出枷邪,到底是詐尸還是另有隱情榛搔,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布东揣,位于F島的核電站践惑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏救斑。R本人自食惡果不足惜童本,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望脸候。 院中可真熱鬧穷娱,春花似錦绑蔫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嫁盲,卻和暖如春篓叶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背羞秤。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國打工缸托, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瘾蛋。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓俐镐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哺哼。 傳聞我的和親對象是個(gè)殘疾皇子佩抹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容