Java經典類庫-Guava中的函數語言程式設計講解

五柳-先生發表於2016-05-23

如果我要新建一個java的專案,那麼有兩個類庫是必備的,一個是junit,另一個是Guava。選擇junit,因為我喜歡TDD,喜歡自動化測試。而是用Guava,是因為我喜歡簡潔的API。Guava提供了很多的實用工具函式來彌補java標準庫的不足,另外Guava還引入了函數語言程式設計的概念,在一定程度上緩解了java在JDK1.8之前沒有lambda的缺陷,使使用java書寫簡潔易讀的函式式風格的程式碼成為可能。

下面就簡單的介紹下Guava中的一些體現了函數語言程式設計的API。

Filter

我們先建立一個簡單的Person類。

Person.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Person {
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

如果要產生一個Person類的List,通常的寫法可能是這樣子。

1
2
3
4
5
        List<Person> people = new ArrayList<Person>();
        people.add(new Person("bowen",27));
        people.add(new Person("bob", 20));
        people.add(new Person("Katy", 18));
        people.add(new Person("Logon", 24));

Guava提供了一個newArrayList的方法,其自帶型別推演,並可以方便的生成一個List,並且通過引數傳遞初始化值。

1
2
3
4
        List<Person> people = newArrayList(new Person("bowen", 27),
                new Person("bob", 20),
                new Person("Katy", 18),
                new Person("Logon", 24));

當然,這不算函數語言程式設計的範疇,這是Guava給我們提供的一個實用的函式。

如果我們選取其中年齡大於20的人,通常的寫法可能是這樣子。

1
2
3
4
5
6
        List<Person> oldPeople = new ArrayList<Person>();
        for (Person person : people) {
            if (person.getAge() >= 20) {
                oldPeople.add(person);
            }
        }

這就是典型的filter模式。filter即從一個集合中根據一個條件篩選元素。其中person.getAge() >=20就是這個條件。Guava為這種模式提供了一個filter的方法。所以我們可以這樣寫。

1
2
3
4
5
        List<Person> oldPeople = newArrayList(filter(people, new Predicate<Person>() {
            public boolean apply(Person person) {
                return person.getAge() >= 20;
            }
        }));

這裡的Predicate是Guava中的一個介面,我們來看看它的定義。

Predicate.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@GwtCompatible
public interface Predicate<T> {
  /**
   * Returns the result of applying this predicate to {@code input}. This method is <i>generally
   * expected</i>, but not absolutely required, to have the following properties:
   *
   * <ul>
   * <li>Its execution does not cause any observable side effects.
   * <li>The computation is <i>consistent with equals</i>; that is, {@link Objects#equal
   *     Objects.equal}{@code (a, b)} implies that {@code predicate.apply(a) ==
   *     predicate.apply(b))}.
   * </ul>
   *
   * @throws NullPointerException if {@code input} is null and this predicate does not accept null
   *     arguments
   */
  boolean apply(@Nullable T input);

  /**
   * Indicates whether another object is equal to this predicate.
   *
   * <p>Most implementations will have no reason to override the behavior of {@link Object#equals}.
   * However, an implementation may also choose to return {@code true} whenever {@code object} is a
   * {@link Predicate} that it considers <i>interchangeable</i> with this one. "Interchangeable"
   * <i>typically</i> means that {@code this.apply(t) == that.apply(t)} for all {@code t} of type
   * {@code T}). Note that a {@code false} result from this method does not imply that the
   * predicates are known <i>not</i> to be interchangeable.
   */
  @Override
  boolean equals(@Nullable Object object);
}

裡面只有一個apply方法,接收一個泛型的實參,返回一個boolean值。由於java世界中函式並不是一等公民,所以我們無法直接傳遞一個條件函式,只能通過Predicate這個類包裝一下。

And Predicate

如果要再實現一個方法來查詢People列表中所有名字中包含b字母的列表,我們可以用Guava簡單的實現。

1
2
3
4
5
        List<Person> namedPeople = newArrayList(filter(people, new Predicate<Person>() {
            public boolean apply(Person person) {
                return person.getName().contains("b");
            }
        }));

一切是這麼的簡單。 那麼新需求來了,如果現在需要找年齡>=20並且名稱包含b的人,該如何實現那? 可能你會這樣寫。

1
2
3
4
5
        List<Person> filteredPeople = newArrayList(filter(people, new Predicate<Person>() {
            public boolean apply(Person person) {
                return person.getName().contains("b") && person.getAge() >= 20;
            }
        }));

這樣寫的話就有一定的程式碼重複,因為之前我們已經寫了兩個Predicate來分別實現這兩個條件判斷,能不能重用之前的Predicate那?答案是能。 我們首先將之前生成年齡判斷和名稱判斷的兩個Predicate抽成方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    private Predicate<Person> ageBiggerThan(final int age) {
        return new Predicate<Person>() {
            public boolean apply(Person person) {
                return person.getAge() >= age;
            }
        };
    }

private Predicate<Person> nameContains(final String str) {
        return new Predicate<Person>() {
            public boolean apply(Person person) {
                return person.getName().contains(str);
            }
        };
    }

而我們的結果其實就是這兩個Predicate相與。Guava給我們提供了and方法,用於對一組Predicate求與。

1
      List<Person> filteredPeople = newArrayList(filter(people, and(ageBiggerThan(20), nameContains("b"))));

由於and接收一組Predicate,返回也是一個Predicate,所以可以直接作為filter的第二個引數。如果不熟悉函數語言程式設計的人可能感覺有點怪異,但是習慣了就會覺得它的強大與簡潔。 當然除了and,Guava還為我們提供了or,用於對一組Predicate求或。這裡就不多講了,大家可以自己練習下。

Map(transform)

列表操作還有另一個常見的模式,就是將陣列中的所有元素對映為另一種元素的列表,這就是map pattern。舉個例子,求People列表中的所有人名。程式設計師十有八九都會這樣寫。

1
2
3
4
        List<String> names = new ArrayList<String>();
        for (Person person : people) {
            names.add(person.getName());
        }

Guava已經給我們提供了這種Pattern的結果辦法,那就是使用transform方法。

1
2
3
4
5
        List<String> names = newArrayList(transform(people, new Function<Person, String>() {
            public String apply( Person person) {
                return person.getName();
            }
        }));

Function是另外一種用於封裝函式的介面物件。它的定義如下:

Function.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@GwtCompatible
public interface Function<F, T> {
  /**
   * Returns the result of applying this function to {@code input}. This method is <i>generally
   * expected</i>, but not absolutely required, to have the following properties:
   *
   * <ul>
   * <li>Its execution does not cause any observable side effects.
   * <li>The computation is <i>consistent with equals</i>; that is, {@link Objects#equal
   *     Objects.equal}{@code (a, b)} implies that {@code Objects.equal(function.apply(a),
   *     function.apply(b))}.
   * </ul>
   *
   * @throws NullPointerException if {@code input} is null and this function does not accept null
   *     arguments
   */
  @Nullable T apply(@Nullable F input);

  /**
   * Indicates whether another object is equal to this function.
   *
   * <p>Most implementations will have no reason to override the behavior of {@link Object#equals}.
   * However, an implementation may also choose to return {@code true} whenever {@code object} is a
   * {@link Function} that it considers <i>interchangeable</i> with this one. "Interchangeable"
   * <i>typically</i> means that {@code Objects.equal(this.apply(f), that.apply(f))} is true for all
   * {@code f} of type {@code F}. Note that a {@code false} result from this method does not imply
   * that the functions are known <i>not</i> to be interchangeable.
   */
  @Override
  boolean equals(@Nullable Object object);
}

它與Predicate非常相似,但不同的是它接收兩個泛型,apply方法接收一種泛型實參,返回值是另一種泛型值。正是這個apply方法定義了陣列間元素一對一的map規則。

reduce

除了filter與map模式外,列表操作還有一種reduce操作。比如求people列表中所有人年齡的和。Guava並未提供reduce方法。具體原因我們並不清楚。但是我們可以自己簡單的實現一個reduce pattern。 先定義一個Func的介面。

Func.java
1
2
3
4
5
     public interface Func<F,T> {

         T apply(F currentElement, T origin);

     }

apply方法的第一個引數為列表中的當前元素,第二個引數為預設值,返回值型別為預設值型別。 然後我們定義個reduce的靜態方法。

Reduce.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Reduce {
    private Reduce() {

    }

    public static <F,T> T reduce(final Iterable<F> iterable, final Func<F, T> func, T origin) {

        for (Iterator iterator = iterable.iterator(); iterator.hasNext(); ) {
            origin = func.apply((F)(iterator.next()), origin);
        }

        return origin;
    }
}

reduce方法接收三個引數,第一個是需要進行reduce操作的列表,第二個是封裝reduce操作的Func,第三個引數是初始值。

我們可以使用這個reduce來實現求people列表中所有人的年齡之和。

1
2
3
4
5
6
        Integer ages = Reduce.reduce(people, new Func<Person, Integer>() {

            public Integer apply(Person person, Integer origin) {
                return person.getAge() + origin;
            }
        }, 0);

我們也可以輕鬆的寫一個方法來得到年齡的最大值。

1
2
3
4
5
6
        Integer maxAge = Reduce.reduce(people, new Func<Person, Integer>() {

            public Integer apply(Person person, Integer origin) {
                return person.getAge() > origin ? person.getAge() : origin;
            }
        }, 0);

Fluent pattern

現在新需求來了,需要找出年齡>=20歲的人的所有名稱。該如何操作那?我們可以使用filter過濾出年齡>=20的人,然後使用transform得到剩下的所有人的人名。

1
2
3
4
5
6
7
8
9
10
11
12
    private Function<Person, String> getName() {
        return new Function<Person, String>() {
            public String apply( Person person) {
                return person.getName();
            }
        };
    }

    public void getPeopleNamesByAge() {

        List<String> names = newArrayList(transform(filter(people, ageBiggerThan(20)), getName()));
    }

這樣括號套括號的著實不好看。能不能改進一下那?Guava為我們提供了fluent模式的API,我們可以這樣來寫。

1
      List<String> names = from(people).filter(ageBiggerThan(20)).transform(getName()).toList();

Guava中還有很多好玩的東西,大家時間可以多發掘發掘。這篇文章的原始碼已經被我放置到github中,感興趣的可以自行檢視。

相關文章