LetsCoding.cn

          天地之間有桿秤,拿秤砣砸老百姓。

          Java 8:Lambda表達(dá)式(三)

          Java 8中,最重要的一個(gè)改變讓代碼更快、更簡潔,并向FP(函數(shù)式編程)打開了方便之門。下面我們來看看,它是如何做到的。

          變量作用域

          你經(jīng)常會想,如果可以在Lambda表達(dá)式里訪問外部方法或類中變量就好了。看下面的例子:

          1. public static void repeatMessage(String text, int count) {
          2.      Runnable r = () -> {
          3.         for (int i = 0; i < count; i++) {
          4.            System.out.println(text);
          5.            Thread.yield();
          6.         }
          7.      };
          8.      new Thread(r).start();
          9. }
          10.  
          11. // Prints Hello 1,000 times in a separate thread
          12. 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è)組成部分:

          1. 代碼塊
          2. 參數(shù)
          3. 自由變量值;自由變量不是參數(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á)式里,只能引用值不變的變量。比如,下面的用法就不對:

          1. public static void repeatMessage(String text, int count) {
          2.      Runnable r = () -> {
          3.         while (count > 0) {
          4.            count--; // Error: Can't mutate captured variable
          5.            System.out.println(text);
          6.            Thread.yield();
          7.         }
          8.      };
          9.      new Thread(r).start();
          10. }

          這樣做,是有原因的。因?yàn)椋贚ambda表達(dá)式中改變自由變量的值,不是線程安全的。比如,考慮一系列的并發(fā)任務(wù),每一個(gè)都更新共享的計(jì)數(shù)器matches:

          1. int matches = 0;
          2. for (Path p : files) {
          3.     // Illegal to mutate matches
          4.     new Thread(() -> { if (p has some property) matches++; }).start();
          5. }

          如果上面的代碼是合法的,那就非常非常糟糕了!“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)合法的。下面的例子就是合法但不正確的:

          1. List< Path > matches = new ArrayList<>();
          2. for (Path p : files)
          3.     new Thread(() -> { if (p has some property) matches.add(p); }).start();
          4.     // 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ù)組:

          1. int[] counter = new int[1];
          2. button.setOnAction(event -> counter[0]++);

          當(dāng)然,這樣的代碼不是線程安全的。也許,對一個(gè)按鈕的回調(diào)方法來說,是無所謂的。但通常,使用這種方式之前,你應(yīng)該多考慮考慮。

          Lambda表達(dá)式的語句體和嵌套代碼塊的作用域是一樣的。變量名沖突和隱藏規(guī)則同樣適用。在Lambda表達(dá)式里聲明的參數(shù)或本地變量跟外部本地變量同名,是非法的。

          1. Path first = Paths.get("/usr/bin");
          2. Comparator< String > comp =
          3.     (first, second) -> Integer.compare(first.length(), second.length());
          4.     // Error: Variable first already defined

          在方法里,你不能有兩個(gè)同名的本地變量。Lambda表達(dá)式同樣如此。在Lambda表達(dá)式里,當(dāng)你使用“this”時(shí),你引用的是創(chuàng)建Lambda表達(dá)式方法的this參數(shù)。例如:

          1. public class Application() {
          2.      public void doWork() {
          3.         Runnable runner = () -> {
          4.             ...;
          5.             System.out.println(this.toString());
          6.             ...
          7.         };
          8.         ...
          9.      }
          10. }

          這里的this.toString調(diào)用的是Application對象的,不是Runnable實(shí)例的。在Lambda中使用this并沒有什么特別的。Lambda的作用域嵌套在doWork方法里,this的含義在方法中哪里都一樣。

          默認(rèn)方法

          很多編程語言在它們的集合類庫中集成了函數(shù)表達(dá)式。這導(dǎo)致它們的代碼,比使用外循環(huán)更短,更易于理解。例如:

          1. for (int i = 0; i < list.size(); i++)
          2.     System.out.println(list.get(i));

          有一個(gè)更好的方法。類庫的設(shè)計(jì)者們可以提供一個(gè)forEach方法,它把函數(shù)應(yīng)用到所包含的每一個(gè)元素上。然后我們就可以簡單的調(diào)用:

          1. 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ī)制。

          看如下的接口:

          1. interface Person {
          2.     long getId();
          3.     default String getName() { return "John Q. Public"; }
          4. }

          接口中有兩個(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è)接口:

          1. interface Named {
          2.     default String getName() { return getClass().getName() + "_" + hashCode(); }
          3. }

          如果你寫一個(gè)實(shí)現(xiàn)接口Person和Named的類,會發(fā)生什么呢?

          1. class Student implements Person, Named {
          2.      ...
          3. }

          Student類繼承了兩個(gè)實(shí)現(xiàn)不一致的getName方法。Java編譯器會報(bào)錯(cuò),并把它留給開發(fā)者去解決沖突,而不是隨便選一個(gè)來使用。在Student類中,簡單的提供一個(gè)getName方法就可以了。至于方法里的實(shí)現(xiàn),你可以在沖突的方法中任選一個(gè)。

          1. class Student implements Person, Named {
          2.      public String getName() { returnPerson.super.getName(); }
          3.      ...
          4. }

          現(xiàn)在假設(shè)接口Named沒有提供getName方法的默認(rèn)實(shí)現(xiàn):

          1. interface Named {
          2.      String getName();
          3. }

          那么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被定義成:

          1. 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接口中:

          1. public interface Path {
          2.     public static Path get(String first, String... more) {
          3.         return FileSystems.getDefault().getPath(first, more);
          4.     }
          5.      ...
          6. }

          這樣,Paths接口就不需要了。

          當(dāng)你在看Collections類的時(shí)候,你會看到兩類方法。一類這樣的方法:

          1. public static void shuffle(List< ? > list)

          將會作為List接口的默認(rèn)方法工作的很好:

          1. public default void shuffle()

          你就可以在任何列表上簡單的調(diào)用list.shuffle()。

          對工廠方法來說,那樣是不行的,因?yàn)槟銢]有調(diào)用方法的對象。這時(shí)候接口中的靜態(tài)方法就有用武之地了。例如:

          1. public static < T > List< T > nCopies(int n, T o)
          2. // 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á)式

          1. (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í)踐

          導(dǎo)航

          統(tǒng)計(jì)

          留言簿(1)

          隨筆分類(12)

          隨筆檔案(19)

          積分與排名

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 饶平县| 竹北市| 弥渡县| 安顺市| 平南县| 宾川县| 伊金霍洛旗| 和顺县| 辽中县| 本溪市| 荣昌县| 汤原县| 财经| 绥宁县| 大理市| 华池县| 绥芬河市| 尚志市| 黄石市| 个旧市| 新巴尔虎右旗| 云安县| 伊川县| 昭觉县| 电白县| 青海省| 南充市| 宜黄县| 铁岭县| 青川县| 滦平县| 定日县| 桐柏县| 米林县| 平阴县| 石门县| 定襄县| 台江县| 南康市| 阜平县| 隆子县|