John Jiang

          a cup of Java, cheers!
          https://github.com/johnshajiang/blog

             :: 首頁 ::  :: 聯系 :: 聚合  :: 管理 ::
            131 隨筆 :: 1 文章 :: 530 評論 :: 0 Trackbacks
          Java 8的語言變化
          --理解Lambda表達式和變化的接口類是如何使Java 8成為新的語言
          本文是IBM developerWorks中的一篇介紹Java 8關鍵新特性的文章,它主要關注Lambda表達式和改進的接口。(2014.04.19最后更新)

              Java 8包含了一組重要的新的語言特性,使你能夠更方便地構造程序。Lambda表達為內聯的代碼塊定義了一種新的語法,給予你與匿名內部類相同的靈活性,但又沒有那么多模板代碼。接口的改變使得能夠為已有接口加入新的特性,而不必打破現有代碼的兼容性。了解這些語言變化是怎樣一起工作的,請閱讀本系列另一篇文章"Java 8并發基礎",可以看到如何在Java 8流中使用Lambda。
              Java 8的最大改變就是增加了對Lambda表達式的支持。Lambda表達式一種通過引用進行傳遞的代碼塊。它類似于某些其它語言的閉包:代碼實現了一個功能,可以傳入一個或多個參數,還可以返回一個結果值。閉包被定義在一個上下文中,它可以訪問(在Lambda中是只讀訪問)上下文中的值。
              如果你不熟悉閉包,也不必擔心。Java 8的Lambda表達式是幾乎每個Java開發者都熟悉的匿名內部類的一個高效版規范。如果你只想在一個位置實現一個接口,或是創建一個基類的子類時,匿名內部類為此提供了一種內聯實現。Lambda表達式也用于相同的方式,但是它使用一種縮略的語法,使得這些實現比一個標準的內部類定義更為簡潔。
              在本文中,你將看到如何在不同的場景下使用Lambda表達式,并且你會學到與Java接口定義相關的擴展。在本文章的姊妹篇JVM并發系列的"Java 8并發基礎"一文中,可以看到更多使用Lambda表達式的例子,包括在Java 8流特性中的應用。

          進入Lambda
              Lambda表達式就是Java 8所稱的函數接口的實現:一個接口只定義一個抽象方法。只定義一個抽象方法的限制是非常重要的,因為Lambda表達式的語法并不會使用方法名。相反,該表達式會使用動態類型識別(匹配參數和返回類型,很多動態語言都這么做)去保證提供的Lambda能夠與期望的接口方法兼容。
              在清單1所示的簡單例子中,一個Lambda表達式被用來對Name實例進行排序。main()方法中的第一個代碼塊使用一個匿名內部類去實現Comparator<Name>接口,第二個語句塊則使用Lambda表達式。
          清單1. 比較Lambda表達式與匿名內部類
          public class Name {
              
          public final String firstName;
              
          public final String lastName;

              
          public Name(String first, String last) {
                  firstName 
          = first;
                  lastName 
          = last;
              }

              
          // only needed for chained comparator
              public String getFirstName() {
                  
          return firstName;
              }

              
          // only needed for chained comparator
              public String getLastName() {
                  
          return lastName;
              }

              
          // only needed for direct comparator (not for chained comparator)
              public int compareTo(Name other) {
                  
          int diff = lastName.compareTo(other.lastName);
                  
          if (diff == 0) {
                      diff 
          = firstName.compareTo(other.firstName);
                  }
                  
          return diff;
              }
              
          }

          public class NameSort {
              
              
          private static final Name[] NAMES = new Name[] {
                  
          new Name("Sally""Smith"),
                  
              };
              
              
          private static void printNames(String caption, Name[] names) {
                  
              }

              
          public static void main(String[] args) {

                  
          // sort array using anonymous inner class
                  Name[] copy = Arrays.copyOf(NAMES, NAMES.length);
                  Arrays.sort(copy, 
          new Comparator<Name>() {
                      @Override
                      
          public int compare(Name a, Name b) {
                          
          return a.compareTo(b);
                      }
                  });
                  printNames(
          "Names sorted with anonymous inner class:", copy);

                  
          // sort array using lambda expression
                  copy = Arrays.copyOf(NAMES, NAMES.length);
                  Arrays.sort(copy, (a, b) 
          -> a.compareTo(b));
                  printNames(
          "Names sorted with lambda expression:", copy);
                  
              }
          }
              在清單1中,Lambda被用于取代匿名內部類。這種匿名內部類在應用中非常普遍,所以Lambda表達式很快就贏得了Java8程序員們的青睞。(在本例中,同時使用匿名內部類和Lambda表達式去實現Name類中的一個方法,以方便對這兩種方法進行比較。如果在Lambda中對compareTo()方法進行內聯的話,該表達式將會更加簡潔。)

          標準的函數式接口
              為了應用Lambda,新的包java.util.function中定義了廣泛的函數式接口。它們被歸結為如下幾個類別:
              函數:使用一個參數,基于參數的值返回結果。
              謂語:使用一個參數,基于參數的值返回布爾結果。
              雙函數:使用兩個參數,基于參數的值返回結果。
              供應器:不使用任何參數,但會返回結果。
              消費者:使用一個參數,但不返回任何結果。
          多數類別都包含多個不同的變體,以便能夠作用于基本數據類型的參數和返回值。許多接口所定義的方法都可被用于組合對象,如清單2所示:
          清單2. 組合謂語
          // use predicate composition to remove matching names
          List<Name> list = new ArrayList<>();
          for (Name name : NAMES) {
              list.add(name);
          }
          Predicate
          <Name> pred1 = name -> "Sally".equals(name.firstName);
          Predicate
          <Name> pred2 = name -> "Queue".equals(name.lastName);
          list.removeIf(pred1.or(pred2));
          printNames(
          "Names filtered by predicate:", list.toArray(new Name[list.size()]));
              清單2定義了一對Predicate<Name>變量,一個用于匹配名為Sally的名字,另一個用于匹配姓為Queue的名字。調用方法pred1.or(pred2)會構造一個組合謂語,該謂語先后使用了兩個謂語,當它們中的任何一個返回true時,這個組合謂語就將返回true(這就相當于早期Java中的邏輯操作符||)。List.removeIf()方法就應用這個組合謂語去刪除列表中的匹配名字。
              Java 8定義了許多有用的java.util.function包中接口的組合接口,但這種組合并不都是一樣的。所有的謂語的變體(DoublePredicate,IntPredicate,LongPredicate和Predicate<T>)都定義了相同的組合與修改方法:and(),negate()和or()。但是Function<T>的基本數據類型變體就沒有定義任何組合與修改方法。如果你擁有使用函數式編程語言的經驗,那么你可能就發會發現這些不同之處和奇怪的忽略。

          改變接口
              在Java 8中,接口(如清單1的Comparator)的結構已發生了改變,部分原因是為了讓Lambda更好用。Java 8之前的接口只能定義常量,以及必須被實現的抽象方法。而Java 8中的接口則能夠定義靜態與默認方法。接口中的靜態方法與抽象類中的靜態方法是完全一樣的。默認方法則更像舊式的接口方法,但提供了該方法的一個實現。該方法實現可用于該接口的實現類,除非它被實現類覆蓋掉了。
              默認方法的一個重要特性就是它可以被加入到已有接口中,但又不會破壞已使用了這些接口的代碼的兼容性(除非已有代碼恰巧使用了相同名字的方法,并且其目的與默認方法不同)。這是一個非常強大的功能,Java 8的設計者們利用這一特性為許多已有Java類庫加入了對Lambda表達式的支持。清單3就展示了這樣的一個例子,它是清單1中對名字進行排序的第三種實現方式。
          清單3. 鍵-提取比較器鏈
          // sort array using key-extractor lambdas
          copy = Arrays.copyOf(NAMES, NAMES.length);
          Comparator
          <Name> comp = Comparator.comparing(name -> name.lastName);
          comp 
          = comp.thenComparing(name -> name.firstName);
          Arrays.sort(copy, comp);
          printNames(
          "Names sorted with key extractor comparator:", copy);
              清單3首先展示了如何使用新的Comparator.comparing()靜態方法去創建一個基于鍵-提取(Key-Extraction) Lambda的比較器(從技術上看,鍵-提取Lambda就是java.util.function.Function<T,R>接口的一個實例,它返回的比較器的類型適用于類型T,而提取的鍵的類型R則要實現Comparable接口)。它還展示了如何使用新的Comparator.thenComparing()默認方法去組合使用比較器,清單3就返回了一個新的比較器,它會先按姓排序,再按名排序。
              你也許期望能夠對比較器進行內聯,如:
          Comparator<Name> comp = Comparator.comparing(name -> name.lastName)
              .thenComparing(name 
          -> name.firstName);
          但不幸地是,Java 8的類型推導不允許這么做。為從靜態方法中得到期望類型的結果,你需要為編譯器提供更多的信息,可以使用如下任何一種形式:
          Comparator<Name> com1 = Comparator.comparing((Name name1) -> name1.lastName)
              .thenComparing(name2 
          -> name2.firstName);
          Comparator
          <Name> com2 = Comparator.<Name,String>comparing(name1 -> name1.lastName)
              .thenComparing(name2 
          -> name2.firstName);
              第一種方式在Lambda表達式中加入參數的類型:(Name name1) -> name1.lastName。有了這個輔助信息,編譯才能知道下面它該做些什么。第二種方式是告訴編譯器要傳遞給Function接口(在此處,該接口通過Lambda表達式實現)中comparing()方法的泛型變量T和R的類型。
              能夠方便地構建比較器以及比較器鏈是Java 8中很有用的特性,但它的代價是增加了復雜度。Java 7的Comparator接口定義了兩個方法(compare()方法,以及遍布于每個對象中的equals()方法)。而在Java 8中,該接口則定義了18個方法(除了原有的2個方法,還新加入了9個靜態方法和7個默認方法)。你將發現,為了能夠使用Lambda而造成的這種接口膨脹會重現于相當一部分Java標準類庫中。

          像Lambda那樣使用已有方法
              如果一個存在的方法已經實現了你的需求,你可以直接使用一個方法引用對它進行傳遞。清單4展示了這種方法。
          清單4. 對已有方法使用Lambda

          // sort array using existing methods as lambdas
          copy = Arrays.copyOf(NAMES, NAMES.length);
          comp 
          = Comparator.comparing(Name::getLastName).thenComparing(Name::getFirstName);
          Arrays.sort(copy, comp);
          printNames(
          "Names sorted with existing methods as lambdas:", copy);
              清單4做著與清單3相同的事情,但它使用了已有方法。使用Java 8的形為"類名:方法名"的方法引用語法,你可以使用任意方法,就像Lambda表達式那樣。其效果就與你定義一個Lambda表達式去調用該方法一樣。對類的靜態方法,特定對象或Lambda輸入類型的實例方法(如在清單4中,getFirstName()和getLastName()方法就是Name類的實例方法),以及類構造器,都可以使用方法引用。
              方法引用不僅方便,因為它們比使用Lambda表達式可能更高效,而且為編譯器提供了更好的類型信息(這也就是為什么在上一節的Lambda中使用.thenComparing()構造Comparator會出現問題,而在清單4卻能正常工作)。如果既可以使用對已有方法的方法引用,也可以使用Lambda表達式,請使用前者。

          捕獲與非捕獲Lambda
              你在本文中已見過的Lambda表達式都是非捕獲的,意即,它們都是把傳入的值當作接口方法參數使用的簡單Lambda表達式。Java 8的捕獲Lambda表達式則是使用外圍環境中的值。捕獲Lambda類似于某些JVM語言(如Scala)使用的閉包,但Java 8的實現與之有所不同,因為來自在外圍環境中的值必須聲明為final。也就是說,這些值要么確實為final(就如同以前的Java版本中由匿名內部類所引用的值),要么在外圍環境中不會被修改。這一規范適用于Lambda表達式和匿名內部類。有一些方法可以繞過對值的final限制。例如,在Lambda中僅使用特定變量的當前值,你可以添加一個新的方法,把這些值作為方法參數,再將捕獲的值(以恰當的接口引用這種形式)返回給Lambda。如果期望一個Lambda去修改外圍環境中的值,那么可以用一個可修改的持有器類(Holder)對這些值進行包裝。
              相比于捕獲Lambda,可以更高效地處理非捕獲Lambda,那是因為編譯能夠把它生成為類中的靜態方法,而運行時環境可以直接內聯的調用這些方法。捕獲Lambda也許低效一些,但在相同上下文環境中它至少可以表現的和匿名內部類一樣好。

          幕后的Lambda
              Lambda表達式看起來像匿名內部類,但它們的實現方法不同。Java的內部類有很多構造器;每個內部類都會有一個字節碼級別的獨立類文件。這就會產生大量的重復代碼(大部分是在常量池實體中),類加載時會造成大量的運行時開銷,哪怕只有少量的代碼也會有如此后果。
              Java 8沒有為Lambda生成獨立的類文件,而是使用了在Java 7中引入的invokedynamic字節碼指令。invokedynamic作用于一個啟動方法,當該方法第一次被調用時它會轉而去創建Lambda表達式的實現。然后,該實現會被返回并被直接調用。這樣就避免了獨立類文件帶來的空間開銷,以及加載類的大量運行時開銷。確切地說,Lambda功能的實現被丟給了啟動程序。目前Java 8生成的啟動程序會在運行時為Lambda創建一個新類,但在將來會使用不同的方法去實現。
              Java 8使用的優化使得通過invokedynamic指令實現的Lambda在實際中運行正常。多數其它的JVM語言,包括Scala (2.10.x),都會為閉包使用編譯器生成的內部類。在將來,這些語言可能會轉而使用invokedynamic指令,以便利用到Java 8(及其后繼版本)的優化。

          Lambda的局限
              如在本文開始時我所提到的,Lambda表達式總是某些特殊函數式接口的實現。你可以僅把Lambda當作接口引用去傳遞,而對于其它的接口實現,你也可以只是把Lambda當作這些特定接口去使用。清單5展示了這種局限性,在該示例使用了一對相同的(名稱除外)函數式接口。Java 8編譯接受String::lenght來作為這兩個接口的Lambda實現。但是,在一個Lambd表達式被定義為第一個接口的實例之后,它不能夠用于第二個接口的實例。
          清單5. Lambda的局限
          private interface A {
              
          public int valueA(String s);
          }
          private interface B {
              
          public int valueB(String s);
          }
          public static void main(String[] args) {
              A a 
          = String::length;
              B b 
          = String::length;

              
          // compiler error!
              
          // b = a;

              
          // ClassCastException at runtime!
              
          // b = (B)a;

              
          // works, using a method reference
              b = a::valueA;
              System.out.println(b.valueB(
          "abc"));
          }
              任何對Java接口概念有所了解的人都不會對清單5中的程序感到驚訝,因為那就是Java接口一直所做的事情(除了最后一點,那是Java 8新引入的方法引用)。但是使用其它函數式編程語言,例如Scala,的開發者們則會認為接口的這種限制是不自然的。
              函數式編程語言是用函數類型,而不是接口,去定義變量。在這些編程語言中會很普遍的使用高級函數:把函數作為參數傳遞給其它的函數,或者把函數當作值去返回。其結果就是你會得到比Lambda更為靈活的編程風格,這包括使用函數去組合其它函數以構建語句塊的能力。因為Java 8沒有定義函數類型,你不能使用這種方法去組合Lambda表達式。你可以組合接口(如清單3所示),但只能是與Java 8中已寫好的那些接口相關的特定接口。僅在新的java.util.function包內,就特殊設定了43個接口去使用Lambda。把它們加入到數以百計的已有接口中,你將看到這種方法在組合接口時總是會有嚴重的限制。
              使用接口而不是在向Java中引入函數類型是一個精妙的選擇。這樣就在防止對Java類庫進行重大改動的同時也能夠對已有類庫使用Lambda表達式。它的壞作用就是對Java 8造成了極大的限制,它只能稱為"接口編程"或是類函數式編程,而不是真正的函數式編程。但依靠JVM上其它語言,也包括函數式語言,的優點,這些限制并不可怕。

          結論
              Lambda是Java語言的最主要擴展,伴著它們的兄弟新特性--方法引用,隨著程序被移植到Java 8,Lambda將很快成為所有Java開發者不可或缺的工具。當與Java 8流結合起來時,Lambda就特別有用。查看文章"JVM并發: Java 8并發基礎",可以了解到將Lambda和流結合起來使用是如何簡化并發編程以及提高程序效率的。
          posted on 2014-04-19 23:48 John Jiang 閱讀(3174) 評論(0)  編輯  收藏 所屬分類: JavaSEJava翻譯
          主站蜘蛛池模板: 阿拉善盟| 五大连池市| 广州市| 巴青县| 巴南区| 宜宾县| 论坛| 华坪县| 深泽县| 台前县| 江津市| 金堂县| 思茅市| 达孜县| 资阳市| 湖南省| 牟定县| 清远市| 无锡市| 兴安盟| 苗栗市| 榆中县| 宜章县| 平原县| 阿克| 铜梁县| 焦作市| 乐都县| 日土县| 赤城县| 祁阳县| 堆龙德庆县| 麻城市| 阿拉善右旗| 密云县| 卓资县| 应城市| 静乐县| 安义县| 罗定市| 石门县|