<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    LetsCoding.cn

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

    Java 8:Lambda表達式(三)

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

    變量作用域

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

    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);

    現在我們來看看Lambda中的變量:count和text。它們不是在Lambda中定義的,而是repeatMessage方法的參數。

    如果你仔細瞧瞧,這里發生的事情并不是那么容易能看出來。Lambda表達式中的代碼,可能會在repeatMessage方法返回之后很久才會被調用,這時候,參數變量已經不存在了。那么text和count是如何保留下來的呢?

    為了理解這段代碼,我們需要進一步的了解Lambda表達式。Lambda表達式有三個組成部分:

    1. 代碼塊
    2. 參數
    3. 自由變量值;自由變量不是參數,也不是在語句體中定義的變量

    在我們的例子中,Lambda表達式有兩個自由變量:text和count。表示Lambda表達式的數據結構,必須保存自由變量的值,在這里,就是“Hello”和“1000”。我們說這些值被Lambda表達式捕獲了。(怎么做到的,那是實現的細節問題。例如,我們可以把Lambda表達式轉化成擁有單個方法的對象,這樣的話,自由變量的值就可以拷貝到對象的實例變量中去。)

    擁有自由變量值的代碼塊的專業術語叫閉包。如果有人很得意的告訴你,他們的語言擁有閉包,那么本文其余的部分會向你保證,Java同樣也有。在Java中,Lambda表達式就是閉包。事實上,內部類一直就是閉包啊!而Java 8也給了我們擁有簡潔語法的閉包。

    就像已經看到的,Lambda表達式可以捕獲外部作用域的變量值。在Java中,為了保證被捕獲的變量值是定義良好的,它有一個很重要的約束。在Lambda表達式里,只能引用值不變的變量。比如,下面的用法就不對:

    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. }

    這樣做,是有原因的。因為,在Lambda表達式中改變自由變量的值,不是線程安全的。比如,考慮一系列的并發任務,每一個都更新共享的計數器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++”不是一個原子操作,當多個線程并發執行它的時候,我們不可能知道,到底會發生什么樣的事情。

    內部類同樣也可以捕獲外部作用域的自由變量值。Java 8之前,內部類只能訪問被final修飾的本地變量。現在,這個規則被放寬到跟Lambda表達式一樣,內部類可以訪問事實上的final變量,也就是那些值不會改變的變量。

    不要指望編譯器去捕獲所有的并發訪問錯誤。禁止改變的規則是使用于本地變量。如果matches是外部類的實例變量,或者靜態變量,就算你得到的結果是不確定的,編譯器也不會告訴你任何錯誤。

    同樣的,盡管不正確,并發改變共享變量的值是相當合法的。下面的例子就是合法但不正確的:

    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是事實上的final變量。(事實上的final變量是指,在它初始化以后,再也沒有改變它的值。)在這里,matches總是引用同一個ArrayList對象,并沒有改變。但是,matches引用的對象以線程不安全的方式,被改變了,因為如果多個線程同時調用add方法,結果就是不可預測的!

    值的計數和搜集是存在線程安全的方式的。你可能想要用stream來收集特定屬性的值。在其他情形下,你可能會使用線程安全的計數器和集合。

    跟內部類相似,有一個變通的方式,可以讓Lambda表達式更新外部本地作用域的計數器的值。比如,用一個長度為一的數組:

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

    當然,這樣的代碼不是線程安全的。也許,對一個按鈕的回調方法來說,是無所謂的。但通常,使用這種方式之前,你應該多考慮考慮。

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

    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

    在方法里,你不能有兩個同名的本地變量。Lambda表達式同樣如此。在Lambda表達式里,當你使用“this”時,你引用的是創建Lambda表達式方法的this參數。例如:

    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調用的是Application對象的,不是Runnable實例的。在Lambda中使用this并沒有什么特別的。Lambda的作用域嵌套在doWork方法里,this的含義在方法中哪里都一樣。

    默認方法

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

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

    有一個更好的方法。類庫的設計者們可以提供一個forEach方法,它把函數應用到所包含的每一個元素上。然后我們就可以簡單的調用:

    1. list.forEach(System.out::println);

    這樣很好,如果類庫從一開始就是這樣設計的話。但是Java集合類庫是很多年前設計的,這就有一個問題。如果Collection接口多了一個新的方法,比如forEach,那么,所有實現了Collection的程序,都會編譯出錯,除非它們也實現多出來的那個方法。這在Java中肯定是不能接受的。

    Java的設計者們決定一勞永逸的解決這個問題:他們允許接口中的方法擁有具體實現(稱為默認方法)!這些方法可以安全的加進現存接口中。下面我們來看看默認方法的細節。在Java 8里,forEach方法被加進了Collection的父接口Iterable接口中,現在我來說說這樣做的機制。

    看如下的接口:

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

    接口中有兩個方法,抽象方法getId和默認方法getName。實現Person的具體類當然必須提供getId方法的實現,但可以選擇保留getName方法的實現,或者重載它。

    默認方法的出現,終結了一個經典的模式:提供一個接口和實現了它的部分或全部方法的抽象類,比如Collection/AbstractCollection,或WindowListener/WindowAdapter。現在你可以直接在接口中實現方法了。

    如果相同的方法在一個接口中被定義為默認方法,在超類或另一個接口中被定義為方法,會怎么樣呢?像Scala和C++都用復雜的規則來解決這種歧義性。幸好,在Java中,規則就簡單多了。它們是:

    超類優先。如果超類提供了具體的方法,接口中的默認方法將被簡單的忽略。
    接口沖突。如果父接口提供了默認方法,另一個接口有相同的方法(默認的或抽象的),那么你需要自己重載這個方法來解決沖突。

    讓我們看看第二條規則。比如擁有getName方法的另一個接口:

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

    如果你寫一個實現接口Person和Named的類,會發生什么呢?

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

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

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

    現在假設接口Named沒有提供getName方法的默認實現:

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

    那么Student類會繼承Person的默認方法嗎?這也許是合理的,但Java的設計者們決定堅持一致性原則:接口之間怎么沖突不重要,只要至少有一個接口提供了默認方法,編譯器就報錯,開發人員必須自己去解決沖突。

    如果接口都沒有提供相同方法的默認實現,那么這跟Java 8之前的時代是一樣的,沒有沖突。實現類有兩個選擇:實現這個方法,或者不實現它。后一種情形下,實現類本身就會是一個抽象類。

    我剛剛討論了接口之間的方法沖突。現在看看一個類繼承了一個父類,并且實現了一個接口。它從兩者繼承了同一個方法。例如,Person是一個類,Student被定義成:

    1. class Student extends Person implements Named { ... }

    這種情況下,只有父類的方法會生效,接口中任何的默認方法都會被簡單的忽略。在我們的例子中,Student會繼承Person中的getName方法,Named接口提不提供默認getName的實現沒有任何區別。這就是“父類優先”的規則,它保證了與Java 7的兼容性。在默認方法出現之前的正常工作的代碼里,如果你給接口添加一個默認方法,它并沒有任何效果。但是小心:你絕不能寫一個默認方法,它重新定義Object類里的任何方法。比如,你不能定義toString或equals方法的默認實現,就算這樣做對有些接口(比如List)來說很有誘惑力,因為”父類優先“原則會導致這樣的方法不可能勝過Object.toString或Object.equals。

    接口中的靜態方法

    在Java 8里,你可以在接口中添加靜態方法。從來都沒有一個技術上的原因說這樣是非法的:它只是簡單的看起來與接口作為抽象規范的精神相違背。

    到目前為止,把靜態方法放在伴生的類中是一個通常的做法。在標準類庫中,你會看到成對的接口和工具類,比如Collection/Collections,或者Path/Paths。

    看看Paths類,它只有幾個工廠方法。你可以從一系列的字符串中,創建一個路徑,比如Paths.get("jdk1.8.0", "jre", "bin")。在Java 8中,你可以把這個方法加到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接口就不需要了。

    當你在看Collections類的時候,你會看到兩類方法。一類這樣的方法:

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

    將會作為List接口的默認方法工作的很好:

    1. public default void shuffle()

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

    對工廠方法來說,那樣是不行的,因為你沒有調用方法的對象。這時候接口中的靜態方法就有用武之地了。例如:

    1. public static < T > List< T > nCopies(int n, T o)
    2. // Constructs a list of n instances of o

    可以作為List的靜態方法。那么,你就可以調用List.nCopies(10, "Fred"),而不是Collections.nCopies(10, "Fred")。這樣,閱讀代碼的人就很清楚,結果一定是個List。

    盡管如此,基本上,要Java集合類庫以上面這種方式去重構是不可能的。但是當你實現你自己的接口時,沒有理由去為工具方法提供單獨的伴生類了吧。

    在Java 8中,很多接口都被添加了靜態方法。比如,Comparator接口有一個非常有用的靜態方法comparing,它接收一個”鍵抽取“函數,并產生一個比較抽取出來的鍵的比較器。要根據name比較Person對象,用Comparator.comparing(Person::name)就行了。

    總結

    本文中,我先用Lambda表達式

    1. (first, second) -> Integer.compare(first.length(), second.length())

    來比較字符串的長度。但我們可以做得更好,簡單的使用Comparator.compare(String::length)就行。這是一個很好的結束本文的方式,因為它展示了用函數開發的力量。compare方法把一個函數(鍵抽取器)變成了另一個更復雜的函數(基于鍵的比較器)。在我的書中,以及各種網上資料里面,就有關于”高階函數“更多細節的討論。

    本文譯自:Lambda Expressions in Java 8

    原創文章,轉載請注明: 轉載自LetsCoding.cn
    本文鏈接地址: Java 8:Lambda表達式(三)

    posted on 2014-05-11 12:07 Rolandz 閱讀(3113) 評論(0)  編輯  收藏 所屬分類: 編程實踐

    導航

    統計

    留言簿(1)

    隨筆分類(12)

    隨筆檔案(19)

    積分與排名

    最新評論

    閱讀排行榜

    評論排行榜

    主站蜘蛛池模板: 亚洲精品偷拍视频免费观看| 日韩在线视频免费| 亚洲爱情岛论坛永久| 国产免费牲交视频| 性做久久久久久免费观看| 久久爰www免费人成| 插鸡网站在线播放免费观看| 亚洲丰满熟女一区二区哦| 亚洲av无码片在线观看| 久久亚洲成a人片| 亚洲国产精品无码AAA片| 爱情岛论坛网亚洲品质自拍| 午夜毛片不卡高清免费| 青青青国产在线观看免费网站| 一级毛片在线免费观看| 在线观看片免费人成视频播放| 一级片在线免费看| 尤物视频在线免费观看| 黄网站色视频免费看无下截| 亚洲欧美日韩中文字幕一区二区三区| 亚洲黄色在线播放| 日韩精品一区二区亚洲AV观看| 亚洲国产精品成人久久| 国产成人亚洲综合无码精品| 亚洲无线观看国产精品| 久久激情亚洲精品无码?V| 亚洲精品tv久久久久| 亚洲av无码成人精品区在线播放 | 亚洲黄色高清视频| 亚洲精选在线观看| 亚洲第一精品福利| 亚洲精品国产福利在线观看| 久久亚洲国产精品成人AV秋霞 | 成人网站免费看黄A站视频| a毛片视频免费观看影院| 成全在线观看免费观看大全| 国产成人AV免费观看| 久久午夜无码免费| 免费看片在线观看| 国产精品视频永久免费播放| 在线中文高清资源免费观看|