【Guava】RangeSetで範囲集合をスマートに管理

数値の範囲を扱う処理は、意外と多くの開発シーンで登場します。たとえば、予約済みのIDの範囲を管理したり、使用中のポート番号を追跡したり、特定の時間帯の重複チェックを行ったり――こうした範囲管理を単純なListSetで実装しようとすると、重複検出や隣接範囲の統合などで、煩雑なロジックが必要になります。

こうした煩雑さをスマートに解決してくれるのが、GoogleのGuavaライブラリが提供するRangeSetです。複数の範囲を効率的に扱えるこのデータ構造を使えば、範囲の追加・削除・検索などを直感的に、かつ効率良く実装できます。

この記事では、RangeSetの基本操作からユースケースまでを具体的なコード例とともに解説し、複雑になりがちな範囲管理をどうシンプルにできるかをご紹介します。

目次

RangeSetとは?

RangeSetは、Google Guavaが提供する複数のRange(区間)を一括して管理するためのインターフェースです。特定の値が複数の範囲のどれかに属するか、重複や隣接する範囲をどう統合するかといった処理を、すっきりと実装できるのが特徴です。

代表的な実装クラスはTreeRangeSetで、内部的にはTreeMapを使って範囲を効率よく管理しています。主な特長は以下のとおりです。

  • 重複や隣接範囲の自動統合:例えば [1,5](5,10) を追加すると、重なりを考慮して1つの連続した範囲に統合されます。
  • 特定の値や範囲に対する高速な判定:ある値が登録済みのどの範囲に含まれるかを素早くチェックできます。
  • 柔軟な削除・分割処理:指定した範囲の一部だけを取り除くような操作も可能です。

このように、煩雑になりがちな範囲の集合処理を、安全かつ効率的に扱える便利なデータ構造です。

なお、各Rangeオブジェクトは開区間・閉区間などの境界指定が可能で、より柔軟な条件を定義できます。Range自体の詳細については、以下の記事をご覧ください。

RangeSetの基本的な使い方

ここでは、TreeRangeSet を使った RangeSet の主な操作を紹介します。

add(Range<C>):範囲の追加

新たな範囲を追加します。既存の範囲と重複・隣接している場合、自動で統合されます。

rangeSet.add(Range.closed(1, 5));
rangeSet.add(Range.open(5, 10));

remove(Range<C>):範囲の削除

指定した範囲を削除します。範囲の一部だけが重なっている場合、その部分だけを分割して除外します。

rangeSet.remove(Range.closed(3, 6));

addAll(RangeSet<C>) / addAll(Iterable<Range<C>>):複数範囲の追加

複数の範囲をまとめて追加します。統合処理も自動で行われます。

rangeSet.addAll(List.of(Range.closed(1, 2), Range.closed(3, 5)));

removeAll(RangeSet<C>) / removeAll(Iterable<Range<C>>):複数範囲の削除

複数の範囲をまとめて削除します。

rangeSet.removeAll(List.of(Range.closed(2, 3), Range.closed(4, 5)));

clear():全消去

すべての範囲を削除します。RangeSetが空になります。

rangeSet.clear();

isEmpty():空チェック

RangeSetが空かどうかを判定します。

boolean isEmpty = rangeSet.isEmpty();

contains(C):値の包含判定

指定した値が、いずれかの範囲に含まれていればtrueを返します。

boolean included = rangeSet.contains(9);

rangeContaining(C):属する範囲の取得

指定した値が含まれるRangeを返します。なければnullを返します。

Range<Integer> range = rangeSet.rangeContaining(7);

encloses(Range<C>):範囲の包含判定

指定した値が、現在のRangeSetの中に完全に含まれているかを判定します。

boolean enclosed = rangeSet.encloses(Range.closed(3, 5));

enclosesAll(RangeSet<C>) / enclosesAll(Iterable<Range<C>>):複数範囲の包含判定

複数のRangeすべてがRangeSetに含まれるかを判定します。

boolean allEnclosed = rangeSet.enclosesAll(List.of(Range.closed(2, 3), Range.closed(4, 5)));

intersects(Range<C>):範囲の交差チェック

指定した範囲と RangeSet のいずれかの範囲が交差しているかを判定します。

boolean intersected = rangeSet.intersects(Range.closed(3, 5));

span():全体の範囲を取得

現在登録されているすべての範囲をカバーする、最小の1つの Range を返します。

Range<Integer> span = rangeSet.span();

complement():補集合の取得

RangeSet に含まれていない範囲(補集合)を取得します。空き領域の検出などに便利です。

RangeSet<Integer> complement = rangeSet.complement();

asRanges() / asDescendingSetOfRanges():すべての範囲を取得

現在の RangeSet に含まれるすべての Range を集合として取得できます。asRanges()は昇順、asDescendingSetOfRanges()は降順になります。

Set<Range<Integer>> ascending = rangeSet.asRanges();
Set<Range<Integer>> descending = rangeSet.asDescendingSetOfRanges();

subRangeSet(Range<C>):部分ビューの作成

指定した範囲に含まれる RangeSet の部分ビューを返します。このビューは元のセットと連動しており、変更は反映されます。

RangeSet<Integer> subRangeSet = rangeSet.subRangeSet(Range.closed(3, 7));

ユースケースで学ぶRangeSetの活用

RangeSet は、単なる技術的な構造にとどまらず、日常的な開発課題をスマートに解決してくれます。ここでは代表的な3つのユースケースを紹介します。

1. 使用中ポートの管理

ネットワークアプリケーションなどで「既に使われているポート番号の範囲」を管理する場面は多くあります。RangeSet を使えば、使えるポート番号かどうかを簡単に判定できます。

RangeSet<Integer> usedPorts = TreeRangeSet.create();
usedPorts.add(Range.closed(8000, 8010));
usedPorts.add(Range.closed(8020, 8030));

boolean canUse = !usedPorts.contains(8015);  // trueなら未使用ポート

2. 空き時間スロットの抽出

予約システムやスケジューラーでは、空いている時間帯の算出がよく必要になります。RangeSet に予約済みの時間を登録し、complement() を使うことで、空き時間の自動計算が可能です。

RangeSet<LocalTime> reserved = TreeRangeSet.create();
reserved.add(Range.closed(LocalTime.of(9, 0), LocalTime.of(10, 0)));
reserved.add(Range.closed(LocalTime.of(13, 0), LocalTime.of(14, 0)));

RangeSet<LocalTime> available = reserved.complement();

このようにすることで、「空いている時間」だけを取り出すことができます。

3. ID範囲によるブラックリスト管理

不正利用のあったユーザーIDなどを「範囲」でまとめて管理したい場合、RangeSet を使うと判定が簡単です。大量のIDを1つずつ持たなくても、範囲指定だけでブロックできます。

RangeSet<Long> blacklist = TreeRangeSet.create();
blacklist.add(Range.closed(1000L, 1999L));
blacklist.add(Range.closed(3000L, 3999L));

if (blacklist.contains(1500L)) {
    System.out.println("このIDは使用できません");
}

個別チェックの手間を省き、柔軟にブラックリストを構築できます。

パフォーマンスの特徴

RangeSetの主な操作の時間計算量は以下の通りです。

  • 追加/削除: O(log n)
  • 包含チェック: O(log n)
  • 範囲取得: O(log n)

ここで、nは管理している範囲の数です。大量の範囲を扱う場合でも、効率的に処理できます。

まとめ:複雑な範囲管理もRangeSetでシンプルに

Guavaの RangeSet を使えば、範囲の追加・削除・検索・統合といった処理を、統一されたインターフェースで直感的に記述できます。自前で複雑なロジックを組む必要がなくなるため、コードの可読性と保守性が大幅に向上します。

特に、数値・時間・IDなど、連続性のあるデータを扱う場面では、その効果が顕著です。複雑な境界条件や範囲の重なりも、RangeSet が自動で処理してくれるため、実装ミスのリスクも低減できます。

「範囲」という概念がコード内で頻出する場合は、RangeSet の導入をぜひ検討してみてください。シンプルで洗練された実装が、開発体験を一段と快適にしてくれるはずです。

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