Set / HashSet をざっくりイメージする
まず感覚からいきます。
List は「順番がある・重複を許す」コレクションでした。
それに対して Set は、
- 「順番は気にしない」
- 「同じ要素を二度入れない(重複禁止)」
という性質を持つコレクションです。
Set はインターフェース(抽象的な型)、HashSet はその代表的な実装クラスです。
コードではだいたいこう書きます。
Set<String> set = new HashSet<>();
Java「同じものが入っているかどうかだけ管理したい」
「集合として扱いたい(重複いらない)」
といった場面で、Set / HashSet は非常に頼りになる存在です。
List と Set の違いを最初にハッキリさせる
「重複 OK で順序が大事」なら List
List はこんな性質でした。
- 要素に順番がある(インデックスでアクセスできる)
- 同じ値を何度でも入れてよい
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("apple"); // 重複してもOK
System.out.println(list); // [apple, banana, apple]
Java「3番目の要素は?」など、位置が重要なときは List を使います。
「重複なしで、順番どうでもいい」なら Set
Set(特に HashSet)は逆の性質です。
- 同じ値を二度入れても 1 回しか保持しない(重複禁止)
- 要素の順番は保証されない(インデックスもない)
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // 同じ値を追加
System.out.println(set); // [banana, apple] など(順番はバラバラだが "apple" は1つだけ)
Java「この値が“存在するかどうか”だけ知りたい」
「とにかくユニークな集合がほしい」
といった場面では、Set が List よりずっと自然です。
HashSet の基本操作(add / remove / contains / size)
要素を追加する add(すでにあれば増えない)
Set<String> set = new HashSet<>();
set.add("Alice");
set.add("Bob");
set.add("Alice"); // 同じ要素を追加
System.out.println(set); // [Alice, Bob] (Alice は一つだけ)
System.out.println(set.size()); // 2
Javaadd は「集合にその要素を含める」イメージです。
すでに同じ値が入っている場合、
内部では「何も変わらない」ことが多いです。
add の戻り値は boolean で、
- 新しい要素が追加された →
true - すでに入っていて変化なし →
false
という意味になります。
boolean added1 = set.add("Carol"); // true(新規追加)
boolean added2 = set.add("Alice"); // false(もともとあった)
Java要素を削除する remove
set.remove("Bob"); // Bob を集合から取り除く
System.out.println(set); // [Alice, Carol] など
Java存在しない要素を remove してもエラーにはなりません。
その場合、戻り値が false になります。
「含まれているか?」を調べる contains(これが Set の真骨頂)
Set の一番の用途は「こいつ、入ってる?」の高速判定です。
if (set.contains("Alice")) {
System.out.println("Alice はすでに登録済み");
}
JavaList で同じことをやると、先頭から順に探していくことになり、
要素数が増えるほど遅くなります。
HashSet は内部でハッシュテーブルを使っているので、
多くの場合、要素数が増えても「含まれているかどうか」の判定が非常に速いです。
HashSet の「順番が保証されない」という大事な性質
表示順に意味を期待してはいけない
HashSet は要素の順番を保証しません。
Set<Integer> set = new HashSet<>();
set.add(10);
set.add(20);
set.add(30);
set.add(40);
System.out.println(set); // [20, 40, 10, 30] など(実行ごとに変わりうる)
Javaよくある誤解は、
- 「追加した順に出てくるはず」と思ってしまう
- 「表示された順番に意味がある」と考えてしまう
ことです。
順序に意味がある処理(ランキング、表示順、並び替え)に Set を使うのは向いていません。
順番に意味があるなら List、順番はどうでもよく重複禁止なら Set と考えてください。
もし「重複禁止+追加順を維持したい」ときは、LinkedHashSet という別の実装がありますが、それは一段先の話として覚えておけば十分です。
重複を取り除きたいときに Set が超便利
List から重複を消してユニークな集合を作る
例えば、ユーザーから入力された文字列のリストがあったとします。
List<String> list = Arrays.asList("apple", "banana", "apple", "orange", "banana");
Java重複を取り除いて「一意な値だけ」にしたいなら、
いったん HashSet に突っ込むのが手っ取り早いです。
Set<String> set = new HashSet<>(list);
System.out.println(set); // [banana, orange, apple] など(順番はバラバラ)
Java順番は問わない(とにかくユニークな集合が欲しい)なら、これで終わりです。
もし「重複を消しつつ、元の順番で List に戻したい」という場合は、
- Set ではなく
LinkedHashSetを使う - そのあと
new ArrayList<>(set)で List に戻す
というパターンがよく使われます。
会員 ID のチェック/既存登録チェックなど
例えば、会員 ID がユニークかどうかチェックしたい場合。
Set<String> ids = new HashSet<>();
public boolean register(String id) {
if (ids.contains(id)) {
return false; // すでに存在している
}
ids.add(id);
return true;
}
Javaこれを List でやると、contains が遅くなりやすいです。
Set にしておけば、大量のIDを扱う場合でも快適になります。
equals / hashCode と HashSet の関係(ここは少し深掘り)
HashSet は「hashCode と equals」で重複判定をしている
HashSet が「同じ要素」とみなす条件は、基本的に次の2つです。
hashCode()の結果が同じequals()がtrueを返す
String, Integer など標準クラスは、この2つがうまく実装されています。
Set<String> set = new HashSet<>();
set.add("abc");
set.add(new String("abc"));
System.out.println(set.size()); // 1
Java"abc" と new String("abc") は別インスタンスですが、equals で「中身の文字列が同じ」なので、Set では同じ要素とみなされます。
自作クラスを Set に入れるときの注意
自分で定義したクラスを HashSet に入れる場合、equals と hashCode をちゃんと実装しないと、
「見た目は同じなのに別物として扱われる」ということが起こります。
class User {
String name;
User(String name) { this.name = name; }
}
Set<User> set = new HashSet<>();
set.add(new User("Alice"));
set.add(new User("Alice"));
System.out.println(set.size()); // 2(equals/hashCode を未実装だと別物扱い)
Javaこれを「名前が同じなら同一ユーザーとみなしたい」という仕様にしたいなら、equals と hashCode を上書きしてあげます。
class User {
String name;
User(String name) { this.name = name; }
@Override
public boolean equals(Object o) {
if (!(o instanceof User)) return false;
User other = (User) o;
return Objects.equals(this.name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
Javaこれで、
Set<User> set = new HashSet<>();
set.add(new User("Alice"));
set.add(new User("Alice"));
System.out.println(set.size()); // 1(重複とみなされる)
Javaとなります。
初心者の段階では、
「Set に自作クラスを入れるなら equals/hashCode のことを思い出す」
くらい意識しておけば十分です。
Set を for / 拡張 for で回す(順番に依存しない書き方)
for-each で全要素をなめる
Set はインデックスを持たないので、get(0) のようなアクセスはできません。
基本的なループは「拡張 for(for-each)」が定番です。
Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("C");
for (String s : set) {
System.out.println(s); // A, B, C(順番は保証されない)
}
Javaこのとき、要素が出てくる順序には意味を持たせないようにしてください。
Set を使っている以上、「順番はどうでもいい設計」になっているべきです。
もし「N 番目」という概念が必要になってきたら、その時点で List を検討するサインです。
まとめ:Set / HashSet を頭の中でこう位置づける
初心者向けに Set / HashSet の本質をまとめると、こうなります。
- Set は「重複を許さないコレクション」。List のようにインデックスは持たない。
- HashSet は Set の代表的な実装で、「順番は保証しないが、存在チェックが速い」。
addで要素を入れると、同じ値は1回しか保持されない。containsで「この値が集合に含まれているか」を高速に判定できる。- 順番に意味があるなら List、順番どうでもよく重複禁止なら Set。
- 自作クラスを入れるときは
equals/hashCodeを意識する。
