閱讀114 返回首頁    go 技術社區[雲棲]


Java8:Lambdas(二)學習怎樣去使用lambda表達式

Java SE 8的發布很快就到了。伴隨著它來的不僅僅是新的語言lambda表達式(同樣被稱為閉包或匿名方法)——伴隨著一些語言特性支持——更重要的是API和library的增強將會使傳統的Java核心libraries變的更易於使用。其中大多數的增強和補充是在Collections API中,因為Collections API在整個應用中隨處可見,這篇文章大部分是在討論它。

然而 ,很有可能大多數的Java開發者將不會熟悉隱藏在lambdas背後的概念,和在設計中體現出的lambda形式與行為 。所以,在使用它們之前,最好先弄清楚它們為什麼這樣設計和怎麼工作。因此,我們將在之前和之後看一些方法,看它們在lambda之前和lambda之後是怎麼去處理一個問題的。

注意:這篇文章是根據b92(2013/3/30)構建的Java SE8,當你讀到這篇文章或Java SE8發布的時候,它的APIs、語法和語義可能已經改變了。當然,Oracle工程師所采取的APIs背後的概念,和方法,應該是非常接近現在展示的。

Algorithms,一個與集合交互更函數化的方法(自從它們初次發布以來就是Collections API的一部分),盡管它們很有用,但得到的關注很少。

自從JDK1.2時Collections API就伴隨著我們,但並不是其中所有的部分都得到開發者社區的關注。Alogrithms,一個與集合交互更函數化的方法(自從它們初次發布以來就是Collections API的一部分),盡管它們很有用,但得到的關注很少。例如,Collections類提供了十幾個方法,這些方法使用集合當參數,並且在collection或它的內容上執行一些操作。

考慮一下,例如,在Listing2中有十幾個Listing 1中的Person對象被放到List中。

Listing 1

public class Person {
  public Person(String fn, String ln, int a) {
    this.firstName = fn; this.lastName = ln; this.age = a;
  }

  public String getFirstName() { return firstName; }
  public String getLastName() { return lastName; }
        public int getAge() { return age; }
}


Listing 2

List<Person> people = Arrays.asList(
      new Person("Ted", "Neward", 42),
      new Person("Charlotte", "Neward", 39),
      new Person("Michael", "Neward", 19),
      new Person("Matthew", "Neward", 13),
      new Person("Neal", "Ford", 45),
      new Person("Candy", "Ford", 39),
      new Person("Jeff", "Brown", 43),
      new Person("Betsy", "Brown", 39)
    );
}

現在,假設我們想要使用last name然後用age來檢測或排序這個list,通常的做法是寫一個for循環(換句話說,是每次需要排序時實現一個排序)。當然,這個的問題違反了DRY(the Don’t Repeat Yourself principle不要重複自己的原則)原則,並且,更糟的是for循環不具有可複用性,所以我們必須每次用到時都重新實現一次。

Collections API有一個很好的方法:Collections類中有一個sort方法可以實現List的排序,如果要使用這個方法,Person類需要實現Comparable方法(這被稱做自然排序,為所有Person類型提供了一個默認排序),或者你可以傳一個Comparator實例來決定Person應該怎麼排序。

所以,如果你想先使用last name再使用age排序(如果last name相同時),代碼將像Listing 3展示的那樣。但是,這會有很多簡單的像先按last name 排序然後按age排序類似的工作要做。這裏新的閉包特性(closures feature)將幫助你,更簡單的去寫Comparator(參照Listing 4)。

Listing 3

 Collections.sort(people, new Comparator<Person>() {
      public int compare(Person lhs, Person rhs) {
        if (lhs.getLastName().equals(rhs.getLastName())) {
          return lhs.getAge() - rhs.getAge();
        }
        else
          return lhs.getLastName().compareTo(rhs.getLastName());
      }
    });

Listing 4

Collections.sort(people, (lhs, rhs) -> {
      if (lhs.getLastName().equals(rhs.getLastName()))
        return lhs.getAge() - rhs.getAge();
      else
        return lhs.getLastName().compareTo(rhs.getLastName());
    });

Comparator是語言中需要使用lambdas的一個初級的例子:這是使用一次性匿名方法的多情況中的一個例子。(記在心裏,這可能是使用lamdbas的好處中最簡單和最脆弱的。我們根本上已經把一種語法轉換另一種,當然這樣讓語法更簡潔。但是,即使你現在把這篇文章丟下,然後離開,大量的代碼依然會以這樣簡潔的方法保存。)
如果我們使用這個特殊的比較一段時間,我們會把lambda當做Comparator實例,因為這是這個方法的實質。既然lamdba適合這樣—”int compare(Person,Person)”,並且直接存放在Person類中,這樣使lambda的實現更簡單(參考Listing 5),和更具可讀性(參考Listing 6)。

Listing 5

public class Person {
  // . . .

  public static final Comparator<Person> BY_LAST_AND_AGE =
    (lhs, rhs) -> {
      if (lhs.lastName.equals(rhs.lastName))
        return lhs.age - rhs.age;
      else
        return lhs.lastName.compareTo(rhs.lastName);
    };
}

Listing 6

 Collections.sort(people, Person.BY_LAST_AND_AGE);

雖然,在Person類中存儲一個Comparator<Person>實例看起來很怪。更好的是用一個方法去做比較,而不是使用Comparator實例。幸運的是,Java將會允許任何滿足Comparator中方法簽名的方法實現類似的功能。所以,同樣可以寫BY_LAST_AND_AGE Comparator做一個標準實例,或者在Person中用靜態方法(如Listing 7),並用它來代替使用(如Listing 8)。

Listing 7

  public static int compareLastAndAge(Person lhs, Person rhs) {
    if (lhs.lastName.equals(rhs.lastName))
      return lhs.age - rhs.age;
    else
      return lhs.lastName.compareTo(rhs.lastName);
  }

Listing 8

Collections.sort(people, Person::compareLastAndAge);

因此,既使Collections API沒有什麼改變,lambdas已經很有幫助了。再一次,如果你現在放下文章離開,事情也已經很好了。但是他們會變的更好。

Collections API的改變
為Collection類加了一些API,各種各樣的新的和功能更強的方法和技術被使用,其中很多是從函數式編程借鑒來的。幸運的是你不需要有函數式編程的知識,你可以把函數當做有操作和重用價值的類和對象就可以。
Comparisons。以前Comparator方法的一個缺點是隱藏在Comparator實現之中。實際上代碼做了兩次比較,第一次是“主元素”的比較,就是lastname先比較。然後如果lastname相同的話再比較age。如果應用需要先按age排序,再次last names排序,就必須要寫一個新的Comparator——compareLastAndAge沒有可以複用的部分。
在這裏函數式方法能發揮它的作用。如果你把這個比較當作一個完全分開的Comparator實例,我們可以把它合並起來創建我們需要的比較方法(參考:Listing 9)。

Listing 9

public static final Comparator<Person> BY_FIRST =
    (lhs, rhs) -> lhs.firstName.compareTo(rhs.firstName);
  public static final Comparator<Person> BY_LAST =
    (lhs, rhs) -> lhs.lastName.compareTo(rhs.lastName);
  public static final Comparator<Person> BY_AGE =
    (lhs, rhs) -> lhs.age – rhs.age;

從經曆來看,手動實現每一個合並不是非常的高效,因為從時間上來看你手寫合並的時間跟實現多級比較是一樣。事實上,像這樣“我要通過X中的方法比較兩個X的值,並返回結果”是非常常用的,平台創造性的給了我們這樣的功能。通過Comparator類,一個比較方法通過一個函數從對象中提取一個比較關鍵字,然後返回一個基於此關鍵字的Comparator。這表明Listing 9可以重寫成像Listing10一樣簡單。

Listing 10

 public static final Comparator<Person> BY_FIRST =
    Comparators.comparing(Person::getFirstName);
  public static final Comparator<Person> BY_LAST =
    Comparators.comparing(Person::getLastName);
  public static final Comparator<Person> BY_AGE =
    Comparators.comparing(Person::getAge);

被簡化:這樣做會錯過Java新API的一個更強大的功能,這是做一個減法——把一個集合的值通過自定義操作合並成一個值。

思考一會我們是在做什麼:Person不再隻是排序,它現在隻需要提取出需要排序的關鍵字就可以。這是件好事——Person不需要考慮怎麼樣排序;Person隻需要關注怎麼樣做一個Person就可以。特別是我們要比較兩個或兩個以上參數的情況正在變好。
Composition。基於Java 8,Comparator接口擁有幾種方法,並通過以不同的方式串起來的方法,組合Comparator實例。例如,comparator.thenComparing()方法是Comparator比較完第一個參數後比較另一個參數後使用的。所以,重新創建“先比較last name,然後比較age”方法,現在可以使用兩個Comparator實例(LAST和AGE),像Listing11展示的那樣。或者,你更傾向使用方法而不是Comparator實例,參考Listing12。
Listing 11

 Collections.sort(people, Person.BY_LAST.
                                   .thenComparing(Person.BY_AGE));


Listing 12

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

順便說一句,對於那些不是使用Collections.sort()長的大人,現在在List中有了一個新的sort()方法。這是接口默認方法介紹中其中一種簡潔的事情:我們把這種基於非繼承(noninheritance-based)的可重用行為放在static方法中,現在可以被放到接口中。(參考 previous article in this series ,可以了解更多)
同樣的,如果代碼想把Person集合通過先通過last name,然後first name排序,不需要新寫Comparator,因為 比較可以通過兩個特殊的原子比較組合實現,像Listing13展示的那樣。
Listing 13

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

這種組合“連接”的方法,就是函數式組合(functional composition),這種在函數式編程中非常常見,並且是函數式編程功能非常強大的原因。
更重要的是需要明白,真正的好處並不僅僅是API允許我們去做比較,而是有能力去傳輸小塊可執行代碼,從而創造機會複用和設計。Comparator隻是其中一個小的應用。很多事情可以做的更靈活和強大,特別是結合和組合它們的時候。
Iteration。另一個lambdas和函數式方法改變編碼實現的例子,參考其中一個在集合中的基本操作:迭代元素。Java 8會通過把forEach默認方法放到Iterator和Iterable接口來改變集合。通過它來打印集合中的項目,例如,在Iterator中的forEach方法中實現lambda,像Listing 14實現的那樣。

Listing 14

people.forEach((it) -> System.out.println("Person: " + it));

官方的定義,lambda類型是被當做一個Consumer實例傳入的,在java.util.function包中定義的。然而,並不像傳統的Java接口,Consumer是一種新的函數式接口,這意味著將不會發生直接實現——反而,要以新的思維去接受它,因為它隻有一個實現,重要的方法——accept,這個方法是lambda提供的。剩下的(例如,compose和andThen)都是被定義為重要方法(important method)的功能方法,它們被設計為支持重要方法(important method)。
例如,andThen()是把兩個Consumer實例連接起來,所以第一個被稱為一,第二個被稱為立即跟進的單獨Consumer(immediately after into a single Consumer)。這提供了有用的組合技術,超出了這篇文章的範疇。

做一個收集者:它醜陋到必須要修改它。實際上如果我們使用內置的Collector接口,和它專門做mutable-reduction操作的夥伴Collectors,代碼將更容易寫。

很多用例都是在集合中尋找一個符合特殊條件的子項——例如,確定集合中的Person對象哪個到了可以飲酒的年齡,因為自動化代碼係統需要給集合中的每個人發一瓶啤酒。這種“在一群東西中選中一個”遠比操作一個集合有更廣泛的用途。設想下在一個文件中操作每一行,在一個結果集中操作每一行,每一個由隨機數生成器生成的值,等等。Java SE 8更進一步深化了這個概念,除了應用在集合中,並給它自己加入了自己的接口:Stream。
Stream。像JDK中的其它幾個接口,Stream接口是需要運用到多種場景的基本接口,包括Collections API。它代表了一個流對象,它就類似於Iterator通過一個集合讓我們一次訪問一個對象。
然而,並不同於集合的是,Stream不能保證集合對象是有限的。因此,這個可以用來從文件中讀字符串,或者其它請求式操作,特別是因為它被設計出來並不僅僅允許函數組合,同樣的允許“在底層的”並行。
考慮到前麵的需求:代碼需要過濾掉任何不滿21周歲的Person對象。當把一個Collection轉化為Stream時(通過Collection接口中定義的stream()方法),filter方法可以隻把過濾出來的對象生成一個新Stream(參考Listing 15)。

Listing 15

people
      .stream()
      .filter(it -> it.getAge() >= 21)

filter的參數是一個Predicate,一個被定義為使用一個通用參數,並返回Boolean值的接口。使用Predicate的意圖是決定參數對象需不需要放到返回對象集中。
filter()的返回對象是另一個Stream,這意味著過濾出的Stream同樣可以做更進一步的操作,比如,使用forEach()讀Stream中的每個元素,在下麵的例子中展示結果(參考Listing 16)。

Listing 16

 people.stream()
      .filter((it) -> it.getAge() >= 21)
      .forEach((it) -> 
        System.out.println("Have a beer, " + it.getFirstName()));

這個巧妙的展示了流的可組合性——我們可以使用流,並通過各種各樣的原子操作去使用它,每一個操作隻能做一件事。此外,filter()是延時操作——當需要它的時候它才會執行,而不是提前遍曆整個Person集合(像我們之前使用Collections API做的那樣)。

Predicates。第一次使用帶一個Predicate參數的filter()方法可能會感到奇怪。畢竟,如果目標是查找年齡大於21歲、last name是Neward的Person對象時,filter()應該使用一對Predicate實例。當然,這如同打開了一個可能性的潘多拉盒子。假如,目標是查找所有Person對象中滿足年齡大於21小於65,並且first name至少有4個字母的Person對象?無限的可能性被打開了,filter()API需要以某種方法去實現。
除非,當然一種機製可能以某種方式把所有的可能合並到一個單獨的Predicate中。幸運的是,很容易看出所有的Predicate實例組合可以自己為一個單獨的Predicate。換句話說,如果一個過濾器在對象使用過濾流之前,需要條件A是true和條件B是true,像這樣Predicate(A and B)。我們可以通過寫一個Predicate包括任意兩個Predicate實例,並且返回true當A和B同時為true時。
這樣 “and” Predicate完全通用並且可以事先寫好——事實上它隻知道兩個需要被調用的Predicate實例(並且這兩個沒有傳入參數)。
如果Predicate是寫在Predicate引用裏(像之前Person之前使用Comparator引用一樣),它們可以用and()方法捆綁在一起使用,像Listing 17展示的那樣。

Listing 17

 Predicate<Person> drinkingAge = (it) -> it.getAge() >= 21;
    Predicate<Person> brown = (it) -> it.getLastName().equals("Brown");
    people.stream()
      .filter(drinkingAge.and(brown))
      .forEach((it) ->
                System.out.println("Have a beer, " +
                                   it.getFirstName()));

正如所料,and()、or()和xor()所有都可用。可以查看Javadoc去了解所有的介紹。
map() and reduce()。其它常用Stream操作包括map(),通過使用一個函數把每個元素放到Stream中,然後從每個元素中輸出結果。所以,我們可以把集合中每個Person的age包括進來,然後執行一個簡單函數把每個Person的age檢索出來,像Listing 18展示的那樣。

Listing 18

  IntStream ages =
      people.stream()
            .mapToInt((it) -> it.getAge());

實際上,IntStream(它的同類LongStream和DoubleStream)對於這些基本類型是一個特殊的Stream<T>接口(表示它會創建該接口的定製版本)。
然後這樣就在Person集合中創建出一個Stream。這同樣在有些時候被稱作轉化操作,因為代碼把Person轉化或重構成int。
同樣的,reduce()需要輸入一係列的值,然後執行某種操作,最後把它們減為一個值。Reduction對開發人員來說是非常熟悉的操作,既然他們並沒有注意到:SQL中的COUNT()操作就是其中之一(把一個行的集合減為一個數),同樣還有SUM(),MAX(),和MIN()操作。對流中的每個值,都通過輸入一係列的值,然後執行一些操作(例如,增加一個計數器,將值添加到正在運行的總和中,查找最高,或者低的值),最後輸出一個單獨的值(一個integer)。
所以,例如,你可以在除以流中元素的個數之前先得到它們的和,然後得到平均年齡。給了新的API,這是最容易使用的內置方法,像Listing 19展示的那樣。

Listing 19

int sum = people.stream()
                .mapToInt(Person::getAge)
                .sum();

但是,這樣做會使你錯過探索Java新API的一個強大特點,這是做一個減法——通過一個特定的操作把一個聚合的值合並成一個單獨的值。所以,讓我們使用reduce()重寫求和的部分:

.reduce(0, (l, r) -> l + r);

這個減法,同樣是在功能圈(functional circles)中做為fold成名的,開始於一個種子值(在這裏是0),然後為種子申請閉包(closure)和流中的第一個值,得到結果並把它當做累積值保存,然後把它當做下一次操作的種子值。
換句慶說,在一係列的integers中像1,2,3,4,5這樣,先是0加上1,得到1做為累積值,然後1就在下一次操作中被當做種子值,執行(1+2)。依次類推,得到最後的值15 。像在Listing 20中展示的那樣。

Listing 20

List<Integer> values = Arrays.asList(1, 2, 3, 4, 5);
    int sum = values.stream().reduce(0, (l,r) -> l+r);
    System.out.println(sum);

注意閉包把reduce的二個參數當作IntBinaryOperator ,被定義為使用兩個integer得到一個int結果。IntBinaryOperator和IntBiFunction是專門功能接口的例子,其中還包括Double和Long類型的專門版本,它們都會需要兩個參數,最後返回一個int。這些特殊版本的建立是用於緩解使用常見基本類型的工作。
IntStream同樣有幾個輔助方法,包括average()、min()和max()方法,去做一些基本integer操作。此外,二進製的操作(像兩個數相加)同樣經常被定義這樣的基本包裝類(Integer::sum,Long::max等等)。
More maps and reduction。Maps和reduction在各種各樣的狀況下不僅僅是被當作一個簡單的數學方法使用。畢竟,它們能在任何情況下把一個對象集合轉化成另一種對象,然後生成一個單獨的值、map和reduction(then collected into a single value, map and reduction operations work)。
例如,map操作可以被用來取出或projection一個對象,並且提取其中的一部分。比如,從Person對象中提取出last name:

Stream lastNames = people.stream().map(Person::getLastName); 

一旦last name從Person流中取出,reduction能把strings連接到一起。例如,把last name轉化為XML表示的數據。參考Listing 21。

Listing 21

String xml =
      "<people data='lastname'>" +
      people.stream()
            .map(it -> "<person>" + it.getLastName() + "</person>")
            .reduce("", String::concat)
      + "</people>";
    System.out.println(xml);

自然,如果需要不同的XML格式,不同格式的內容用不同的操作,要麼使用特別提供的操作,像Listing 21,或者使用其它類定義的方法,例如,像Listing 22中展示的Person類。要麼像Listing 23展示的,使用map()的一部分操作把Person對象流轉化為JSON串。

Listing 22

public class Person {
  // . . .
  public static String toJSON(Person p) {
    return
      "{" +
        "firstName: \"" + p.firstName + "\", " +
        "lastName: \"" + p.lastName + "\", " +
        "age: " + p.age + " " +
      "}";
  }
}


Listing 23

String json =
      people.stream()
        .map(Person::toJSON)
        .reduce("[", (l, r) -> l + (l.equals("[") ? "" : ",") + r)
        + "]";
    System.out.println(json);

準備:Java SE 8的發布日期很快就到了,伴隨著它來的不僅僅是lambda語法表達式(同樣被稱為閉包和匿名方法)——伴隨著一些語法特性支持——更重要的是API和library的增強將會使傳統的Java核心libraries變的更易於使用。

在reduce操作中間的三目操作是避免在把Person轉化為JSON時在第一個Person前麵加上逗號。有一些JSON解析器能識別,但不規範,並且看起來很醜。

實事上它醜到必須要去修改它。代碼實際上可以用Collector接口內置方法和Collectors能把它變的更簡單,特別是做這種mutable-reduction操作(參考Listing 24)。這個比我們之前用的reduce和String::concat運行的更快,所以它是一個更好的選擇。

Listing 24

  String joined = people.stream()
                          .map(Person::toJSON)
                          .collect(Collectors.joining(", "));System.out.println("[" + joined + "]");

哦,不要忘了我們的老朋友Comparator,注意Stream同樣有排序stream的操作,所以排序Person的JSON串例子可以像Listing 25展示的那樣寫。

Listing 25

String json = people.stream()
                        .sorted(Person.BY_LAST)
                        .collect(Collectors.joining(", " "[", "]"));
    System.out.println(json);

這是個很強大的東西。

Parallelization。更強大的是這些操作在邏輯上是完全獨立的,需要將每個對象通過stream操作每一個。這意味著傳統的for循環將會被棄用,當試圖把集合分成幾段使用iterate、map、或者reduce操作一個大的集合時,每段可以用單獨的線程處理。

了解更多

Lambda表示式

 

然而,Stream API已經覆蓋了,與前麵使用過的XML或者JSON map()和reduce()操作有區別的操作——parallelStream(),而不是調用stream()從集合中獲得一個流。像Listing 26中展示的那樣。

Listing 26

  people.parallelStream()
      .filter((it) -> it.getAge() >= 21)
      .forEach((it) ->
                System.out.println("Have a beer " + it.getFirstName() +
                  Thread.currentThread()));

至少在我的筆記本電腦上一個包括十二個子項目的集合,兩個線程用於處理集合:Java中調用main()的主線程,和另一個不是我們創建的線程ForkJoinPool.commonPool worker-1。

很顯然,對於一個包括十二子項目的集合,這是沒有必要的。但是對於有幾百或更多個的情況,運行的“足夠好”跟“需要加快”就有區別了。如果沒有這些新的方法,你就需要關注這些重要代碼和學習算法。使用它們,你可以通過為之前的順序處理增加八個鍵(如果Shift鍵需要使用流就是9個)去寫並行代碼。(譯者:不明白這句話是什麼意思英文原文為:With them, you can write parallelized code literally by adding eight keystrokes (nine if you count the Shift key required to capitalize the s in stream) to the previously sequential processing.)

並且在必要的時候,一個並行的流可以退回到之前的順序流,通過調用sequential()。

重要的是,不管並行或順序誰更好用,它們都可以運用在同一個流接口上。我們更關注於業務需求,隻有當需要的時候才實現,這樣順序或並行的實現就成為了一個實現細節。我們並不想關注線程池中啟動和同步線程的低層實現細節。

總結

Lambdas能給Java帶來很多改變,包括怎麼樣寫和設計Java代碼。有一些改變已經在Java SE 包中,並且它們將會改變其它的包(包括Java平台的包和其它的開源包),開發者使用lambdas會越來越方便。

當Java SE 8發布的時候將會出現更多的改變。如果你能理解lambdas在集合中做工作,你將會在自己設計和編碼時使用lambdas更加順手。並且,在今後幾年你會創造更好的解耦代碼。

最後更新:2017-05-23 17:32:07

  上一篇:go  並發網2014.7月閱讀量Top10
  下一篇:go  Quartz教程二:API,Job和Trigger