
【Guava 教學】(3)高階排序概念的實現
排名這種東西,人類還蠻愛的,到了程式設計領域,排序這東西依舊舉足輕重。在 Java 中要進行排序,可以使用標準 API 中 用三個值來表示順序,蠻方便的,不是嗎?並不是!有多少次你弄錯了 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); } } 你覺得 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(); } }
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; } ... 談到模式,物件導向的開發者都會想到設計模式,想到有關創建、結構與行為模式。實際上寫程式時也有許多流程重複著一定模式,只是因為程式碼太混亂,程式區塊混著太多職責,而觀察不出來罷了,只要你能讓每段程式流程的職責單一化,就可以觀察並抽取出更高階的語義,像是這邊就可抽取出每個 程式碼太混亂,程式區塊混著太多職責,觀察不出模式,抽取不出高階抽象的另一壞處就是,沒辦法重用某些基礎元素,沒辦法基於這些元素建構更複雜的的元素,因此,每次都得撰寫重複的東西。 舉例來說,如果你不想要用物件天生的順序進行排序,那麼 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; } } 不怎麼好讀,對吧!更別說為了表示這個比較器的目的,必須取個又臭又長的類別名稱,雖然在必要的時候,不用去畏懼取個較長的名稱,不過名稱真的太長,長到影響可讀性,或者很難簡短地描述出它的意圖時,就得想一下,是不是它做了太多事了? 仔細想想,將 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); } }
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)); } } 好吧!難道不能傳入 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)); } } 現在你連 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()))) ); 這麼一連串的抽取,達到了一些元素的重用,不過語義上並不怎麼流暢,如果比較器可以主動生出另一個比較器,可以改進一下這個問題。在繼續進行重構之前,你發現了 Guava 做了你想做的事了,那就拿來用吧! Collections.sort(names, Ordering.natural().reverse().nullsFirst() .onResultOf(new Function<String, Integer>() { @Override public Integer apply(String p) { return p == null ? null : p.length(); } }) );
public abstract class Ordering<T> extends Object implements Comparator<T> 不過,它是個功能強悍的比較器,可以基於目前的比較器,加上某個條件,直接產生新的 Collections.sort(names, Ordering.natural().reverse().nullsFirst() .onResultOf(p -> p == null ? null : p.length()) ); 只要事物不斷重複,就會形成一種模式,若能抽取出模式,就能用更高階的語義來表述意圖。Guava 在排序這方面,某些部份就是在表現這類意涵,不僅只有排序,實際上撰寫程式時,還存在許多高階語義在程式流程之中,只是就如先前所談到的,也許是因為程式碼太混亂,或者程式區塊混著太多職責,而觀察不出來罷了,因為看不出來,所以重複的工作就一再進行,日復一日地… Guava 看來只是個程式庫,但它實際上包括了不少高階觀念,先前的兩篇文章 從避免使用 null 開始、命名明確的條件檢查,其實也都是在談這些高階觀念,想善用 Guava,瞭解這些觀念是必要的,不然,只是當個程式庫來使用,就沒辦法用得順手,這樣是有點可惜了。 嗯? |
caterpillar
06/28
Guava的Wiki對Ordering的說明中指出,Ordering的操作鏈閱讀時應由右往左讀,才能瞭解排序的結果,這反而令人不易瞭解語意;人類閱讀方式基本上就是由左而右,Ordering的操作鏈由左而右的閱讀方式,目的在於自然地瞭解Ordering實例會是如何組裝而成。