如果想要建立一個包括 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
,你就可以作一些基本的查詢動作,像是使用 contains 或 containAll
來察看某元素或某些元素是否包括在建立的範圍內。
Range
與 Range
之間也可以進行範圍的相關測試或操作,像是使用 enclose
方法測試某範圍是否包括另一範圍,使用 isConnected
測試某範圍是否連接至另一範圍,使用 intersection
取得兩個範圍的交集,使用 span
來建立橫跨兩個範圍的範圍,你可以在 RangeExplained 的 Operations 察看相關範例程式碼。實際上,範圍不是數列,也就是像
Range.closed(1, 20)
並沒有實際產生 1、2 ... 20
的整數數列,它就僅僅只是個…呃…範圍!如果想要取得的是範圍中的數字,那麼可以透過 ContiguousSet
類別 static
的 create 方法,呼叫時必須指定 Range
物件及一個 DiscreteDomain
物件,DiscreteDomain
物件定義了指定的範圍中,不連續元素間的關係以及 DiscreteDomain
的邊界。
由於經常打算取得的是整數,因此 DiscreteDomain
提供了 integers
、 longs
以及支援大數的 bigIntegers
三個 static
方法。
例如,結合 Range
與 DiscreteDomain
來迭代 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_VALUE
至 Integer.MAX_VALUE
的 List
,以上方法顯然經濟多了。你可以建立自己的 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
後,一定要實作的三個方法是 next
、previous
與 distance
,當你建立的範圍是有界時,若要取得下一個不連續元素,會呼叫 next
方法,若要取得前一個不連續元素,則會呼叫 previous
,distance
則指出,從範圍的 start
至 end
間,必須呼叫幾次 next
才能達到。如果你指定的範圍是無界的,像是指定
Range.atLeast('a')
時,則必須定義 DiscreteDomain
的 maxValue
與 minValue
,這兩個方法指出在 DiscreteDomain
中最大值與最小值為何,這很重要,範圍可以是無界,但 DiscreteDomain
會是有界的。例如 DiscreteDomain
的 integers
方法傳回的是 IntegerDomain
其邊界是 Integer.MIN_VALUE
與 Integer.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;
}
...
}
同理,DiscreteDomain
的 longs
方法傳回的是 LongDomain
其邊界是 Long.MIN_VALUE
與 Long.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
就可以達到目的,而且不用一開始就建立所有的元素,只需在必要的時候取用即可。