Set インターフェースの概要
Set<E> インターフェースは、重複要素を許可しないコレクションです。数学的な「集合」の概念を表現しており、要素の追加順序は保持されない場合があります(実装による)。Setは要素の一意性を保証するため、重複チェックや集合演算(和集合、差集合、積集合など)に適しています。
Set の主な特徴
- 一意性: 同じ要素を複数含むことができません
- 順序保証なし: 基本的に要素の追加順序は保証されません(一部の実装を除く)
- Null許容: 多くの実装ではnull要素を1つだけ格納できます
- 集合演算: 和集合、差集合、積集合などの数学的集合操作に適しています
主な Set 実装クラス
HashSet
HashSet はハッシュテーブルに基づく Set インターフェースの実装です。内部的には HashMap を使用しており、要素のハッシュコードに基づいて格納されます。
HashSet の特徴
- 高速な基本操作: add()、remove()、contains() 操作は平均的に O(1) の時間計算量
- 順序保証なし: 要素の追加順序は保持されません
- ハッシュ関数に依存: パフォーマンスは要素のハッシュコードの品質に依存
- Null許容: null要素を1つだけ格納可能
- スレッドセーフではない: 同期化されていないため、複数スレッドでの使用には外部同期が必要
HashSet の基本的な使用例
import java.util.HashSet;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
// HashSet の作成
Set<String> fruits = new HashSet<>();
// 要素の追加
fruits.add("りんご");
fruits.add("バナナ");
fruits.add("オレンジ");
// 重複要素の追加はできない
boolean added = fruits.add("りんご"); // false が返される
// 要素の確認
boolean contains = fruits.contains("バナナ"); // true
// 要素の削除
fruits.remove("オレンジ");
// セットのサイズ取得
int size = fruits.size(); // 2
// セットの走査
for (String fruit : fruits) {
System.out.println(fruit);
// 出力順序は保証されない
}
// セットのクリア
fruits.clear();
boolean isEmpty = fruits.isEmpty(); // true
}
}
LinkedHashSet
LinkedHashSet は HashSet を拡張し、挿入順序を保持する Set インターフェースの実装です。内部的には LinkedHashMap を使用しています。
LinkedHashSet の特徴
- 挿入順序の保持: 要素は追加された順序で走査される
- HashSet と同様のパフォーマンス: 基本操作は O(1) の時間計算量
- わずかに大きいメモリ使用量: 順序情報を保持するための追加のリンクが必要
- 予測可能なイテレーション: 走査順序が一定で、テストやデバッグに適している
LinkedHashSet の基本的な使用例
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetExample {
public static void main(String[] args) {
// LinkedHashSet の作成
Set<String> orderedSet = new LinkedHashSet<>();
// 要素の追加
orderedSet.add("最初");
orderedSet.add("2番目");
orderedSet.add("3番目");
// 順序を保持して走査
for (String element : orderedSet) {
System.out.println(element);
// "最初", "2番目", "3番目" の順で出力される
}
}
}
TreeSet
TreeSet は赤黒木に基づく Set インターフェースの実装です。要素は自然順序(Comparable)またはコンストラクタに提供される Comparator に従ってソートされた順序で保持されます。
TreeSet の特徴
- ソート順序の保証: 要素は常にソートされた状態で保持
- 対数時間の操作: add()、remove()、contains() 操作は O(log n) の時間計算量
- 範囲検索に効率的: first()、last()、floor()、ceiling() などのメソッドが利用可能
- Comparableまたは明示的なComparator必須: 要素は比較可能である必要がある
- null要素の禁止: Java 7以降、TreeSetはnull要素を許可しない
TreeSet の基本的な使用例
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class TreeSetExample {
public static void main(String[] args) {
// 自然順序を使用する TreeSet
Set<Integer> numbers = new TreeSet<>();
numbers.add(5);
numbers.add(2);
numbers.add(8);
numbers.add(1);
// ソートされた順序で走査
for (Integer number : numbers) {
System.out.println(number);
// 1, 2, 5, 8 の順で出力される
}
// カスタム Comparator を使用する TreeSet(降順)
Set<String> descendingSet = new TreeSet<>(Comparator.reverseOrder());
descendingSet.add("りんご");
descendingSet.add("バナナ");
descendingSet.add("オレンジ");
// 降順で走査
for (String fruit : descendingSet) {
System.out.println(fruit);
// "りんご", "バナナ", "オレンジ" の順で出力される(日本語なので実際の順序はロケールに依存)
}
// NavigableSet としての機能利用
TreeSet<Integer> navigableSet = new TreeSet<>(numbers);
// 範囲検索
Integer lower = navigableSet.lower(5); // 5 より小さい最大の要素: 2
Integer floor = navigableSet.floor(5); // 5 以下の最大の要素: 5
Integer ceiling = navigableSet.ceiling(6); // 6 以上の最小の要素: 8
Integer higher = navigableSet.higher(5); // 5 より大きい最小の要素: 8
// サブセット
Set<Integer> subSet = navigableSet.subSet(2, 8); // [2, 5]
Set<Integer> headSet = navigableSet.headSet(5); // [1, 2]
Set<Integer> tailSet = navigableSet.tailSet(5); // [5, 8]
}
}
HashSet、LinkedHashSet、TreeSet の使い分け
特性 | HashSet | LinkedHashSet | TreeSet |
---|---|---|---|
追加・削除・検索 | O(1) - 最も高速 | O(1) - 高速 | O(log n) - 比較的低速 |
順序 | 順序なし | 挿入順序 | ソート順序 |
メモリ使用量 | 低〜中 | 中 | 高い |
Null要素 | 1つ許可 | 1つ許可 | 不許可 |
範囲操作 | サポートなし | サポートなし | サポートあり |
主なユースケース | 高速な重複チェック | 挿入順序が重要な場合 | ソートされた要素が必要な場合 |
選択のガイドライン
- HashSet を選ぶ場合:
- 順序が重要でない場合
- 最高のパフォーマンスが必要な場合
- 単純な重複排除が目的の場合
- LinkedHashSet を選ぶ場合:
- 要素の挿入順序を保持したい場合
- 反復処理の順序が予測可能である必要がある場合
- HashSet 相当のパフォーマンスが必要な場合
- TreeSet を選ぶ場合:
- 要素が常にソートされていることが必要な場合
- 範囲検索(範囲内の要素の取得)が必要な場合
- 最大値/最小値の要素に頻繁にアクセスする場合
集合演算
Set インターフェースを使用すると、数学的な集合演算を簡単に実現できます。
Set の集合演算例
import java.util.HashSet;
import java.util.Set;
public class SetOperationsExample {
public static void main(String[] args) {
Set<Integer> set1 = new HashSet<>();
set1.add(1); set1.add(2); set1.add(3); set1.add(4);
Set<Integer> set2 = new HashSet<>();
set2.add(3); set2.add(4); set2.add(5); set2.add(6);
// 和集合 (Union): set1 と set2 のすべての要素
Set<Integer> union = new HashSet<>(set1);
union.addAll(set2); // [1, 2, 3, 4, 5, 6]
// 積集合 (Intersection): set1 と set2 の共通要素
Set<Integer> intersection = new HashSet<>(set1);
intersection.retainAll(set2); // [3, 4]
// 差集合 (Difference): set1 にあって set2 にない要素
Set<Integer> difference = new HashSet<>(set1);
difference.removeAll(set2); // [1, 2]
// 対称差 (Symmetric Difference): set1 と set2 のいずれか一方にのみ存在する要素
Set<Integer> symmetricDifference = new HashSet<>(set1);
symmetricDifference.addAll(set2); // 和集合を取得
Set<Integer> temp = new HashSet<>(set1);
temp.retainAll(set2); // 積集合を取得
symmetricDifference.removeAll(temp); // 和集合から積集合を削除 [1, 2, 5, 6]
// 部分集合チェック: set1 のすべての要素が set2 に含まれているか
boolean isSubset = set2.containsAll(intersection); // true: intersection は set2 の部分集合
}
}
EnumSet - 列挙型専用の Set 実装
EnumSet は enum 型に特化した高性能な Set 実装です。内部的にはビットベクトルを使用するため、非常に効率的です。
EnumSet の使用例
import java.util.EnumSet;
import java.util.Set;
public class EnumSetExample {
// 曜日を表す列挙型
enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
public static void main(String[] args) {
// 空の EnumSet を作成
Set<Day> weekdays = EnumSet.noneOf(Day.class);
// 要素を追加
weekdays.add(Day.MONDAY);
weekdays.add(Day.TUESDAY);
weekdays.add(Day.WEDNESDAY);
weekdays.add(Day.THURSDAY);
weekdays.add(Day.FRIDAY);
// 一度に複数の要素を持つ EnumSet を作成
Set<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
// 全要素を含む EnumSet を作成
Set<Day> allDays = EnumSet.allOf(Day.class);
// 範囲指定で EnumSet を作成
Set<Day> midWeek = EnumSet.range(Day.TUESDAY, Day.THURSDAY);
// 補集合を作成
Set<Day> complementOfWeekdays = EnumSet.complementOf(weekdays); // weekend と同等
// 集合演算
Set<Day> businessDays = EnumSet.copyOf(weekdays);
businessDays.remove(Day.FRIDAY); // 金曜日を除く平日
}
}
カスタムクラスを Set で使用する際の注意点
カスタムクラスを Set で使用する場合、equals() と hashCode() メソッドを適切にオーバーライドすることが重要です。これらのメソッドは、Set における要素の一意性の判断に使用されます。
カスタムクラスで equals() と hashCode() をオーバーライドする例
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class PersonSetExample {
static class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// equals() をオーバーライド
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
// hashCode() をオーバーライド
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + '}';
}
}
public static void main(String[] args) {
Set<Person> people = new HashSet<>();
// 人物を追加
people.add(new Person("田中", 30));
people.add(new Person("佐藤", 25));
// 同じ内容の Person オブジェクトは重複と見なされる
boolean added = people.add(new Person("田中", 30)); // false が返される
// 異なる内容の Person オブジェクトは追加される
people.add(new Person("田中", 31)); // 年齢が異なるので追加される
System.out.println("Set size: " + people.size()); // 3
for (Person person : people) {
System.out.println(person);
}
}
}
Java 8以降の Stream API との連携
Java 8以降では、Set も Stream API と組み合わせて強力なデータ処理が可能になります。
Set と Stream API の連携例
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public class SetStreamExample {
public static void main(String[] args) {
Set<String> names = new HashSet<>();
names.add("田中太郎");
names.add("鈴木一郎");
names.add("佐藤花子");
names.add("山田次郎");
// フィルタリング: 特定の文字を含む名前だけを抽出
Set<String> filteredNames = names.stream()
.filter(name -> name.contains("田"))
.collect(Collectors.toSet());
// [田中太郎, 山田次郎]
// マッピング: 名前の長さを抽出
Set<Integer> nameLengths = names.stream()
.map(String::length)
.collect(Collectors.toSet());
// [4] (すべての名前が4文字)
// 文字列結合: 名前をカンマ区切りで結合
String nameString = names.stream()
.sorted()
.collect(Collectors.joining(", "));
// "佐藤花子, 山田次郎, 田中太郎, 鈴木一郎" (順序はロケールに依存)
}
}
まとめ
Set インターフェースは、重複を許さないコレクションが必要な場合に非常に役立ちます。主な実装クラスには以下があります:
- HashSet: 最も高速な実装。順序は保証されません。
- LinkedHashSet: 挿入順序を保持しながら、HashSet に近いパフォーマンスを提供。
- TreeSet: 要素を常にソートされた状態で保持。範囲検索に最適。
- EnumSet: 列挙型のセットに特化した高性能な実装。
アプリケーションの要件に応じて適切な Set 実装を選択することで、効率的なデータ処理が可能になります。一般的には、特別な順序要件がない限り HashSet が最適な選択肢です。