掌握 Java 8 Lambda 表示式

雲在千峰發表於2016-03-17

Lambda 表示式 是 Java8 中最重要的功能之一。使用 Lambda 表示式 可以替代只有一個函式的介面實現,告別匿名內部類,程式碼看起來更簡潔易懂。Lambda 表示式 同時還提升了對 集合 框架的迭代、遍歷、過濾資料的操作。

匿名內部類

在 Java 世界中,匿名內部類 可以實現在應用程式中可能只執行一次的操作。例如,在 Android 應用程式中,一個按鈕的點選事件處理。你不需要為了處理一個點選事件單獨編寫一個獨立的類,可以用匿名內部類完成該操作:

Button button = (Button) findViewById(R.id.button1);
button.setOnClickListener(new OnClickListener() {

    @Override
    public void onClick(View view) {
        Toast.makeText(MainActivity.this, "Button Clicked", Toast.LENGTH_SHORT).show();
    }

});

通過匿名內部類,雖然程式碼看起來不是很優雅,但是程式碼看起來比使用單獨的類要好理解,可以直接在程式碼呼叫的地方知道點選該按鈕會觸發什麼操作。

Functional Interfaces(函式型介面)

定義 OnClickListener 介面的程式碼如下:

    public interface OnClickListener {
        void onClick(View v);
    }

OnClickListener 是一個只有一個函式的介面。在 Java 8 中,這種只有一個函式的介面被稱之為 “Functional Interface”。

在 Java 中 Functional Interface 用匿名內部類實現是一種非常常見的形式。除了 OnClickListener 介面以外,像 Runnable 和 Comparator 等介面也符合這種形式。

Lambda 表示式語法

Lambda 表示式通過把匿名內部類五行程式碼簡化為一個語句。這樣使程式碼看起來更加簡潔。

一個 Lambda 表示式 由三個組成部分:

引數列表 箭頭符號 函式體

(int x, int y) -> x + y

函式體可以是單個表示式,也可以是程式碼塊。如果是單個表示式的話,函式體直接求值並返回了。如果是程式碼塊的話,就和普通的函式一樣執行,return 語句控制呼叫者返回。在最外層是不能使用 break 和 continue 關鍵字的,在迴圈中可以用來跳出迴圈。如果程式碼塊需要返回值的話,每個控制路徑都需要返回一個值或者丟擲異常。

下面是一些示例:

(int x, int y) -> x + y

() -> 42

(String s) -> { System.out.println(s); }

第一個表示式有兩個整數型引數 x 和 y,表示式返回 x + y 的值。第二個表示式沒有引數直接返回一個表示式的值 42,。 第三個有一個 string 引數,使用程式碼塊的方式把該引數列印出來,沒有返回值。

Lambda 示例

Runnable Lambda

來看幾個示例, 下面是一個 Runnable 的示例:

    public void runnableTest() {
        System.out.println("=== RunnableTest ===");
        // 一個匿名的 Runnable
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello world one!");
            }
        };

        // Lambda Runnable
        Runnable r2 = () -> System.out.println("Hello world two!");

        // 執行兩個 run 函式
        r1.run();
        r2.run();
    }

這兩個實現方式都沒有引數也沒有返回值。Runnable lambda 表示式使用程式碼塊的方式把五行程式碼簡化為一個語句。

Comparator Lambda

在 Java 中,Comparator 介面用來排序集合。在下面的示例中一個 ArrayList 中包含了一些 Person 物件, 並依據 Person 物件的 surName 來排序。下面是 Person 類中包含的 fields:

public class Person {
    private String givenName;
    private String surName;
    private int age;
    private Gender gender;
    private String eMail;
    private String phone;
    private String address;
}

下面是分別用匿名內部類和 Lambda 表示式實現 Comparator 介面的方式:

public class ComparatorTest {
    public static void main(String[] args) {
        List<Person> personList = Person.createShortList();

        // 使用內部類實現排序
        Collections.sort(personList, new Comparator<Person>() {
            public int compare(Person p1, Person p2) {
                return p1.getSurName().compareTo(p2.getSurName());
            }
        });

        System.out.println("=== Sorted Asc SurName ===");
        for (Person p : personList) {
            p.printName();
        }

        // 使用 Lambda 表示式實現

        // 升序排列
        System.out.println("=== Sorted Asc SurName ===");
        Collections.sort(personList, (Person p1, Person p2) -> p1.getSurName().compareTo(p2.getSurName()));
        for (Person p : personList) {
            p.printName();
        }

        // 降序排列
        System.out.println("=== Sorted Desc SurName ===");
        Collections.sort(personList, (p1, p2) -> p2.getSurName().compareTo(p1.getSurName()));
        for (Person p : personList) {
            p.printName();
        }
    }
}

可以看到 匿名內部類可以通過 Lambda 表示式實現。注意 第一個 Lambda 表示式定義了引數的型別為 Person;而第二個 Lambda 表示式省略了該型別定義。Lambda 表示式支援型別推倒,如果通過上下文可以推倒出所需要的型別,則可以省略型別定義。這裡由於 我們把 Lambda 表示式用在一個使用泛型定義的 Comparator 地方,編譯器可以推倒出這兩個引數型別為 Person 。

Listener 表示式

最後來看看 View 點選事件的表示式寫法:

view.setOnClickListener( v -> Toast.makeText(MainActivity.this, "Button Clicked", Toast.LENGTH_SHORT).show() );

注意, Lambda 表示式可以當做引數傳遞。型別推倒可以在如下場景使用:

  • 變數定義
  • 賦值操作
  • 返回語句
  • 陣列初始化
  • 函式或者建構函式引數
  • Lambda 表示式程式碼塊中
  • 條件表示式中 ? :
  • 強制轉換表示式

使用 Lambda 表示式提升程式碼

本節通過一個示例來看看 Lambda 表示式 如何提升你的程式碼。Lambda 表示式可以更好的支援不要重複自己(DRY)原則並且讓程式碼看起來更加簡潔易懂。

一個常見的查詢案例

編碼生涯中一個很常見的案例就是從一個集合中查詢出符合要求的資料。例如有很多人,每個人都帶有很多屬性,需要從這裡找出符合一些條件的人。

在本示例中,我們需要查詢符合三個條件的人群:

– 司機:年齡在 16 以上的人才能成為司機

– 需要服役的人:年齡在 18到25歲的男人

– 飛行員:年齡在 23 到 65 歲的人

找到這些人後,我們可以給這些人發郵件、打電話 告訴他們可以來考駕照、需要服役了等。

Person Class

Person 類代表每個人,該類具有如下屬性:

package com.example.lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

/**
 * @author MikeW
 */
public class Person {
  private String givenName;
  private String surName;
  private int age;
  private Gender gender;
  private String eMail;
  private String phone;
  private String address;

  public static class Builder{

    private String givenName="";
    private String surName="";
    private int age = 0;
    private Gender gender = Gender.FEMALE;
    private String eMail = "";
    private String phone = "";
    private String address = "";

    public Builder givenName(String givenName){
      this.givenName = givenName;
      return this;
    }

    public Builder surName(String surName){
      this.surName = surName;
      return this;
    }

    public Builder age (int val){
      age = val;
      return this;
    }

    public Builder gender(Gender val){
      gender = val;
      return this;
    }

    public Builder email(String val){
      eMail = val;
      return this;
    }

    public Builder phoneNumber(String val){
      phone = val;
      return this;
    }

    public Builder address(String val){
      address = val;
      return this;
    }

    public Person build(){
      return new Person(this);
    }
  }

  private Person(){
    super();
  }

  private Person(Builder builder){
    givenName = builder.givenName;
    surName = builder.surName;
    age = builder.age;
    gender = builder.gender;
    eMail = builder.eMail;
    phone = builder.phone;
    address = builder.address;

  }

  public String getGivenName(){
    return givenName;
  }

  public String getSurName(){
    return surName;
  }

  public int getAge(){
    return age;
  }

  public Gender getGender(){
    return gender;
  }

  public String getEmail(){
    return eMail;
  }

  public String getPhone(){
    return phone;
  }

  public String getAddress(){
    return address;
  }

  public void print(){
    System.out.println(
      "\nName: " + givenName + " " + surName + "\n" + 
      "Age: " + age + "\n" +
      "Gender: " + gender + "\n" + 
      "eMail: " + eMail + "\n" + 
      "Phone: " + phone + "\n" +
      "Address: " + address + "\n"
                );
  } 

  @Override
  public String toString(){
    return "Name: " + givenName + " " + surName + "\n" + "Age: " + age + "  Gender: " + gender + "\n" + "eMail: " + eMail + "\n";
  } 

  public static List<Person> createShortList(){
    List<Person> people = new ArrayList<>();

    people.add(
      new Builder()
            .givenName("Bob")
            .surName("Baker")
            .age(21)
            .gender(Gender.MALE)
            .email("bob.baker@example.com")
            .phoneNumber("201-121-4678")
            .address("44 4th St, Smallville, KS 12333")
            .build() 
      );

    people.add(
      new Builder()
            .givenName("Jane")
            .surName("Doe")
            .age(25)
            .gender(Gender.FEMALE)
            .email("jane.doe@example.com")
            .phoneNumber("202-123-4678")
            .address("33 3rd St, Smallville, KS 12333")
            .build() 
      );

    people.add(
      new Builder()
            .givenName("John")
            .surName("Doe")
            .age(25)
            .gender(Gender.MALE)
            .email("john.doe@example.com")
            .phoneNumber("202-123-4678")
            .address("33 3rd St, Smallville, KS 12333")
            .build()
    );

    people.add(
      new Builder()
            .givenName("James")
            .surName("Johnson")
            .age(45)
            .gender(Gender.MALE)
            .email("james.johnson@example.com")
            .phoneNumber("333-456-1233")
            .address("201 2nd St, New York, NY 12111")
            .build()
    );

    people.add(
      new Builder()
            .givenName("Joe")
            .surName("Bailey")
            .age(67)
            .gender(Gender.MALE)
            .email("joebob.bailey@example.com")
            .phoneNumber("112-111-1111")
            .address("111 1st St, Town, CA 11111")
            .build()
    );

    people.add(
      new Builder()
            .givenName("Phil")
            .surName("Smith")
            .age(55)
            .gender(Gender.MALE)
            .email("phil.smith@examp;e.com")
            .phoneNumber("222-33-1234")
            .address("22 2nd St, New Park, CO 222333")
            .build()
    );

    people.add(
      new Builder()
            .givenName("Betty")
            .surName("Jones")
            .age(85)
            .gender(Gender.FEMALE)
            .email("betty.jones@example.com")
            .phoneNumber("211-33-1234")
            .address("22 4th St, New Park, CO 222333")
            .build()
    );

    return people;
  }

}

Person 類使用一個 Builder 來建立新的物件。 通過 createShortList 函式來建立一些模擬資料。

常見的實現方式

有 Person 類和搜尋的條件了,現在可以撰寫一個 RoboContact 類來搜尋符合條件的人了:

public class RoboContactMethods {

  public void callDrivers(List<Person> pl){
    for(Person p:pl){
      if (p.getAge() >= 16){
        roboCall(p);
      }
    }
  }

  public void emailDraftees(List<Person> pl){
    for(Person p:pl){
      if (p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE){
        roboEmail(p);
      }
    }
  }

  public void mailPilots(List<Person> pl){
    for(Person p:pl){
      if (p.getAge() >= 23 && p.getAge() <= 65){
        roboMail(p);
      }
    }
  }

  public void roboCall(Person p){
    System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
  }

  public void roboEmail(Person p){
    System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
  }

  public void roboMail(Person p){
    System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
  }

}

這裡分別定義了 callDrivers、 emailDraftees 和 mailPilots 三個函式,每個函式的名字都表明了他們實現的功能。在每個函式中都包含了搜尋的條件,但是這個實現由一些問題:

  • 沒有遵守 DRY 原則
  • 每個函式都重複了一個迴圈操作
  • 每個函式都需要重新寫一次查詢條件
  • 每個搜尋場景都需要很多程式碼來實現
  • 程式碼沒有靈活性。如果搜尋條件改變了,需要修改程式碼的多個地方來符合新的需求。並且程式碼也不好維護。

重構這些函式

如何改進這些問題呢?如果把搜尋條件判斷提取出來,放到單獨的地方是個不錯的想法。

public class RoboContactMethods2 {

  public void callDrivers(List<Person> pl){
    for(Person p:pl){
      if (isDriver(p)){
        roboCall(p);
      }
    }
  }

  public void emailDraftees(List<Person> pl){
    for(Person p:pl){
      if (isDraftee(p)){
        roboEmail(p);
      }
    }
  }

  public void mailPilots(List<Person> pl){
    for(Person p:pl){
      if (isPilot(p)){
        roboMail(p);
      }
    }
  }

  public boolean isDriver(Person p){
    return p.getAge() >= 16;
  }

  public boolean isDraftee(Person p){
    return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
  }

  public boolean isPilot(Person p){
    return p.getAge() >= 23 && p.getAge() <= 65;
  }

  public void roboCall(Person p){
    System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
  }

  public void roboEmail(Person p){
    System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
  }

  public void roboMail(Person p){
    System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
  }

}

搜尋條件判斷封裝到一個函式中了,比第一步的實現有點改進。搜尋測試條件可以重用,但是這裡還是有一些重複的程式碼並且每個搜尋用例還是需要一個額外的函式。是否有更好的方法把搜尋條件傳遞給函式?

匿名類

在 lambda 表示式出現之前,匿名內部類是一種選擇。例如,我們可以定義個 MyTest 介面,裡面有個 test 函式,該函式有個引數 t 然後返回一個 boolean 值告訴該 t 是否符合條件。該介面定義如下:

public interface MyTest<T> {
  public boolean test(T t);
}

使用該介面的實現搜尋功能的改進程式碼如下:

public class RoboContactAnon {

  public void phoneContacts(List<Person> pl, MyTest<Person> aTest){
    for(Person p:pl){
      if (aTest.test(p)){
        roboCall(p);
      }
    }
  }

  public void emailContacts(List<Person> pl, MyTest<Person> aTest){
    for(Person p:pl){
      if (aTest.test(p)){
        roboEmail(p);
      }
    }
  }

  public void mailContacts(List<Person> pl, MyTest<Person> aTest){
    for(Person p:pl){
      if (aTest.test(p)){
        roboMail(p);
      }
    }
  }  

  public void roboCall(Person p){
    System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
  }

  public void roboEmail(Person p){
    System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
  }

  public void roboMail(Person p){
    System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
  }

}

這比之前的程式碼又改進了一步,現在只需要執行 3個函式就可以實現搜尋功能了。但是呼叫這些程式碼需要使用匿名內部類,這樣呼叫的程式碼看起來非常醜:

public class RoboCallTest03 {

  public static void main(String[] args) {

    List<Person> pl = Person.createShortList();
    RoboContactAnon robo = new RoboContactAnon();

    System.out.println("\n==== Test 03 ====");
    System.out.println("\n=== Calling all Drivers ===");
    robo.phoneContacts(pl, 
        new MyTest<Person>(){
          @Override
          public boolean test(Person p){
            return p.getAge() >=16;
          }
        }
    );

    System.out.println("\n=== Emailing all Draftees ===");
    robo.emailContacts(pl, 
        new MyTest<Person>(){
          @Override
          public boolean test(Person p){
            return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
          }
        }
    );

    System.out.println("\n=== Mail all Pilots ===");
    robo.mailContacts(pl, 
        new MyTest<Person>(){
          @Override
          public boolean test(Person p){
            return p.getAge() >= 23 && p.getAge() <= 65;
          }
        }
    );

  }
}

這就是大家深惡痛絕的匿名內部類巢狀問題,五行程式碼中只有一行是真正有用的程式碼,但是其他四行模板程式碼每次都要重新來一遍。

Lambda 表示式派上用場了

Lambda 表示式可以完美的解決該問題。前面我們已經看到了 Lambda 表示式如何解決 OnClickListener 問題的了。

在看看這裡 Lambda 表示式如何實現的之前,我們先來看看 Java 8 中的一個新包:/java

在上一個示例中,MyTest functional interface 作為函式的引數。但是如果每次都需要我們自己自定義一個這樣的介面是不是比較繁瑣呢? 所以 Java 8 提供了這個 java.util.function 包,裡面定義了幾十個常用的 functional interface。這裡 Predicate 這個介面符合我們的要求:

public interface Predicate<T> {
  public boolean test(T t);
}

test 函式需要一個泛型的引數然後返回一個布林值。過濾一個物件就需要這樣的操作。下面是如何用 Lambda 表示式實現搜尋的程式碼:

public class RoboContactLambda {
  public void phoneContacts(List<Person> pl, Predicate<Person> pred){
    for(Person p:pl){
      if (pred.test(p)){
        roboCall(p);
      }
    }
  }

  public void emailContacts(List<Person> pl, Predicate<Person> pred){
    for(Person p:pl){
      if (pred.test(p)){
        roboEmail(p);
      }
    }
  }

  public void mailContacts(List<Person> pl, Predicate<Person> pred){
    for(Person p:pl){
      if (pred.test(p)){
        roboMail(p);
      }
    }
  }  

  public void roboCall(Person p){
    System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
  }

  public void roboEmail(Person p){
    System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
  }

  public void roboMail(Person p){
    System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
  }

}

這樣使用 Lambda 表示式就解決了這個匿名內部類的問題,下面是使用 Lambda 表示式來呼叫這些搜尋函式的程式碼:

public class RoboCallTest04 {

  public static void main(String[] args){ 

    List<Person> pl = Person.createShortList();
    RoboContactLambda robo = new RoboContactLambda();

    // Predicates
    Predicate<Person> allDrivers = p -> p.getAge() >= 16;
    Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
    Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65;

    System.out.println("\n==== Test 04 ====");
    System.out.println("\n=== Calling all Drivers ===");
    robo.phoneContacts(pl, allDrivers);

    System.out.println("\n=== Emailing all Draftees ===");
    robo.emailContacts(pl, allDraftees);

    System.out.println("\n=== Mail all Pilots ===");
    robo.mailContacts(pl, allPilots);

    // Mix and match becomes easy
    System.out.println("\n=== Mail all Draftees ===");
    robo.mailContacts(pl, allDraftees);  

    System.out.println("\n=== Call all Pilots ===");
    robo.phoneContacts(pl, allPilots);    

  }
}

上面的示例程式碼可以在這裡下載:RoboCallExample.zip

java.util.function 包

該包包含了很多常用的介面,比如:

– Predicate: 判斷是否符合某個條件

– Consumer: 使用引數物件來執行一些操作

– Function: 把物件 T 變成 U

– Supplier:提供一個物件 T (和工廠方法類似)

– UnaryOperator: A unary operator from T -> T

– BinaryOperator: A binary operator from (T, T) -> T

可以詳細看看這個包裡面都有哪些介面,然後思考下如何用 Lambda 表示式來使用這些介面。

改進人名的輸出方式

比如在上面的示例中 ,把找到的人名字給列印出來,但是不同的地方列印的格式要求不一樣,比如有些地方要求把 姓 放到 名字的前面列印出來;而有些地方要求把 名字 放到 姓 的前面列印出來。 下面來看看如何實現這個功能:

常見的實現

兩種不同列印人名的實現方式:

  public void printWesternName(){

    System.out.println("\nName: " + this.getGivenName() + " " + this.getSurName() + "\n" +
             "Age: " + this.getAge() + "  " + "Gender: " + this.getGender() + "\n" +
             "EMail: " + this.getEmail() + "\n" + 
             "Phone: " + this.getPhone() + "\n" +
             "Address: " + this.getAddress());
  }

  public void printEasternName(){

    System.out.println("\nName: " + this.getSurName() + " " + this.getGivenName() + "\n" +
             "Age: " + this.getAge() + "  " + "Gender: " + this.getGender() + "\n" +
             "EMail: " + this.getEmail() + "\n" + 
             "Phone: " + this.getPhone() + "\n" +
             "Address: " + this.getAddress());
  }

Function 介面非常適合這類情況,該介面的 apply 函式是這樣定義的:

public R apply(T t){ }

引數為泛型型別 T 返回值為泛型型別 R。例如把 Person 類當做引數而 String 當做返回值。這樣可以用該函式實現一個更加靈活的列印人名的實現:

  public String printCustom(Function <Person, String> f){
      return f.apply(this);
  }

很簡單,一個 Function 物件作為引數,返回一個 字串。

下面是測試列印的程式:

public class NameTestNew {

  public static void main(String[] args) {

    System.out.println("\n==== NameTestNew ===");

    List<Person> list1 = Person.createShortList();

    // Print Custom First Name and e-mail
    System.out.println("===Custom List===");
    for (Person person:list1){
        System.out.println(
            person.printCustom(p -> "Name: " + p.getGivenName() + " EMail: " + p.getEmail())
        );
    }

    // Define Western and Eastern Lambdas

    Function<Person, String> westernStyle = p -> {
      return "\nName: " + p.getGivenName() + " " + p.getSurName() + "\n" +
             "Age: " + p.getAge() + "  " + "Gender: " + p.getGender() + "\n" +
             "EMail: " + p.getEmail() + "\n" + 
             "Phone: " + p.getPhone() + "\n" +
             "Address: " + p.getAddress();
    };

    Function<Person, String> easternStyle =  p -> "\nName: " + p.getSurName() + " " 
            + p.getGivenName() + "\n" + "Age: " + p.getAge() + "  " + 
            "Gender: " + p.getGender() + "\n" +
            "EMail: " + p.getEmail() + "\n" + 
            "Phone: " + p.getPhone() + "\n" +
            "Address: " + p.getAddress();   

    // Print Western List
    System.out.println("\n===Western List===");
    for (Person person:list1){
        System.out.println(
            person.printCustom(westernStyle)
        );
    }

    // Print Eastern List
    System.out.println("\n===Eastern List===");
    for (Person person:list1){
        System.out.println(
            person.printCustom(easternStyle)
        );
    }

  }
}

上面的示例中演示了各種使用方式。也可以把 Lambda 表示式儲存到一個變數中,然後用這個變數來呼叫函式。

以上程式碼可以在這裡下載:LambdaFunctionExamples.zip

當集合遇到 Lambda 表示式

前面介紹瞭如何配合 Function 介面來使用 Lambda 表示式。其實 Lambda 表示式最強大的地方是配合集合使用。

在前面的示例中我們多次用到了集合。並且一些使用 Lambda 表示式 的地方也改變了我們使用集合的方式。這裡我們再來介紹一些配合集合使用的高階用法。

我們可以把前面三種搜尋條件封裝到一個 SearchCriteria 類中:

public class SearchCriteria {

  private final Map<String, Predicate<Person>> searchMap = new HashMap<>();

  private SearchCriteria() {
    super();
    initSearchMap();
  }

  private void initSearchMap() {
    Predicate<Person> allDrivers = p -> p.getAge() >= 16;
    Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
    Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65;

    searchMap.put("allDrivers", allDrivers);
    searchMap.put("allDraftees", allDraftees);
    searchMap.put("allPilots", allPilots);

  }

  public Predicate<Person> getCriteria(String PredicateName) {
    Predicate<Person> target;

    target = searchMap.get(PredicateName);

    if (target == null) {

      System.out.println("Search Criteria not found... ");
      System.exit(1);

    }

    return target;

  }

  public static SearchCriteria getInstance() {
    return new SearchCriteria();
  }
}

每個 Predicate 示例都儲存在這個類中,然後可以在後面測試程式碼中使用。

迴圈

先來看看結合中的 forEach 函式如何配合 Lambda 表示式使用:

public class Test01ForEach {

  public static void main(String[] args) {

    List<Person> pl = Person.createShortList();

    System.out.println("\n=== Western Phone List ===");
    pl.forEach( p -> p.printWesternName() );

    System.out.println("\n=== Eastern Phone List ===");
    pl.forEach(Person::printEasternName);

    System.out.println("\n=== Custom Phone List ===");
    pl.forEach(p -> { System.out.println(p.printCustom(r -> "Name: " + r.getGivenName() + " EMail: " + r.getEmail())); });

  }

}

第一個使用了標準的 Lambda 表示式,呼叫 Person 物件的 printWesternName 函式來列印名字。而第二個使用者則演示瞭如何使用函式引用(method reference)。如果要執行物件上的一個函式則這種函式引用的方式可以替代標準的 Lambda 的語法。最後一個演示瞭如何 printCustom 函式。注意檢視在 Lambda 表示式裡面巢狀 Lambda 表示式的時候,引數的名字是有變化的。(第一個 Lambda 表示式的引數為 p 而第二個為 r)

Chaining and Filters

除了迴圈迭代集合以外,還可以串聯多個函式的呼叫。如下所示:

public class Test02Filter {

  public static void main(String[] args) {

    List<Person> pl = Person.createShortList();

    SearchCriteria search = SearchCriteria.getInstance();

    System.out.println("\n=== Western Pilot Phone List ===");

    pl.stream().filter(search.getCriteria("allPilots"))
      .forEach(Person::printWesternName);

    System.out.println("\n=== Eastern Draftee Phone List ===");

    pl.stream().filter(search.getCriteria("allDraftees"))
      .forEach(Person::printEasternName);

  }
}

先把集合轉換為 stream 流,然後就可以串聯呼叫多個操作了。這裡先用搜尋條件過濾集合,然後在符合過濾條件的新集合上執行迴圈列印操作。

Getting Lazy

上面演示的功能有用,但是集合中已經有迴圈方法了為啥還需要新增一個新的迴圈的方式呢? 通過把迴圈迭代集合的功能實現到類庫中,Java 開發者可以做更多的程式碼優化。要進一步解釋這個概念,需要先了解一些術語:

  • Laziness:在程式語言中,Laziness 代表只有當你需要處理該物件的時候才去處理他們。在上面的示例中,最後一種迴圈變數的方式為 lazy 的,因為通過搜尋條件的物件只有 2 個留著集合中,最終的列印人名只會發生在這兩個物件上。
  • Eagerness: 在集合中的每個物件上都執行操作別稱之為 eager。例如一個 增強的 for 迴圈遍歷一個集合去處理裡面的兩個物件,並稱之為更加 eager 。

stream 函式

前面的示例中,在過濾和迴圈操作之前,先呼叫了stream 函式。該函式把集合物件變為一個 java.util.stream.Stream 物件。在 Stream 物件上可以串聯呼叫各種操作。預設情況下,一個物件被處理後在 stream 中就不可用了。所以一個特定 stream 物件上的串聯操作只能執行一次。 同時 Stream 還可以是順序(預設如此)執行還可以並行執行。最後我們會演示並行執行的示例。

變化和結果(Mutation and Results)

前面已經說了, Stream 使用後就不能再次使用了。因此,在 Stream 中的物件狀態不能改變,也就是要求每個元素都是不可變的。但是,如果你想在串聯操作中返回物件該如何辦呢? 可以把結果儲存到一個新的集合中。如下所示:

public class Test03toList {

  public static void main(String[] args) {

    List<Person> pl = Person.createShortList();

    SearchCriteria search = SearchCriteria.getInstance();

    // Make a new list after filtering.
    List<Person> pilotList = pl
            .stream()
            .filter(search.getCriteria("allPilots"))
            .collect(Collectors.toList());

    System.out.println("\n=== Western Pilot Phone List ===");
    pilotList.forEach(Person::printWesternName);

  }

}

上面示例中的 collect 函式把過濾的結果儲存到一個新的結合中。然後我們可以遍歷這個集合。

使用 map 來計算結果

map 函式通常配合 filter 使用。該 函式使用一個物件並把他轉換為另外一個物件。下面顯示瞭如何通過map 來計算所有人的年齡之和。

public class Test04Map {

  public static void main(String[] args) {
    List<Person> pl = Person.createShortList();

    SearchCriteria search = SearchCriteria.getInstance();

    // Calc average age of pilots old style
    System.out.println("== Calc Old Style ==");
    int sum = 0;
    int count = 0;

    for (Person p:pl){
      if (p.getAge() >= 23 && p.getAge() <= 65 ){
        sum = sum + p.getAge();
        count++;
      }
    }

    long average = sum / count;
    System.out.println("Total Ages: " + sum);
    System.out.println("Average Age: " + average);

    // Get sum of ages
    System.out.println("\n== Calc New Style ==");
    long totalAge = pl
            .stream()
            .filter(search.getCriteria("allPilots"))
            .mapToInt(p -> p.getAge())
            .sum();

    // Get average of ages
    OptionalDouble averageAge = pl
            .parallelStream()
            .filter(search.getCriteria("allPilots"))
            .mapToDouble(p -> p.getAge())
            .average();

    System.out.println("Total Ages: " + totalAge);
    System.out.println("Average Age: " + averageAge.getAsDouble());    

  }

}

第一種使用傳統的 for 迴圈來計算平均年齡。第二種使用 map 把 person 物件轉換為其年齡的整數值,然後計算其總年齡和平均年齡。

在計算平均年齡時候還呼叫了 parallelStream 函式,所以平均年齡可以並行的計算。

上面的示例程式碼可以在這裡下載: LambdaCollectionExamples.zip

總結

感覺如何?是不是覺得 Lambda 表示式棒棒噠,亟不可待的想在專案中使用了吧。 神馬? 你說 Andorid 不支援 Java 8 不能用 Lambda 表示式。好吧,其實你可以使用 gradle-retrolambda 外掛把 Lambda 表示式 抓換為 Java 7 版本的程式碼。 還不趕緊去試試!!

相關文章