【Guava 教學】(8)你需要的其實是範圍(Range)?


如果想要建立一個包括 1 到 5 的數列,你會怎麼做?手寫一個 Arrays.asList(1, 2, 3, 4, 5)?如果是 1 到 20 呢?手寫一個看來就有點笨了,至少用個迴圈 ...
List<Integer> list = new ArrayList<>(20);
for(int i = 1; i <= 20; i++) {
    list.add(i);
}
那如果要建立一個數列,包括字元 'a' 到 'z' 呢?也許是這樣寫:
List<Character> list = new ArrayList<>(26);
for(char c = 'a'; c <= 'z'; c++) {
    list.add(c);
}
如果你需要一個無限長的自然數列呢?實際上不可能在電腦上產生無限長的數列,所以得撰寫個產生器(Generator),只是在 Java 中撰寫產生器有點麻煩。如果使用 Guava 的 Range,則只需要撰寫 Range.atLeast(1),就代表著 1 到 +∞ 的自然數範圍,如果要建立 1 到 20 的範圍,則可以撰寫為 Range.closed(1, 20),如果要建立包括字元 'a' 到 'z' 的範圍,則可以撰寫為 Range.closed('a', 'z')Range 的一些 static 方法與範圍的對照為:
(a..b) {x | a < x < b} open
[a..b] {x | a <= x <= b} closed
(a..b] {x | a < x <= b} openClosed
[a..b) {x | a <= x < b} closedOpen
(a..+∞) {x | x > a} greaterThan
[a..+∞) {x | x >= a} atLeast
(-∞..b) {x | x < b} lessThan
(-∞..b] {x | x <= b} atMost
(-∞..+∞) {x} all
你也可以使用字串建立範圍,例如 Range.closed("abc", "abz")。實際上,任何物件只要與生俱有順序,也就是任何物件只要實作了 Comparable,都可以使用 Range 來建立範圍。一旦有了 Range,你就可以作一些基本的查詢動作,像是使用 containscontainAll 來察看某元素或某些元素是否包括在建立的範圍內。
RangeRange 之間也可以進行範圍的相關測試或操作,像是使用 enclose 方法測試某範圍是否包括另一範圍,使用 isConnected 測試某範圍是否連接至另一範圍,使用 intersection 取得兩個範圍的交集,使用 span 來建立橫跨兩個範圍的範圍,你可以在 RangeExplained 的 Operations 察看相關範例程式碼。
實際上,範圍不是數列,也就是像 Range.closed(1, 20) 並沒有實際產生 1、2 ... 20 的整數數列,它就僅僅只是個…呃…範圍!如果想要取得的是範圍中的數字,那麼可以透過 ContiguousSet 類別 staticcreate 方法,呼叫時必須指定 Range 物件及一個 DiscreteDomain 物件,DiscreteDomain 物件定義了指定的範圍中,不連續元素間的關係以及 DiscreteDomain 的邊界。
由於經常打算取得的是整數,因此 DiscreteDomain 提供了 integerslongs 以及支援大數的 bigIntegers 三個 static 方法。例如,結合 RangeDiscreteDomain 來迭代 1 到 20 的數字,可以如下撰寫:
for(int i : create(Range.closed(1, 20), integers())) {
    // 做些事 ...            
}
create 方法不會傳回整個數列,它傳回的是 Set,因此實際上 for 迴圈是運用其 iterator 方法傳回的 Iterator 物件進行迭代,也就是這讓你有實現惰性(Laziness)的可能性。例如,如果你要找的某 int 數是在自然數列中,但不確定其範圍,那麼就可以如下撰寫:
for(int i : create(Range.atLeast(1), integers())) {
    // 做些運算
    if(某些條件) { break; }
}
比起一開始就建立一個包括 Integer.MIN_VALUEInteger.MAX_VALUEList,以上方法顯然經濟多了。你可以建立自己的 DiscreteDomain,例如,建立小寫字母的 LowerCaseDomain 話,可以如下定義:
class LowerCaseDomain extends DiscreteDomain<Character> {
    private static LowerCaseDomain domain = new LowerCaseDomain();

    public static DiscreteDomain letters() {
        return domain;
    }

    @Override
    public Character next(Character c) {
        return (char) (c + 1);
    }

    @Override
    public Character previous(Character c) {
        return (char) (c - 1);
    }

    @Override
    public long distance(Character start, Character end) {
        return end - start;
    }

    @Override
    public Character maxValue() {
        return 'z';
    }

    @Override
    public Character minValue() {
        return 'a';
    }
}
在繼承 DiscreteDomain 後,一定要實作的三個方法是 nextpreviousdistance,當你建立的範圍是有界時,若要取得下一個不連續元素,會呼叫 next 方法,若要取得前一個不連續元素,則會呼叫 previousdistance 則指出,從範圍的 startend 間,必須呼叫幾次 next 才能達到。
如果你指定的範圍是無界的,像是指定 Range.atLeast('a') 時,則必須定義 DiscreteDomainmaxValueminValue,這兩個方法指出在 DiscreteDomain 中最大值與最小值為何,這很重要,範圍可以是無界,但 DiscreteDomain 會是有界的。例如 DiscreteDomainintegers 方法傳回的是 IntegerDomain 其邊界是 Integer.MIN_VALUEInteger.MAX_VALUE,這是受限於 int 的位元組長度,因而其是有界的:
  private static final class IntegerDomain extends DiscreteDomain<Integer>
      implements Serializable {
    ...
    @Override public Integer minValue() {
      return Integer.MIN_VALUE;
    }

    @Override public Integer maxValue() {
      return Integer.MAX_VALUE;
    }
    ...
  }
同理,DiscreteDomainlongs 方法傳回的是 LongDomain 其邊界是 Long.MIN_VALUELong.MAX_VALUE,這是受限於 long 的位元組長度:
  private static final class LongDomain extends DiscreteDomain<Long>
      implements Serializable {
    ...
    @Override public Long minValue() {
      return Long.MIN_VALUE;
    }

    @Override public Long maxValue() {
      return Long.MAX_VALUE;
    }
    ...
  }
先前定義的 LowerCaseDomain 是有界的,也就是 'a' 到 'z',你可以這麼使用:
for(char i : create(Range.closed('a', 'z'), LowerCaseDomain.letters())) {
    // 做些事 ...
}
for(char i : create(Range.atLeast('m'), LowerCaseDomain.letters())) {
    // 做些事 ...
}
實際上,你需要的或許只是範圍,那麼用 Range 就足夠了,如果真的需要逐一取得範圍中的不連續元素,搭配 DiscreteDomain 就可以達到目的,而且不用一開始就建立所有的元素,只需在必要的時候取用即可。