排名這種東西,人類還蠻愛的,到了程式設計領域,排序這東西依舊舉足輕重。在 Java 中要進行排序,可以使用標準 API 中
Collections 的 sort 方法,使用前必須遵守的協定是物件本身必須有天生的順序(Natural ordering),也就是必須擁有 Comparable 的行為,語法上具體來說,就是必須實作 Comparable 介面。由於 Collections 已經實作了排序演算法,你只需要告訴它,如果物件要與另一個物件比較時,順序上哪個大哪個小,也就是 compareTo 傳回 1、0、-1 分別來告訴它,順序上物件比另一物件大、相等或小。用三個值來表示順序,蠻方便的,不是嗎?並不是!有多少次你弄錯了 1、0、-1 的意義呢?實際上,排序的要求還蠻多的,例如你可能想要排序時先依某人的姓來排,如果姓相同再依名來排,如果姓名都相同,再依他們居住地的郵遞區號來排,那你就可能會寫出像 compare/compareTo 中的程式碼:
class Person implements Comparable<Person> {
  private String lastName;
  private String firstName;
  private int zipCode;
  public int compareTo(Person other) {
    int cmp = lastName.compareTo(other.lastName);
    if (cmp != 0) {
      return cmp;
    }
    cmp = firstName.compareTo(other.firstName);
    if (cmp != 0) {
      return cmp;
    }
    return Integer.compare(zipCode, other.zipCode);
  }
}compareTo 好讀嗎?如果你學過 SQL,應該知道有 ORDER BY 這東西,相較之下,compareTo 的邏輯並不清楚,如果你使用 Guava 的 ComparisonChain,可以寫成這樣:
      import com.google.common.collect.ComparisonChain;
class Person implements Comparable<Person> {
  private String lastName;
  private String firstName;
  private int zipCode;
  public int compareTo(Person other) {
    return ComparisonChain.start()
             .compare(lastName, other.lastName)
             .compare(firstName, other.firstName)
             .compare(zipCode, other.zipCode)
           .result();
  }
}ComparisonChain 的 start 與 compare 都會傳回 ComparisonChain 實例,在最後 result 計算結果時,就如原先 compareTo 的實作,會逐一比較要比較的對象,只要它們各自的 compareTo 不為 0 時就傳回結果。Guava 在排序上提供了一些 API ,確實是很好用,不過這不是這篇文章要論述的,這邊要談的是,如何觀察並抽取出程式碼中更高階的抽象概念,像是排序這樣的東西,其實也一直重複著某些模式。上例中的模式就是:
int cmp = f1.compareTo(other.f1);
if (cmp != 0) {
  return cmp;
}
cmp = f2.compareTo(other.f2);
if (cmp != 0) {
  return cmp;
}
cmp = f3.compareTo(other.f3);
if (cmp != 0) {
  return cmp;
}
...compare 的語義,而這就是 ComparisonChain 在做的事,在使用它之前,你就像是在告訴電腦低階指令,1、0、-1?這什麼東西?在使用 ComparisonChain 之後,會比較像是在跟人說話了。程式碼太混亂,程式區塊混著太多職責,觀察不出模式,抽取不出高階抽象的另一壞處就是,沒辦法重用某些基礎元素,沒辦法基於這些元素建構更複雜的的元素,因此,每次都得撰寫重複的東西。
舉例來說,如果你不想要用物件天生的順序進行排序,那麼
Collections 的 sort 還有另一個版本,可以指定一個比較器,也就是實現了 Comparator 介面的物件,它的 compare 方法也是得傳回 1、0、-1,告訴 Collections 的 sort 兩個被傳入的元素順序為何。例如,如果有個 List 中某些索引處包括 null,現在你打算讓 那些 null 排在最前頭,之後依字串的長度由大到小排序,那會怎麼寫?
      class StringLengthInverseNullFirstComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
        if(s1 == s2) {
            return 0;
        }
        if(s1 == null) {
            return -1;
        }
        if(s2 == null) {
            return 1;
        }
        if(s1.length() == s2.length()) {
            return 0;
        }
        if(s1.length() > s2.length()) {
            return -1;
        } 
        return 1;
    }
}仔細想想,將
null 全部排序在前或者在後,其實是一種常見的需求,長度是個整數,本身就有由小至大的天生順序,如果要由大至小,就是反向排序,反向排序也是一個經常的需求。有沒有辦法將這些常見需求抽取出來呢?試試看!
      class Natural implements Comparator<Comparable> {
    @Override
    public int compare(Comparable c1, Comparable c2) {
        return c1.compareTo(c2);
    }
}
class Inverse implements Comparator {
    private Comparator comparator;
    public Inverse(Comparator comparator) {
        this.comparator = comparator;
    }
    @Override
    public int compare(Object o1, Object o2) {
        return -comparator.compare(o1, o2);
    }
}
class NullsFirst implements Comparator {
    private final static int LEFT_IS_GREATER = 1;
    private final static int RIGHT_IS_GREATER = -1;
    private Comparator comparator;
    public NullsFirst(Comparator comparator) {
        this.comparator = comparator;
    }
    @Override
    public int compare(Object o1, Object o2) {
        if(o1 == o2) {
            return 0;
        }
        if(o1 == null) {
            return RIGHT_IS_GREATER;
        }
        if(o2 == null) {
            return LEFT_IS_GREATER;
        }
        return comparator.compare(o1, o2);
    }
}Natural、Inverse、NullsFirst 都是從過去排序經驗中,不斷重現的流程模式中抽取出來的比較器,這樣一來,先前那個 StringLengthInverseNullFirstComparator 就可以基於它們來建構了:
      interface F1<P, R> {
    R apply(P p);
}
class StringLengthInverseNullFirstComparator implements Comparator<String> {
    private Comparator comparator = new NullsFirst(new Inverse(new Natural()));
    private F1<String, Integer> f = new F1<String, Integer>() {
        @Override
        public Integer apply(String p) {
            return p == null ? null : p.length();
        }
    };
    @Override
    public int compare(String s1, String s2) {
        return comparator.compare(f.apply(s1), f.apply(s2));
    }
}F1 實例就好嗎?這麼一來,連上頭那個 compare 方法中的流程也能重用了:
      class OnResultOf<P, R> implements Comparator<P> {
    private Comparator comparator;
    private F1<P, R> f;
    public OnResultOf(F1<P, R> f, Comparator comparator) {
        this.f = f;
        this.comparator = comparator;
    }
    @Override
    public int compare(P p1, P p2) {
        return comparator.compare(f.apply(p1), f.apply(p2));
    }    
}StringLengthInverseNullFirstComparator 都不用定義了,可以直接建構並組合相關實例就可以進行複雜的排序了:
      List names = Arrays.asList("Monica", null, "Justin", null, "Jao");
Collections.sort(names, 
        new OnResultOf(new F1<String, Integer>() {
            @Override
            public Integer apply(String p) {
                return p == null ? null : p.length();
            }}, 
        new NullsFirst(new Inverse(new Natural())))
);Collections.sort(names, 
        Ordering.natural().reverse().nullsFirst()
                .onResultOf(new Function<String, Integer>() {
                    @Override
                    public Integer apply(String p) {
                        return p == null ? null : p.length();
                    }
               })
);Ordering 本身就是比較器,這看看它的類別定義就知道了:
      public abstract class Ordering<T> extends Object implements Comparator<T>Ordering 實例,就如上面的例子中看到的,不過我承認,那個匿名類別實例語法挺惱人的,如果可以用上 JDK8 的 Lambda 語法,也許會好一些:
      Collections.sort(names, 
        Ordering.natural().reverse().nullsFirst()
            .onResultOf(p -> p == null ? null : p.length())
);Guava 看來只是個程式庫,但它實際上包括了不少高階觀念,先前的兩篇文章 從避免使用 null 開始、命名明確的條件檢查,其實也都是在談這些高階觀念,想善用 Guava,瞭解這些觀念是必要的,不然,只是當個程式庫來使用,就沒辦法用得順手,這樣是有點可惜了。 嗯?
Ordering 更多的範例?其實看 API 文件應該就夠清楚了,如果你瞭解 Ordering 存在的目的的話!網路上搜尋一下 「Guava Ordering」,你可以找到一卡車的資料。好吧!我知道有些人很性急,那麼 google's guava library tutorial part 2: joys of Ordering 這個鏈結有不少簡單易懂的範例。
