Java 8:Lambda表達(dá)式(三)
Java 8中,最重要的一個(gè)改變讓代碼更快、更簡潔,并向FP(函數(shù)式編程)打開了方便之門。下面我們來看看,它是如何做到的。
變量作用域
你經(jīng)常會想,如果可以在Lambda表達(dá)式里訪問外部方法或類中變量就好了。看下面的例子:
-
public static void repeatMessage(String text, int count) {
-
Runnable r = () -> {
-
for (int i = 0; i < count; i++) {
-
System.out.println(text);
-
Thread.yield();
-
}
-
};
-
new Thread(r).start();
-
}
-
-
// Prints Hello 1,000 times in a separate thread
-
repeatMessage("Hello", 1000);
現(xiàn)在我們來看看Lambda中的變量:count和text。它們不是在Lambda中定義的,而是repeatMessage方法的參數(shù)。
如果你仔細(xì)瞧瞧,這里發(fā)生的事情并不是那么容易能看出來。Lambda表達(dá)式中的代碼,可能會在repeatMessage方法返回之后很久才會被調(diào)用,這時(shí)候,參數(shù)變量已經(jīng)不存在了。那么text和count是如何保留下來的呢?
為了理解這段代碼,我們需要進(jìn)一步的了解Lambda表達(dá)式。Lambda表達(dá)式有三個(gè)組成部分:
- 代碼塊
- 參數(shù)
- 自由變量值;自由變量不是參數(shù),也不是在語句體中定義的變量
在我們的例子中,Lambda表達(dá)式有兩個(gè)自由變量:text和count。表示Lambda表達(dá)式的數(shù)據(jù)結(jié)構(gòu),必須保存自由變量的值,在這里,就是“Hello”和“1000”。我們說這些值被Lambda表達(dá)式捕獲了。(怎么做到的,那是實(shí)現(xiàn)的細(xì)節(jié)問題。例如,我們可以把Lambda表達(dá)式轉(zhuǎn)化成擁有單個(gè)方法的對象,這樣的話,自由變量的值就可以拷貝到對象的實(shí)例變量中去。)
擁有自由變量值的代碼塊的專業(yè)術(shù)語叫閉包。如果有人很得意的告訴你,他們的語言擁有閉包,那么本文其余的部分會向你保證,Java同樣也有。在Java中,Lambda表達(dá)式就是閉包。事實(shí)上,內(nèi)部類一直就是閉包啊!而Java 8也給了我們擁有簡潔語法的閉包。
就像已經(jīng)看到的,Lambda表達(dá)式可以捕獲外部作用域的變量值。在Java中,為了保證被捕獲的變量值是定義良好的,它有一個(gè)很重要的約束。在Lambda表達(dá)式里,只能引用值不變的變量。比如,下面的用法就不對:
-
public static void repeatMessage(String text, int count) {
-
Runnable r = () -> {
-
while (count > 0) {
-
count--; // Error: Can't mutate captured variable
-
System.out.println(text);
-
Thread.yield();
-
}
-
};
-
new Thread(r).start();
-
}
這樣做,是有原因的。因?yàn)椋贚ambda表達(dá)式中改變自由變量的值,不是線程安全的。比如,考慮一系列的并發(fā)任務(wù),每一個(gè)都更新共享的計(jì)數(shù)器matches:
-
int matches = 0;
-
for (Path p : files) {
-
// Illegal to mutate matches
-
new Thread(() -> { if (p has some property) matches++; }).start();
-
}
如果上面的代碼是合法的,那就非常非常糟糕了!“matches++”不是一個(gè)原子操作,當(dāng)多個(gè)線程并發(fā)執(zhí)行它的時(shí)候,我們不可能知道,到底會發(fā)生什么樣的事情。
內(nèi)部類同樣也可以捕獲外部作用域的自由變量值。Java 8之前,內(nèi)部類只能訪問被final修飾的本地變量。現(xiàn)在,這個(gè)規(guī)則被放寬到跟Lambda表達(dá)式一樣,內(nèi)部類可以訪問事實(shí)上的final變量,也就是那些值不會改變的變量。
不要指望編譯器去捕獲所有的并發(fā)訪問錯(cuò)誤。禁止改變的規(guī)則是使用于本地變量。如果matches是外部類的實(shí)例變量,或者靜態(tài)變量,就算你得到的結(jié)果是不確定的,編譯器也不會告訴你任何錯(cuò)誤。
同樣的,盡管不正確,并發(fā)改變共享變量的值是相當(dāng)合法的。下面的例子就是合法但不正確的:
-
List< Path > matches = new ArrayList<>();
-
for (Path p : files)
-
new Thread(() -> { if (p has some property) matches.add(p); }).start();
-
// Legal to mutate matches, but unsafe
注意,matches是事實(shí)上的final變量。(事實(shí)上的final變量是指,在它初始化以后,再也沒有改變它的值。)在這里,matches總是引用同一個(gè)ArrayList對象,并沒有改變。但是,matches引用的對象以線程不安全的方式,被改變了,因?yàn)槿绻鄠€(gè)線程同時(shí)調(diào)用add方法,結(jié)果就是不可預(yù)測的!
值的計(jì)數(shù)和搜集是存在線程安全的方式的。你可能想要用stream來收集特定屬性的值。在其他情形下,你可能會使用線程安全的計(jì)數(shù)器和集合。
跟內(nèi)部類相似,有一個(gè)變通的方式,可以讓Lambda表達(dá)式更新外部本地作用域的計(jì)數(shù)器的值。比如,用一個(gè)長度為一的數(shù)組:
-
int[] counter = new int[1];
-
button.setOnAction(event -> counter[0]++);
當(dāng)然,這樣的代碼不是線程安全的。也許,對一個(gè)按鈕的回調(diào)方法來說,是無所謂的。但通常,使用這種方式之前,你應(yīng)該多考慮考慮。
Lambda表達(dá)式的語句體和嵌套代碼塊的作用域是一樣的。變量名沖突和隱藏規(guī)則同樣適用。在Lambda表達(dá)式里聲明的參數(shù)或本地變量跟外部本地變量同名,是非法的。
-
Path first = Paths.get("/usr/bin");
-
Comparator< String > comp =
-
(first, second) -> Integer.compare(first.length(), second.length());
-
// Error: Variable first already defined
在方法里,你不能有兩個(gè)同名的本地變量。Lambda表達(dá)式同樣如此。在Lambda表達(dá)式里,當(dāng)你使用“this”時(shí),你引用的是創(chuàng)建Lambda表達(dá)式方法的this參數(shù)。例如:
-
public class Application() {
-
public void doWork() {
-
Runnable runner = () -> {
-
...;
-
System.out.println(this.toString());
-
...
-
};
-
...
-
}
-
}
這里的this.toString調(diào)用的是Application對象的,不是Runnable實(shí)例的。在Lambda中使用this并沒有什么特別的。Lambda的作用域嵌套在doWork方法里,this的含義在方法中哪里都一樣。
默認(rèn)方法
很多編程語言在它們的集合類庫中集成了函數(shù)表達(dá)式。這導(dǎo)致它們的代碼,比使用外循環(huán)更短,更易于理解。例如:
-
for (int i = 0; i < list.size(); i++)
-
System.out.println(list.get(i));
有一個(gè)更好的方法。類庫的設(shè)計(jì)者們可以提供一個(gè)forEach方法,它把函數(shù)應(yīng)用到所包含的每一個(gè)元素上。然后我們就可以簡單的調(diào)用:
-
list.forEach(System.out::println);
這樣很好,如果類庫從一開始就是這樣設(shè)計(jì)的話。但是Java集合類庫是很多年前設(shè)計(jì)的,這就有一個(gè)問題。如果Collection接口多了一個(gè)新的方法,比如forEach,那么,所有實(shí)現(xiàn)了Collection的程序,都會編譯出錯(cuò),除非它們也實(shí)現(xiàn)多出來的那個(gè)方法。這在Java中肯定是不能接受的。
Java的設(shè)計(jì)者們決定一勞永逸的解決這個(gè)問題:他們允許接口中的方法擁有具體實(shí)現(xiàn)(稱為默認(rèn)方法)!這些方法可以安全的加進(jìn)現(xiàn)存接口中。下面我們來看看默認(rèn)方法的細(xì)節(jié)。在Java 8里,forEach方法被加進(jìn)了Collection的父接口Iterable接口中,現(xiàn)在我來說說這樣做的機(jī)制。
看如下的接口:
-
interface Person {
-
long getId();
-
default String getName() { return "John Q. Public"; }
-
}
接口中有兩個(gè)方法,抽象方法getId和默認(rèn)方法getName。實(shí)現(xiàn)Person的具體類當(dāng)然必須提供getId方法的實(shí)現(xiàn),但可以選擇保留getName方法的實(shí)現(xiàn),或者重載它。
默認(rèn)方法的出現(xiàn),終結(jié)了一個(gè)經(jīng)典的模式:提供一個(gè)接口和實(shí)現(xiàn)了它的部分或全部方法的抽象類,比如Collection/AbstractCollection,或WindowListener/WindowAdapter。現(xiàn)在你可以直接在接口中實(shí)現(xiàn)方法了。
如果相同的方法在一個(gè)接口中被定義為默認(rèn)方法,在超類或另一個(gè)接口中被定義為方法,會怎么樣呢?像Scala和C++都用復(fù)雜的規(guī)則來解決這種歧義性。幸好,在Java中,規(guī)則就簡單多了。它們是:
超類優(yōu)先。如果超類提供了具體的方法,接口中的默認(rèn)方法將被簡單的忽略。
接口沖突。如果父接口提供了默認(rèn)方法,另一個(gè)接口有相同的方法(默認(rèn)的或抽象的),那么你需要自己重載這個(gè)方法來解決沖突。
讓我們看看第二條規(guī)則。比如擁有g(shù)etName方法的另一個(gè)接口:
-
interface Named {
-
default String getName() { return getClass().getName() + "_" + hashCode(); }
-
}
如果你寫一個(gè)實(shí)現(xiàn)接口Person和Named的類,會發(fā)生什么呢?
-
class Student implements Person, Named {
-
...
-
}
Student類繼承了兩個(gè)實(shí)現(xiàn)不一致的getName方法。Java編譯器會報(bào)錯(cuò),并把它留給開發(fā)者去解決沖突,而不是隨便選一個(gè)來使用。在Student類中,簡單的提供一個(gè)getName方法就可以了。至于方法里的實(shí)現(xiàn),你可以在沖突的方法中任選一個(gè)。
-
class Student implements Person, Named {
-
public String getName() { returnPerson.super.getName(); }
-
...
-
}
現(xiàn)在假設(shè)接口Named沒有提供getName方法的默認(rèn)實(shí)現(xiàn):
-
interface Named {
-
String getName();
-
}
那么Student類會繼承Person的默認(rèn)方法嗎?這也許是合理的,但Java的設(shè)計(jì)者們決定堅(jiān)持一致性原則:接口之間怎么沖突不重要,只要至少有一個(gè)接口提供了默認(rèn)方法,編譯器就報(bào)錯(cuò),開發(fā)人員必須自己去解決沖突。
如果接口都沒有提供相同方法的默認(rèn)實(shí)現(xiàn),那么這跟Java 8之前的時(shí)代是一樣的,沒有沖突。實(shí)現(xiàn)類有兩個(gè)選擇:實(shí)現(xiàn)這個(gè)方法,或者不實(shí)現(xiàn)它。后一種情形下,實(shí)現(xiàn)類本身就會是一個(gè)抽象類。
我剛剛討論了接口之間的方法沖突。現(xiàn)在看看一個(gè)類繼承了一個(gè)父類,并且實(shí)現(xiàn)了一個(gè)接口。它從兩者繼承了同一個(gè)方法。例如,Person是一個(gè)類,Student被定義成:
-
class Student extends Person implements Named { ... }
這種情況下,只有父類的方法會生效,接口中任何的默認(rèn)方法都會被簡單的忽略。在我們的例子中,Student會繼承Person中的getName方法,Named接口提不提供默認(rèn)getName的實(shí)現(xiàn)沒有任何區(qū)別。這就是“父類優(yōu)先”的規(guī)則,它保證了與Java 7的兼容性。在默認(rèn)方法出現(xiàn)之前的正常工作的代碼里,如果你給接口添加一個(gè)默認(rèn)方法,它并沒有任何效果。但是小心:你絕不能寫一個(gè)默認(rèn)方法,它重新定義Object類里的任何方法。比如,你不能定義toString或equals方法的默認(rèn)實(shí)現(xiàn),就算這樣做對有些接口(比如List)來說很有誘惑力,因?yàn)?#8221;父類優(yōu)先“原則會導(dǎo)致這樣的方法不可能勝過Object.toString或Object.equals。
接口中的靜態(tài)方法
在Java 8里,你可以在接口中添加靜態(tài)方法。從來都沒有一個(gè)技術(shù)上的原因說這樣是非法的:它只是簡單的看起來與接口作為抽象規(guī)范的精神相違背。
到目前為止,把靜態(tài)方法放在伴生的類中是一個(gè)通常的做法。在標(biāo)準(zhǔn)類庫中,你會看到成對的接口和工具類,比如Collection/Collections,或者Path/Paths。
看看Paths類,它只有幾個(gè)工廠方法。你可以從一系列的字符串中,創(chuàng)建一個(gè)路徑,比如Paths.get("jdk1.8.0", "jre", "bin")。在Java 8中,你可以把這個(gè)方法加到Path接口中:
-
public interface Path {
-
public static Path get(String first, String... more) {
-
return FileSystems.getDefault().getPath(first, more);
-
}
-
...
-
}
這樣,Paths接口就不需要了。
當(dāng)你在看Collections類的時(shí)候,你會看到兩類方法。一類這樣的方法:
-
public static void shuffle(List< ? > list)
將會作為List接口的默認(rèn)方法工作的很好:
-
public default void shuffle()
你就可以在任何列表上簡單的調(diào)用list.shuffle()。
對工廠方法來說,那樣是不行的,因?yàn)槟銢]有調(diào)用方法的對象。這時(shí)候接口中的靜態(tài)方法就有用武之地了。例如:
-
public static < T > List< T > nCopies(int n, T o)
-
// Constructs a list of n instances of o
可以作為List的靜態(tài)方法。那么,你就可以調(diào)用List.nCopies(10, "Fred"),而不是Collections.nCopies(10, "Fred")。這樣,閱讀代碼的人就很清楚,結(jié)果一定是個(gè)List。
盡管如此,基本上,要Java集合類庫以上面這種方式去重構(gòu)是不可能的。但是當(dāng)你實(shí)現(xiàn)你自己的接口時(shí),沒有理由去為工具方法提供單獨(dú)的伴生類了吧。
在Java 8中,很多接口都被添加了靜態(tài)方法。比如,Comparator接口有一個(gè)非常有用的靜態(tài)方法comparing,它接收一個(gè)”鍵抽取“函數(shù),并產(chǎn)生一個(gè)比較抽取出來的鍵的比較器。要根據(jù)name比較Person對象,用Comparator.comparing(Person::name)就行了。
總結(jié)
本文中,我先用Lambda表達(dá)式
-
(first, second) -> Integer.compare(first.length(), second.length())
來比較字符串的長度。但我們可以做得更好,簡單的使用Comparator.compare(String::length)就行。這是一個(gè)很好的結(jié)束本文的方式,因?yàn)樗故玖擞煤瘮?shù)開發(fā)的力量。compare方法把一個(gè)函數(shù)(鍵抽取器)變成了另一個(gè)更復(fù)雜的函數(shù)(基于鍵的比較器)。在我的書中,以及各種網(wǎng)上資料里面,就有關(guān)于”高階函數(shù)“更多細(xì)節(jié)的討論。
本文譯自:Lambda Expressions in Java 8
原創(chuàng)文章,轉(zhuǎn)載請注明: 轉(zhuǎn)載自LetsCoding.cn
本文鏈接地址: Java 8:Lambda表達(dá)式(三)
posted on 2014-05-11 12:07 Rolandz 閱讀(3124) 評論(0) 編輯 收藏 所屬分類: 編程實(shí)踐