TreeSet をざっくりイメージする
まず全体像からいきます。
TreeSet は
「重複を許さない集合(Set)でありつつ、
中身を“常にソートされた順番”で持ってくれるクラス」
です。
前回の HashSet は「順番は一切気にしない代わりに、高速な集合」でした。TreeSet は、
- Set なので「重複なし」
- さらに「常に小さい順/辞書順に整っている」
- その代わりに
HashSetより少し重い
という性質を持っています。
コードではたいていこう書きます。
import java.util.Set;
import java.util.TreeSet;
Set<Integer> set = new TreeSet<>();
Java左側を Set(インターフェース)にしておくのは、常に良い習慣です。
HashSet と TreeSet の違いを直感で押さえる
HashSet:順番はどうでもいいけど、とにかく速く「存在チェック」したい
Set<String> set = new HashSet<>();
set.add("banana");
set.add("apple");
set.add("orange");
System.out.println(set); // [orange, banana, apple] など順番バラバラ
JavaHashSet の特徴は、
- 「順番の概念がない」
containsがとても速い(多くの場合 O(1))
です。
順番に意味がない「単なる集合」として使うなら、HashSet が一番シンプルです。
TreeSet:常に「小さい順/辞書順」で並べて持ってくれる
import java.util.Set;
import java.util.TreeSet;
Set<String> set = new TreeSet<>();
set.add("banana");
set.add("apple");
set.add("orange");
System.out.println(set); // [apple, banana, orange] (常にソート順)
JavaTreeSet の特徴は、
- 要素が常に「ソートされた順」に並んでいる
containsや追加・削除も比較的速い(内部は「木構造」)
です。
「集合として重複なしで持ちたい」かつ「順番も意味がある(ソートしたい)」
というときに TreeSet がかなり気持ちよくハマります。
TreeSet の基本操作(add / remove / contains / size)
要素を追加する add(重複は禁止)
Set<Integer> set = new TreeSet<>();
set.add(30);
set.add(10);
set.add(20);
set.add(10); // 重複
System.out.println(set); // [10, 20, 30]
System.out.println(set.size()); // 3
JavaSet なので、同じ値を何度 add しても、入っているのは一つだけです。
内部的には「二分探索木(赤黒木)」という構造で管理されていて、
追加した時点で「適切な位置」に挿入され、
結果として常に小さい順になっています。
contains で「あるかどうか」を調べる
if (set.contains(20)) {
System.out.println("20 は含まれている");
}
JavaHashSet ほどではありませんが、
ほとんどの用途では十分に速く動きます。
「重複なし・ソート済み・存在チェックも速い」の三拍子というイメージでいて大丈夫です。
remove で削除
set.remove(20);
System.out.println(set); // [10, 30]
Java存在しないものを remove しても例外は出ず、何も起きません(戻り値が false)。
「常にソートされている」ことを活かす周辺メソッド
TreeSet は、SortedSet / NavigableSet というインターフェースも実装しています。
つまり、「順序付きならでは」の便利メソッドが使えます。
ここが HashSet との決定的な違いです。少し深掘りします。
最小値/最大値を簡単に取れる(first / last)
TreeSet<Integer> set = new TreeSet<>();
set.add(10);
set.add(30);
set.add(20);
System.out.println(set.first()); // 10(最小値)
System.out.println(set.last()); // 30(最大値)
Java最小値・最大値がすぐ取れるのは、「常にソート済み」の TreeSet ならではです。
HashSet ではこうはいきません。
ある値より「小さい最大」「大きい最小」を取る(lower / higher)
TreeSet<Integer> set = new TreeSet<>();
set.add(10);
set.add(20);
set.add(30);
set.add(40);
System.out.println(set.lower(25)); // 20(25より小さい中で最大)
System.out.println(set.higher(25)); // 30(25より大きい中で最小)
Java「ある値のすぐ前/すぐ後ろ」を知りたいときに使えます。
例えば、「あるスコアに一番近い、上のランクを知りたい」など、
ランキングや閾値の判定に便利です。
以下/以上の範囲を取る(floor / ceiling)
System.out.println(set.floor(25)); // 20(25以下で最大)
System.out.println(set.ceiling(25)); // 30(25以上で最小)
System.out.println(set.floor(20)); // 20(ちょうどあるのでそれ)
System.out.println(set.ceiling(20)); // 20(同上)
Javalower / higher は「より小さい」「より大きい」だけですが、floor / ceiling は「同じ値を含むかどうか」がポイントです。
この違いは、境界判定や「しきい値」の処理で効いてきます。
部分集合を切り出す(headSet / tailSet / subSet)
TreeSet<Integer> set = new TreeSet<>();
set.add(10);
set.add(20);
set.add(30);
set.add(40);
set.add(50);
System.out.println(set.headSet(30)); // [10, 20] (30より小さい要素)
System.out.println(set.tailSet(30)); // [30, 40, 50] (30以上の要素)
System.out.println(set.subSet(20, 50)); // [20, 30, 40] (20以上50未満)
Java「ある範囲のデータだけ欲しい」というときに、
一発で「範囲ビュー」が取れるのが TreeSet の強みです。
ここまで来ると少し高度ですが、
「TreeSet はソートされているからこそ“範囲操作”が得意なんだ」
という感覚だけ持っておくと、後で効いてきます。
String / 自作クラスを TreeSet に入れるときの「順序」の話
String を入れると辞書順に並ぶ
TreeSet<String> set = new TreeSet<>();
set.add("banana");
set.add("apple");
set.add("orange");
System.out.println(set); // [apple, banana, orange]
JavaString は「自然な順序(自然順序)」として辞書順が定義されているので、
TreeSet に入れるだけで勝手に辞書順に並びます。
自作クラスを入れる場合は「どう並べるか」を決める必要がある
TreeSet は内部で「小さい順」を判断するために、compareTo や Comparator を使います。
自作クラスを TreeSet に入れたいときは、
少なくともどちらかが必要になります。
1. クラス自体に「自然順序」を実装する(Comparable)
class User implements Comparable<User> {
String name;
User(String name) { this.name = name; }
@Override
public int compareTo(User other) {
return this.name.compareTo(other.name); // 名前の辞書順で比較
}
@Override
public String toString() {
return name;
}
}
Javaこのようにしておくと、
TreeSet<User> users = new TreeSet<>();
users.add(new User("Bob"));
users.add(new User("Alice"));
users.add(new User("Carol"));
System.out.println(users); // [Alice, Bob, Carol]
Javaのように、name の辞書順に並んでくれます。
2. TreeSet を作るときに Comparator を渡す
クラス自体に自然順序を持たせたくない場合は、
TreeSet を new するときに「並べ方」を渡せます。
TreeSet<User> users = new TreeSet<>(
(u1, u2) -> u1.name.compareTo(u2.name) // ラムダでComparator
);
Javaこのようにすると、「この TreeSet では name の辞書順で並べる」
というルールになります。
ここで重要なのは、
- TreeSet は「順序が定義できない型」は扱えない
- 自作クラスを入れるときは「何を基準に並べるか」をはっきり決める必要がある
という点です。
「HashSet ではなく TreeSet を選ぶべき場面」と「逆の場面」
TreeSet を選ぶ場面
次のような条件のとき、TreeSet が向いています。
- 重複なしで良い(Set でよい)
- 順番に意味がある(小さい順や辞書順で扱いたい)
- 最小・最大、範囲検索、しきい値の判定など「順序を使う」ロジックがある
例えば:
- ソート済みのユニークなタグ一覧を持ちたい
- スコアの集合を持っておいて、「何点以上」「何点未満」のような判定をしたい
- ユニークなユーザー名を辞書順で列挙したい
こういうときは、HashSet + 別途ソート でもできますが、
「ソートされた集合」として最初から TreeSet にしておくとコードが素直になります。
HashSet を選ぶ場面
逆に、次のようなときは HashSet が向いています。
- 重複なしで良い
- 順番はほんとうにどうでもいい
- とにかく「存在チェック」が高速であってほしい
例えば:
- ログイン中ユーザーIDの集合(いるかいないかだけ重要)
- 使用済みトークンの集合
- 一度見たことがある単語かどうかのチェック
こういう用途で TreeSet を使うと、
「順序機能にコストを払っているのに使っていない」状態になります。
まとめ:TreeSet を自分の中でこう位置づける
初心者向けに TreeSet の本質をまとめると、こうなります。
TreeSet は「重複なし」+「常にソートされた順」を両立した Set。
HashSet との違いは、「順番を持つかどうか」と「そのために木構造を使っているかどうか」。first, last, lower, higher, floor, ceiling, headSet, tailSet, subSet など、順序ベースの便利メソッドがある。
String や Integer など「自然順序」がある型はそのまま使える。自作クラスを入れるなら Comparable 実装や Comparator が必要。
「順番に意味があるユニーク集合」を扱うときに選ぶ。単なる存在チェックだけなら HashSet のほうが素直。
