Set インターフェース

重複を許さないコレクションとその実装クラスの特性と効率的な使い方

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 が最適な選択肢です。