1. はじめに
身近な例から始めましょう。あなたがパーティーを企画していて、ゲストのリストを作成しているとします。招待状を送ったあと、同じ人物がリストに二重(あるいは三重——パーティーが大好き!)に入っていることが判明することがあります。ふつうのリスト(List)を使っていると、こうした重複は起きがちです。しかし、同じゲストを二度と追加できないコレクションがあれば、話はずっと簡単になります。
そこで登場するのがコレクション Set(集合)です。
Set は、重複のない要素だけを保持するコレクションです。すでに存在する要素を追加しようとしても、追加は行われません(誰も傷つきません)。
インターフェース Set: 基本的な性質
Java における Set は、重複のないコレクションの振る舞いを定義するインターフェースです。これはインターフェース Collection を継承しており、追加(add)、削除(remove)、要素の存在確認(contains)、反復といった操作をサポートします。
主な特徴:
- Set には同一の要素を2つ入れることはできません。
- 要素の順序は実装に依存し、任意の順序で保持されることがあります。
- インデックスがありません。リストのように番号で要素にアクセスすることはできません。
宣言の構文
Set<String> guests = new HashSet<>();
2. HashSet: 高速・シンプル・順序なし
HashSet はインターフェース Set の最も一般的な実装です。これはハッシュテーブルに基づいており(HashMap と同様ですが、「キーと値」のペアではなく、ユニークな値だけを持ちます)、最大の利点は追加・削除・検索の高速性です。
HashSet はどう動作する?
箱に物を入れていく場面を想像してください。すでに似たものがあるかを素早く判断するため、各アイテムに「番号」——ハッシュコード——が与えられます。HashSet に要素を追加すると、まずそのハッシュコードが計算されます。見たことのないハッシュなら、要素はそのままコレクションに入ります。同じハッシュがあれば、さらに equals() による等価性の確認が行われます。実際にオブジェクトが同一である場合のみ、新しい要素は追加されません。
つまり HashSet は自動的に一意性を担保します。同一のオブジェクトが2つ存在することはありません。
注意点として、自作クラスのオブジェクトを HashSet に格納したい場合は、equals() と hashCode() をオーバーライドする必要があります。これをしないとコレクションは予測不能な挙動を示すことがあり、見た目には同じオブジェクトが別物として扱われることがあります。
HashSet の主なメソッド
Set<String> guests = new HashSet<>();
guests.add("イワン");
guests.add("マリア");
guests.add("ピョートル");
guests.add("イワン"); // 重複!追加されません。
System.out.println(guests); // [イワン, マリア, ピョートル] — 順序は任意です
guests.remove("ピョートル"); // 要素を削除
System.out.println(guests.contains("マリア")); // true
System.out.println(guests.size()); // 2
コードで試してみましょう
たとえば、同名のタスクが重複しないように、アプリで一意なタスク名を保存したいとします:
import java.util.HashSet;
import java.util.Set;
public class UniqueTasksDemo {
public static void main(String[] args) {
Set<String> tasks = new HashSet<>();
tasks.add("Java の宿題をする");
tasks.add("猫をなでる");
tasks.add("Java の宿題をする"); // 重複!
System.out.println("タスク一覧:");
for (String task : tasks) {
System.out.println("- " + task);
}
// リストには2件だけが残り、重複は追加されません
}
}
3. TreeSet: 順序が大事!
一意性だけでなく、ソート済みの要素集合が必要なときがあります。たとえば、ゲストの名前をランダムではなくアルファベット順で見たい場合です。そのために TreeSet があります。
TreeSet は、要素を(昇順で)ソートされた順序で保持する Set の実装です。内部的には「赤黒木」に基づいています。
TreeSet の使用例
import java.util.Set;
import java.util.TreeSet;
public class SortedGuestsDemo {
public static void main(String[] args) {
Set<String> guests = new TreeSet<>();
guests.add("ウラジーミル");
guests.add("アレクセイ");
guests.add("エカテリーナ");
guests.add("アレクセイ"); // 重複!
System.out.println("ゲスト(アルファベット順):");
for (String guest : guests) {
System.out.println("- " + guest);
}
// 出力例:
// - アレクセイ
// - ウラジーミル
// - エカテリーナ
}
}
注意: 重複を追加しても、集合には現れません。あるべき挙動です!
TreeSet を使うとき
- ソート済みの一意な要素集合が必要なとき。
- 検索を高速にしたいが、追加の速度が最優先ではないとき(HashSet よりわずかに遅い)。
- 要素が自作クラスの場合、それらは「比較可能」(Comparable を実装)であるか、または独自の Comparator を提供する必要があります。
4. 役立つ注意点
HashSet と TreeSet: どちらを選ぶ?
| 基準 | HashSet | TreeSet |
|---|---|---|
| 保持順序 | 保証なし | 昇順にソート |
| 操作の速度 | 高速(O(1)) | やや遅い(O(log n)) |
| 型の要件 | 任意(equals()/hashCode() があれば十分) | Comparable または Comparator |
| 典型的な用途 | 一意な要素へ高速にアクセスしたいとき | 順序付きの出力・走査が重要なとき |
Set を使うときの特徴
- インデックスなし。 List と違い、Set には get(int index) のようなメソッドはありません。インデックスアクセスが必要なら List を使いましょう。
- 重複なし。 既存の要素を追加しようとしても追加されません。add メソッドは false を返します。
- 順序は保証されない(TreeSet を除く)。 HashSet では、要素の順序は実行ごとに異なる場合があります。追加順を保持したい場合は LinkedHashSet を使用しましょう。
- null 値。
- HashSet は null を1つだけ格納できます。
- TreeSet は特別な Comparator を用意しない限り null を追加できません。そうでない場合は NullPointerException になります。
5. Set の典型的な利用タスク
リストから重複を削除
学生のリストがあり、一部が二重に登録されているとします。一意な名前だけを残したい場合:
import java.util.*;
public class RemoveDuplicatesDemo {
public static void main(String[] args) {
List<String> students = Arrays.asList("アンナ", "イーゴリ", "アンナ", "マリア", "イーゴリ", "パベル");
Set<String> uniqueStudents = new HashSet<>(students);
System.out.println("ユニークな学生: " + uniqueStudents);
// 順序は保証されません!
}
}
ソート済みの結果が必要なら、TreeSet を使いましょう:
Set<String> sortedUniqueStudents = new TreeSet<>(students);
System.out.println("ユニークな学生(アルファベット順): " + sortedUniqueStudents);
一意性のチェック(例: ユーザーのログイン名)
Set<String> usedLogins = new HashSet<>();
usedLogins.add("student1");
usedLogins.add("java_lover");
String newLogin = "student1";
if (usedLogins.contains(newLogin)) {
System.out.println("そのログイン名は既に使用されています!");
} else {
System.out.println("ログイン名は利用できます!");
}
集合の要素を反復処理する
反復は for-each ループで行います:
for (String name : uniqueStudents) {
System.out.println(name);
}
6. Set を扱うときのよくあるミス
エラー1: HashSet の要素順序を期待する。 多くの初心者は、集合の要素が「奇妙な」順序で出力されることに驚きます。これは普通のことです。HashSet は順序を保証しません。追加順が必要なら LinkedHashSet、ソートが必要なら TreeSet を使いましょう。
エラー2: インデックスで要素にアクセスしようとする。 たとえば set.get(0) のように書こうとすることがあります。これはできません。Set はインデックスをサポートしていません。インデックスでアクセスしたいなら List を使いましょう。
エラー3: 可変オブジェクトを格納する。 もし、equals()/hashCode() に関与するフィールドを変更可能なオブジェクトを保存している場合、これらのフィールドを変更すると、その要素は集合から「見失われる」ことがあります。要素を不変にするか、識別用フィールドは変更しないようにしましょう。
エラー4: 重複が追加されると期待する。 同じ要素を何度追加しても、集合のサイズは増えません。重複は無視され、add メソッドは false を返します。
エラー5: プリミティブ型を使う。 Set<int> のような記述はコンパイルできません。ラッパークラスを使用しましょう: Set<Integer>、Set<Double> など。
GO TO FULL VERSION