【Guava】Rangeで範囲チェックを簡潔に実装

Javaで「値がある範囲に含まれているか」をチェックする場面はよくあります。たとえば、年齢が18歳以上か、日付が有効期限内か、スコアが一定の範囲内か――こうした判定には、一般的に if (value >= 1 && value <= 10) のような条件分岐が使われます。

シンプルな条件であれば問題ありませんが、範囲が複数あったり、開区間・閉区間などの細かい境界条件が関わってくると、コードが煩雑になりやすく、バグの温床にもなりかねません。

そこで活用したいのが、Googleが提供するライブラリ GuavaRange クラスです。Range を使えば、開区間・閉区間・無限区間などを直感的に記述でき、範囲チェックを簡潔かつ明確に記述できます。

この記事では、Javaエンジニア歴10年以上の筆者が、Range の基本的な使い方から実践的なテクニック、知っておくべき注意点までを丁寧に解説します。

目次

Rangeとは?— Guavaが提供する範囲クラス

Guavaの Range<C extends Comparable> クラスは、特定の値がある区間(範囲)に含まれているかどうかを判断するためのユーティリティです。

数学における「区間」の概念(例:1以上10以下の範囲など)を、Javaでそのまま表現できるのが特徴です。たとえば、次のように使います。

Range<Integer> range = Range.closed(1, 10);
range.contains(5);  // true

Range の主な利点は以下の通りです。

  • 閉区間・開区間・半開区間を明示的に表現できる
  • 無限区間(例:5以上、10未満など)を簡単に定義できる
  • Comparable を実装した型(例:IntegerStringLocalDateなど)ならどんな値でも使える

条件分岐のロジックを簡潔にし、可読性・保守性を高めてくれるのが Range の大きな魅力です。

範囲の定義方法と種類

Guavaの Range クラスでは、さまざまな形式の区間(範囲)を柔軟に定義できます。閉区間・開区間・無限区間・単一値・動的な境界指定など、あらゆるニーズに対応しており、コードでの表現も非常に直感的です。

以下の表は、代表的な定義メソッドとその意味、例をまとめたものです。

メソッド区間表記範囲使用例
closed(a, b)[a, b]a以上b以下Range.closed(1, 10)
open(a, b)(a, b)aより大きくb未満Range.open(1, 10)
closedOpen(a, b)[a, b)a以上b未満Range.closedOpen(1, 10)
openClosed(a, b)(a, b]aより大きくb以下Range.openClosed(1, 10)
atLeast(a)[a, ∞)a以上Range.atLeast(5)
greaterThan(a)(a, ∞)aより大きいRange.greaterThan(5)
atMost(b)(-∞, b]b以下Range.atMost(5)
lessThan(b)(-∞, b)b未満Range.lessThan(5)
all()(-∞, ∞)すべての値Range.all()
singleton(a)[a]aのみRange.singleton(5)
range(a, b, BoundType, BoundType)任意の開閉組み合わせ開閉を動的に指定Range.range(1, BoundType.OPEN, 10, BoundType.CLOSED)
downTo(a, BoundType)[a, ∞) / (a, ∞)下限のみ指定Range.downTo(5, BoundType.OPEN)
upTo(b, BoundType)(-∞, b] / (-∞, b)上限のみ指定Range.upTo(10, BoundType.CLOSED)
encloseAll(Iterable<T>)最小〜最大をカバー要素集合をすべて含む範囲Range.encloseAll(List.of(1, 4, 9))[1..9]

これらのメソッドを活用することで、複雑な境界条件を if文や比較演算子に頼らず、シンプルに表現できます。特に、境界の開閉を意識した厳密なチェックが求められる場面で重宝します。

範囲の上下限の確認方法

Guavaの Range クラスでは、指定した範囲に「下限」や「上限」が存在するかどうか、またその値や境界の種類(開区間か閉区間か)を調べることができます。

これは、動的に生成された範囲の内容を確認したいときや、他の処理と組み合わせる際に役立ちます。

下限に関するメソッド

メソッド説明使用例
hasLowerBound()下限が存在するかどうかを返すRange.atLeast(5).hasLowerBound()true
lowerEndpoint()下限の値を返すRange.closed(3, 8).lowerEndpoint()3
lowerBoundType()下限が開区間か閉区間か(OPEN / CLOSEDRange.openClosed(3, 8).lowerBoundType()OPEN
Range<Integer> range = Range.closedOpen(3, 8);
if (range.hasLowerBound()) {
    System.out.println("下限: " + range.lowerEndpoint()); // 3
    System.out.println("下限の種類: " + range.lowerBoundType()); // CLOSED
}

上限に関するメソッド

メソッド説明使用例
hasUpperBound()上限が存在するかどうかを返すRange.lessThan(10).hasUpperBound()true
upperEndpoint()上限の値を返すRange.closed(3, 8).upperEndpoint()8
upperBoundType()上限が開区間か閉区間か(OPEN / CLOSEDRange.closedOpen(3, 8).upperBoundType()OPEN
Range<Integer> range = Range.open(3, 8);
if (range.hasUpperBound()) {
    System.out.println("上限: " + range.upperEndpoint()); // 8
    System.out.println("上限の種類: " + range.upperBoundType()); // OPEN
}

これらのメソッドを使えば、範囲の詳細な条件を動的に把握してロジックに反映することが可能になります。たとえば、「下限のみある範囲か?」「上限が開区間か閉区間か?」といった分岐が必要な場面で役立ちます。

値や範囲のチェック方法

Guavaの Range では、単に範囲を定義するだけでなく、その範囲に対してさまざまなチェック処理を行うことができます。

contains():値が範囲内かどうかを判定

単一の値が指定した範囲に含まれるかどうかを判定します。

Range<Integer> range = Range.closed(1, 10);
range.contains(5);  // true
range.contains(11); // false

containsAll():複数の値がすべて範囲内かどうか

指定したコレクション内のすべての要素が範囲に収まっているかどうかを判定します。

Range<Integer> range = Range.closed(1, 10);

List<Integer> ok = List.of(3, 5, 7);
range.containsAll(ok); // true

List<Integer> ng = List.of(3, 11);
range.containsAll(ng); // false

isEmpty():範囲が空であるかどうか

範囲に有効な値が1つも含まれていない場合、true を返します。

Range<Integer> empty = Range.openClosed(5, 5);
empty.isEmpty(); // true(5より大きく、かつ5以下 → 該当なし)

Range<Integer> single = Range.closed(5, 5);
single.isEmpty(); // false(5ちょうどを含む)

encloses():別の範囲を完全に含むかどうか

ある Range が、別の Range をすべて含んでいるかどうかをチェックします。

Range<Integer> outer = Range.closed(1, 10);
Range<Integer> inner = Range.closed(3, 7);

outer.encloses(inner); // true

isConnected():2つの範囲が接続・重なっているか

2つの範囲が重なっている、または隣接していて連続性がある場合に true を返します。

Range<Integer> r1 = Range.closed(1, 5);
Range<Integer> r2 = Range.closed(4, 10);

r1.isConnected(r2); // true(4〜5が重なる)

📌 補足: isConnected() は重なりだけでなく「隣接している」場合も true になります。

範囲間の演算

Guavaの Range クラスは、単なる範囲チェックだけでなく、複数の範囲に対する演算や変換処理も強力にサポートしています。複雑な条件ロジックやデータフィルタリングの実装時に役立つ機能を紹介します。

intersection():2つの範囲の重なり部分を取得

2つの範囲が重なっている場合、その共通部分(交差)を取得できます。

Range<Integer> r1 = Range.closed(1, 5);
Range<Integer> r2 = Range.closed(3, 10);

Range<Integer> intersected = r1.intersection(r2); // [3..5]

📌 注意: 範囲が重なっていない場合、IllegalArgumentException がスローされます。安全のため、isConnected() で事前に確認しておきましょう。

gap():重なっていない2範囲の間の「すき間」を取得

gap() メソッドは、交差していない2つの Range の間にある空白部分(ギャップ)を返します。

Range<Integer> r1 = Range.closed(1, 3);
Range<Integer> r2 = Range.closed(5, 7);

Range<Integer> gap = r1.gap(r2); // (3..5)

📌 注意: 範囲が重なっている場合はIllegalArgumentException がスローされます。こちらも isConnected() を活用してください。

span():2つの範囲を含む最小の範囲を取得

2つの範囲を「1つにまとめた」最小の範囲(スパン)を取得します。重なっているかどうかに関係なく使えます。

Range<Integer> r1 = Range.closed(1, 5);
Range<Integer> r2 = Range.closed(8, 10);

Range<Integer> spanned = r1.span(r2); // [1..10]

複数の範囲を「包括的にカバーする」範囲を求めたいときに便利です。

canonical()DiscreteDomainでの正規化

特定の DiscreteDomain (離散的な領域) において、その Range が表す値の集合を一意に表現する正規化された形式を指します。

DiscreteDomain<Integer> domain = DiscreteDomain.integers();

Range<Integer> r1 = Range.closedOpen(2, 6);
Range<Integer> c1 = r1.canonical(domain); // [2..6)

Range<Integer> r2 = Range.openClosed(1, 5);
Range<Integer> c2 = r2.canonical(domain); // [2..6)
Range の柔軟性と正規化の必要性

Guavaの Range は、連続した値の区間を表現するための強力なツールです。例えば、整数を扱う場合、Range.closed(1, 5)[1, 5] を表し、Range.closedOpen(1, 6)[1, 5] を表します(つまり、1から5までの整数を含む)。これらは異なる Range オブジェクトですが、表現する整数の集合としては同じです。

このような「同じ集合を表す複数の Range 表現」が存在する場合、比較や集合演算などを行う際に混乱が生じる可能性があります。そこで、特定の DiscreteDomain (例えば、整数全体、日付など) に基づいて、その Range が表す値の集合を唯一の形式に統一するのが canonical() メソッドの役割です。

まとめ:Rangeでスマートな範囲判定を実現しよう

Guavaの Range クラスを活用すれば、値の範囲チェックや範囲同士の関係を簡潔かつ明瞭に記述できます。

開区間・閉区間・無限区間といった表現も直感的で、if 文による複雑な条件分岐を避けられるため、コードの可読性と保守性が大幅に向上します。

さらに、範囲同士の演算や正規化といった高度な機能も備えており、バリデーション、フィルタリング、ビジネスルールの実装など、さまざまな用途で活躍します。

Java標準では得られない柔軟な表現力を、Guavaの Range でぜひ体験してみてください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次