- JAVA
Java HashSetの使い方|重複削除とTreeSetとの違い
HashSetとは、同じ値を1つしか持たないコレクション(Set)の代表的な実装で、追加・検索・削除が要素数にほぼ左右されない速さで行えるクラスです。 並び順が必要なら LinkedHashSet(追加した順)、ソートされた状態で持ちたいなら TreeSet(小さい順)を選びます。この3つの違いさえ押さえれば、Set で迷う場面はほとんどなくなります。
この記事では、すべてのコードを OpenJDK 21.0.10 で実行し、出力をそのまま掲載しています(JDK 25.0.4.1 でも同じコードを実行して差分を確認しました)。List の重複削除コピペ3種、集合演算、equals/hashCode を書いていないクラスで重複が残る例、Set.of の例外、TreeSet の ClassCastException、Java 21 で加わった SequencedSet、contains の速度比較まで、つまずきやすい箇所を実際のエラー文とともに確認していきます。
Java Setの早見表
変数の型は Set<String> のようにインターフェースで受け、new する実装クラスで性質を決めるのが基本の書き方です。
| やりたいこと | 書き方 | ポイント |
|---|---|---|
| 空の Set を作る | Set<String> set = new HashSet<>(); |
順序は保証されない |
| 追加した順を保つ | new LinkedHashSet<>() |
追加順で取り出せる |
| ソートされた状態で持つ | new TreeSet<>() |
小さい順。null は入らない |
| 追加する | set.add("a") |
新しく入れば true、既にあれば false |
| 含まれるか調べる | set.contains("a") |
HashSet なら要素数によらず速い |
| 削除する | set.remove("a") |
消せたら true |
| 条件で削除する | set.removeIf(s -> s.isEmpty()) |
ループ中の削除はこれを使う |
| 件数 / 空か | set.size() / set.isEmpty() |
|
| List の重複を消す(順序維持) | new ArrayList<>(new LinkedHashSet<>(list)) |
最初に出た順を保つ |
| List の重複を消す(ソート) | new ArrayList<>(new TreeSet<>(list)) |
重複削除と並べ替えを同時に |
| List の重複を消す(Stream) | list.stream().distinct().toList() |
結果は変更できない List |
| 和集合 | a.addAll(b) |
a が書き換わる |
| 積集合(共通部分) | a.retainAll(b) |
a が書き換わる |
| 差集合 | a.removeAll(b) |
a が書き換わる |
| 変更しない Set を作る | Set.of("a", "b") |
重複・null は例外 |
| 先頭・末尾を取る(Java 21) | seq.getFirst() / seq.getLast() |
LinkedHashSet・TreeSet で使える |
| 逆順で見る(Java 21) | seq.reversed() |
コピーではなくビュー |
HashSet・LinkedHashSet・TreeSetの比較表
| 項目 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 取り出す順序 | 保証なし | 追加した順 | ソート順(小さい順) |
| null を入れられるか | 入る(1つだけ) | 入る(1つだけ) | 入らない(NullPointerException) |
| add / contains / remove の計算量 | 平均 O(1) | 平均 O(1) | O(log n) |
| 内部のしくみ | HashMap | LinkedHashMap(ハッシュ表+連結リスト) | TreeMap(赤黒木) |
| 重複の判定に使うもの | hashCode() と equals() |
hashCode() と equals() |
compareTo() または Comparator |
| 要素に必要な条件 | equals/hashCode が正しいこと | equals/hashCode が正しいこと | Comparable か Comparator を渡すこと |
| SequencedSet(Java 21) | 対象外 | 実装している | 実装している |
| 範囲検索(〜以上、〜未満) | できない | できない | headSet / tailSet / floor など |
| 向いている用途 | 存在チェック、重複削除 | 順序を保った重複削除 | ランキング、範囲で取り出す処理 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java HashSetの使い方|重複削除とTreeSetとの違い」
HashSetの基本的な使い方(add・contains・remove)
Set は「同じ値を2回入れても1つしか残らない」入れ物です。add の戻り値が boolean になっていて、「新しく入ったか」を教えてくれるのが List との大きな違いです。
import java.util.HashSet;
import java.util.Set;
public class SetBasic {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
System.out.println(set.add("banana")); // 新しく入った
System.out.println(set.add("apple"));
System.out.println(set.add("banana")); // 既にあるので入らない
System.out.println(set);
System.out.println(set.size());
System.out.println(set.contains("apple"));
System.out.println(set.remove("apple")); // 消せた
System.out.println(set.remove("grape")); // 元々ない
for (String s : set) {
System.out.println("item=" + s);
}
set.clear();
System.out.println(set + " " + set.isEmpty());
}
}
実行結果(java SetBasic.java、OpenJDK 21.0.10):
true
true
false
[banana, apple]
2
true
true
false
item=banana
[] true
重複を入れても例外にはならず、静かに無視されて false が返ります。
なお Set には get(0) のような番号で取り出すメソッドがありません。取り出すときは拡張 for 文か iterator()、Stream を使います。番号で扱いたいときは new ArrayList<>(set) で List に変換します。
HashSet・LinkedHashSet・TreeSetの順序の違いを実行して確認
同じデータを3つの実装に入れて、出力される順序を比べます。
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.TreeSet;
public class SetOrder {
public static void main(String[] args) {
List<String> input = List.of("melon", "apple", "cherry", "banana", "apple", "kiwi");
System.out.println("HashSet : " + new HashSet<>(input));
System.out.println("LinkedHashSet : " + new LinkedHashSet<>(input));
System.out.println("TreeSet : " + new TreeSet<>(input));
List<Integer> nums = List.of(300, 5, 42, 1000, 17, 5);
System.out.println("HashSet : " + new HashSet<>(nums));
System.out.println("LinkedHashSet : " + new LinkedHashSet<>(nums));
System.out.println("TreeSet : " + new TreeSet<>(nums));
}
}
実行結果:
HashSet : [banana, apple, cherry, kiwi, melon]
LinkedHashSet : [melon, apple, cherry, banana, kiwi]
TreeSet : [apple, banana, cherry, kiwi, melon]
HashSet : [17, 5, 1000, 42, 300]
LinkedHashSet : [300, 5, 42, 1000, 17]
TreeSet : [5, 17, 42, 300, 1000]
- HashSet は追加順でもソート順でもない並びになります。これはハッシュ値で格納場所を決めているためで、容量や JDK の版によって変わりうる順序です。「たまたまこの順で出た」ことに頼らないのがポイントです。
- LinkedHashSet は最初に追加された順を保ちます。2回目の
appleは無視され、位置は最初のままです。 - TreeSet は常にソートされた状態で持ちます。String は辞書順、Integer は数値の小さい順です。
nullを入れたときの違い
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
public class SetNull {
public static void main(String[] args) {
Set<String> hs = new HashSet<>();
hs.add(null);
hs.add(null);
System.out.println("HashSet " + hs);
Set<String> lhs = new LinkedHashSet<>();
lhs.add(null);
System.out.println("LinkedHashSet " + lhs);
try {
new TreeSet<String>().add(null);
} catch (Exception e) {
System.out.println("TreeSet " + e);
}
try {
Set.of("a", null);
} catch (Exception e) {
System.out.println("Set.of " + e);
}
try {
System.out.println(Set.of("a").contains(null));
} catch (Exception e) {
System.out.println("Set.of contains(null) " + e);
}
}
}
実行結果:
HashSet [null]
LinkedHashSet [null]
TreeSet java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "k1" is null
Set.of java.lang.NullPointerException
Set.of contains(null) java.lang.NullPointerException: Cannot invoke "Object.equals(Object)" because "o" is null
HashSet と LinkedHashSet は null を1つだけ持てますが、TreeSet と Set.of は null を受け付けません。 TreeSet は大小比較のために compareTo を呼ぶので、null では比較できず例外になります。Set.of で作った Set は contains(null) を呼ぶだけでも NullPointerException になる点も覚えておくと、原因の特定が早くなります。
Setの主要メソッド一覧と実行結果
| メソッド | 戻り値 | 動き |
|---|---|---|
add(e) |
boolean | 追加。新しく入れば true |
addAll(c) |
boolean | まとめて追加(和集合)。変化があれば true |
contains(o) |
boolean | 含まれていれば true |
containsAll(c) |
boolean | c の要素をすべて含めば true(部分集合の判定) |
remove(o) |
boolean | 削除。消せたら true |
removeAll(c) |
boolean | c に含まれる要素を削除(差集合) |
retainAll(c) |
boolean | c に含まれる要素だけ残す(積集合) |
removeIf(条件) |
boolean | 条件に合う要素を削除 |
size() / isEmpty() |
int / boolean | 件数 / 空かどうか |
clear() |
void | 全削除 |
iterator() |
Iterator | 1つずつ取り出す。it.remove() で安全に削除できる |
stream() |
Stream | Stream API で処理する |
toArray(new T[0]) |
T[] | 配列に変換 |
equals(o) |
boolean | 同じ要素を持つ Set なら true(順序・実装クラスは問わない) |
Set.of(...) |
Set | 変更できない Set を作る(Java 9) |
Set.copyOf(c) |
Set | 変更できないコピーを作る(Java 10) |
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class SetMethods {
public static void main(String[] args) {
Set<Integer> a = new TreeSet<>(List.of(1, 2, 3, 4, 5));
System.out.println(a.containsAll(List.of(1, 5)));
System.out.println(a.removeIf(n -> n % 2 == 0) + " " + a);
Integer[] arr = a.toArray(new Integer[0]);
System.out.println(Arrays.toString(arr));
System.out.println(a.stream().mapToInt(Integer::intValue).sum());
System.out.println(a.equals(Set.of(5, 3, 1)));
Iterator<Integer> it = a.iterator();
while (it.hasNext()) {
if (it.next() == 3) {
it.remove();
}
}
System.out.println(a);
}
}
実行結果:
true
true [1, 3, 5]
[1, 3, 5]
9
true
[1, 5]
5行目に注目してください。TreeSet の [1, 3, 5] と Set.of(5, 3, 1) は、実装クラスも順序も違いますが equals は true です。Set 同士の比較は「同じ要素を持っているか」だけで決まります。
Listの重複削除コピペ集(順序維持・ソート・Stream distinct)
「Set を使う理由」でいちばん多いのが List の重複削除です。元の順序を保ちたいかどうかで書き方を選びます。
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
public class Dedupe {
public static void main(String[] args) {
List<String> list = List.of("melon", "apple", "cherry", "banana", "apple", "melon", "kiwi");
// 1. 順序維持版:最初に出てきた順を保つ
List<String> keepOrder = new ArrayList<>(new LinkedHashSet<>(list));
// 2. ソート版:重複削除と並べ替えを同時に行う
List<String> sorted = new ArrayList<>(new TreeSet<>(list));
// 3. Stream distinct 版:順序を保ち、結果は変更できない List
List<String> viaStream = list.stream().distinct().toList();
// 4. 順序を問わない版
List<String> anyOrder = new ArrayList<>(new HashSet<>(list));
System.out.println("order kept : " + keepOrder);
System.out.println("sorted : " + sorted);
System.out.println("distinct : " + viaStream);
System.out.println("HashSet : " + anyOrder);
// 重複があるかどうかだけ知りたい
System.out.println("has dup? : " + (list.size() != new HashSet<>(list).size()));
// 重複している値だけを取り出す(add が false を返す=2回目以降)
Set<String> seen = new HashSet<>();
Set<String> dups = list.stream()
.filter(s -> !seen.add(s))
.collect(Collectors.toCollection(LinkedHashSet::new));
System.out.println("duplicates : " + dups);
// 大文字小文字を区別せずに重複削除
Set<String> ignoreCase = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
ignoreCase.addAll(List.of("Apple", "apple", "APPLE", "Kiwi"));
System.out.println("ignore case: " + ignoreCase);
}
}
実行結果:
order kept : [melon, apple, cherry, banana, kiwi]
sorted : [apple, banana, cherry, kiwi, melon]
distinct : [melon, apple, cherry, banana, kiwi]
HashSet : [banana, apple, cherry, kiwi, melon]
has dup? : true
duplicates : [apple, melon]
ignore case: [Apple, Kiwi]
| 書き方 | 順序 | 結果の List | 選ぶ場面 |
|---|---|---|---|
new ArrayList<>(new LinkedHashSet<>(list)) |
最初に出た順 | 変更できる | 画面表示など、元の並びを保ちたい |
new ArrayList<>(new TreeSet<>(list)) |
ソート順 | 変更できる | 一覧をソートして出したい |
list.stream().distinct().toList() |
最初に出た順 | 変更できない | Stream の処理の途中で重複を除きたい |
new ArrayList<>(new HashSet<>(list)) |
保証なし | 変更できる | 順序が不要で件数が多い |
distinct() も内部では equals / hashCode で重複を判定します。つまり自作クラスの List を重複削除したいときは、次の章の equals/hashCode が前提になります。
集合演算(addAll・retainAll・removeAll)で和・積・差を求める
Set には和集合・積集合・差集合のメソッドがあります。どれも呼び出した側の Set を書き換えるので、元の Set を残したいときは先にコピーを作ります。
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
public class SetOps {
public static void main(String[] args) {
Set<Integer> x = Set.of(1, 2, 3, 4);
Set<Integer> y = Set.of(3, 4, 5, 6);
Set<Integer> union = new TreeSet<>(x); // コピーしてから操作する
union.addAll(y);
Set<Integer> inter = new TreeSet<>(x);
inter.retainAll(y);
Set<Integer> diff = new TreeSet<>(x);
diff.removeAll(y);
Set<Integer> sym = new TreeSet<>(union); // 対称差=和集合−積集合
sym.removeAll(inter);
System.out.println("union : " + union);
System.out.println("intersection : " + inter);
System.out.println("difference : " + diff);
System.out.println("symmetric : " + sym);
System.out.println("subset? : " + union.containsAll(x));
System.out.println("disjoint? : " + Collections.disjoint(x, Set.of(7, 8)));
try {
x.retainAll(y); // Set.of で作った Set は書き換えられない
} catch (Exception e) {
System.out.println(e);
}
}
}
実行結果:
union : [1, 2, 3, 4, 5, 6]
intersection : [3, 4]
difference : [1, 2]
symmetric : [1, 2, 5, 6]
subset? : true
disjoint? : true
java.lang.UnsupportedOperationException
最後の例外は、Set.of で作った Set に retainAll を呼んだためです。集合演算は「変更できる Set のコピー」に対して行う、と覚えておくと安心です。
equals/hashCode未実装のクラスは重複が消えない
自作クラスを HashSet に入れたのに重複が消えない。これは Set で最もよく出会うつまずきです。原因と直し方を実行して確認します。
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class SetEquals {
// equals/hashCode を書いていないクラス
static class ItemNoEq {
final String code;
ItemNoEq(String code) { this.code = code; }
@Override public String toString() { return "Item(" + code + ")"; }
}
// equals/hashCode を書いたクラス
static class ItemEq {
final String code;
ItemEq(String code) { this.code = code; }
@Override public boolean equals(Object o) {
return o instanceof ItemEq other && code.equals(other.code);
}
@Override public int hashCode() { return Objects.hash(code); }
@Override public String toString() { return "Item(" + code + ")"; }
}
// record は equals/hashCode が自動で用意される
record Tag(String name) {}
public static void main(String[] args) {
Set<ItemNoEq> s1 = new HashSet<>();
s1.add(new ItemNoEq("A-1"));
s1.add(new ItemNoEq("A-1"));
System.out.println(s1.size() + " " + s1);
System.out.println(s1.contains(new ItemNoEq("A-1")));
Set<ItemEq> s2 = new HashSet<>();
s2.add(new ItemEq("A-1"));
s2.add(new ItemEq("A-1"));
System.out.println(s2.size() + " " + s2);
System.out.println(s2.contains(new ItemEq("A-1")));
Set<Tag> s3 = new HashSet<>();
s3.add(new Tag("java"));
s3.add(new Tag("java"));
System.out.println(s3.size() + " " + s3);
}
}
実行結果:
2 [Item(A-1), Item(A-1)]
false
1 [Item(A-1)]
true
1 [Tag[name=java]]
equals/hashCode がないクラスは、Object の既定の動き(別々のインスタンスは別物)で比べるため2件残ります。
HashSet はまず hashCode() で入れる場所を決め、同じ場所にある要素と equals() で比べて重複を判定します。 どちらか片方だけでは足りず、2つをセットでオーバーライドする必要があります。値を表すだけのクラスなら、record にするのがいちばん手軽です。オーバーライドの書き方の詳細や IDE での自動生成は、「equals・hashCode のオーバーライド方法」の記事で扱っているので、ここでは Set との関係に絞りました。
入れたあとに中身を変えると見つからなくなる
もう1つ、知っておくと役立つ動きがあります。Set に入れた要素の中身(hashCode に関わる値)をあとから変えると、contains で見つからなくなります。
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SetMutable {
public static void main(String[] args) {
Set<List<String>> set = new HashSet<>();
List<String> key = new ArrayList<>(List.of("a"));
set.add(key);
key.add("b"); // 入れたあとで中身を変える
System.out.println(set.contains(key) + " " + set);
}
}
実行結果:
false [[a, b]]
表示すると入っているのに、contains は false です。追加時の hashCode で決めた場所に置かれたまま、検索は新しい hashCode の場所を見に行くためです。Set の要素には、変更されない値(String、Integer、record、List.of で作った List など)を使うと、この動きを避けられます。
Set.ofの例外:重複でIllegalArgumentException、変更でUnsupportedOperationException
Java 9 で入った Set.of は、変更できない Set を1行で作れる便利なメソッドです。new HashSet<>() とは性質が異なるので、例外を実際に出して確認します。
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class SetOfErrors {
public static void main(String[] args) {
try {
Set.of("a", "b", "a"); // 重複した値を渡す
} catch (Exception e) {
System.out.println(e);
}
Set<String> im = Set.of("a", "b", "c");
try {
im.add("d"); // 変更しようとする
} catch (Exception e) {
System.out.println(e);
}
try {
im.remove("a");
} catch (Exception e) {
System.out.println(e);
}
// Set.copyOf は重複があっても例外にならず、まとめてくれる
System.out.println(Set.copyOf(List.of("a", "b", "a")).size());
// Collections.unmodifiableSet も変更できない
Set<String> um = Collections.unmodifiableSet(new HashSet<>(List.of("a")));
try {
um.add("z");
} catch (Exception e) {
System.out.println(e);
}
// 変更したいときは、変更できる Set にコピーする
Set<String> mutable = new HashSet<>(im);
mutable.add("d");
System.out.println(new TreeSet<>(mutable));
}
}
実行結果:
java.lang.IllegalArgumentException: duplicate element: a
java.lang.UnsupportedOperationException
java.lang.UnsupportedOperationException
2
java.lang.UnsupportedOperationException
[a, b, c, d]
| 作り方 | 重複した値 | null | 変更 | 順序 |
|---|---|---|---|---|
new HashSet<>(List.of(...)) |
1つにまとまる | 入る | できる | 保証なし |
Set.of(...) |
IllegalArgumentException | NullPointerException | UnsupportedOperationException | 保証なし(実行のたびに変わることがある) |
Set.copyOf(c) |
1つにまとまる | NullPointerException | UnsupportedOperationException | 保証なし |
Collections.unmodifiableSet(s) |
元の Set に従う | 元の Set に従う | UnsupportedOperationException | 元の Set に従う |
Set.of は「重複を取り除く道具」ではなく、「重複のない定数の集まりを宣言する道具」です。 重複を含むかもしれないデータには new HashSet<>(list) か Set.copyOf(list) を使います。また Set.of の表示順は実行のたびに変わることがあるので、表示順が必要なら new TreeSet<>(Set.of(...)) のようにソートしてから出力します。
TreeSetにComparableでないクラスを入れるとClassCastException
TreeSet は要素同士を大小比較して並べるので、比較方法がわからないクラスは入れられません。
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class TreeSetDemo {
static class Box {
final int size;
Box(int size) { this.size = size; }
@Override public String toString() { return "Box(" + size + ")"; }
}
public static void main(String[] args) {
// 直す前:Comparable でないクラスをそのまま入れる
try {
Set<Box> boxes = new TreeSet<>();
boxes.add(new Box(1));
} catch (Exception e) {
System.out.println(e.getClass().getName());
System.out.println(e.getMessage().substring(0, 68));
}
// 直した後:Comparator を渡す
Set<Box> boxes = new TreeSet<>(Comparator.comparingInt(b -> b.size));
boxes.add(new Box(3));
boxes.add(new Box(1));
boxes.add(new Box(3));
System.out.println(boxes);
// TreeSet ならではのメソッド
TreeSet<Integer> t = new TreeSet<>(List.of(10, 20, 30, 40, 50));
System.out.println(t.first() + " " + t.last());
System.out.println(t.floor(25) + " " + t.ceiling(25) + " " + t.lower(30) + " " + t.higher(30));
System.out.println(t.headSet(30) + " " + t.tailSet(30) + " " + t.subSet(20, 40));
System.out.println(t.descendingSet());
System.out.println(t.pollFirst() + " " + t);
// 降順で持つ
TreeSet<String> rev = new TreeSet<>(Comparator.reverseOrder());
rev.addAll(List.of("b", "a", "c"));
System.out.println(rev);
// 文字数だけで比べると、同じ長さの要素は重複扱いになる
TreeSet<String> byLen = new TreeSet<>(Comparator.comparingInt(String::length));
byLen.addAll(List.of("kiwi", "plum", "fig"));
System.out.println(byLen);
}
}
実行結果:
java.lang.ClassCastException
class TreeSetDemo$Box cannot be cast to class java.lang.Comparable (
[Box(1), Box(3)]
10 50
20 30 20 40
[10, 20] [30, 40, 50] [20, 30]
[50, 40, 30, 20, 10]
10 [20, 30, 40, 50]
[c, b, a]
[fig, kiwi]
例外メッセージの全文は class TreeSetDemo$Box cannot be cast to class java.lang.Comparable (TreeSetDemo$Box is in unnamed module of loader ...) と続きます。コンパイルは通り、最初の add で実行時に例外になるのが特徴です。直し方は2つあります。
| 直し方 | 書き方 | 向いている場面 |
|---|---|---|
| Comparator を渡す | new TreeSet<>(Comparator.comparingInt(b -> b.size)) |
クラスを変えられない、並び順を場面ごとに変えたい |
| クラスに Comparable を実装する | class Box implements Comparable<Box> |
そのクラスに自然な順序が1つある |
最終行の [fig, kiwi] も大事なポイントです。plum が消えたのは、TreeSet が equals ではなく比較結果(0 かどうか)で重複を判定するためです。「文字数で比べる」Comparator では kiwi と plum が同じ扱いになります。文字数順に並べつつ両方残したいときは、Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()) のように、最後まで区別がつく比較にします。Comparator の組み立て方は「Comparator で複数キーソート」の記事にまとめています。
Java 21のSequencedSet(getFirst・getLast・reversed)
Java 21 で SequencedSet というインターフェースが加わり、順序を持つ Set の先頭・末尾を共通のメソッドで扱えるようになりました。公式の API ドキュメントによると、実装しているのは LinkedHashSet・TreeSet・ConcurrentSkipListSet で、HashSet は対象外です。
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SequencedSet;
import java.util.TreeSet;
public class SeqDemo {
public static void main(String[] args) {
SequencedSet<String> seq = new LinkedHashSet<>(List.of("b", "c", "d"));
System.out.println(seq.getFirst() + " " + seq.getLast());
seq.addFirst("a");
seq.addLast("e");
System.out.println(seq);
seq.addLast("a"); // 既にある要素は末尾へ移動する
System.out.println(seq);
System.out.println(seq.reversed());
System.out.println(seq.removeFirst() + " " + seq.removeLast() + " " + seq);
SequencedSet<Integer> ts = new TreeSet<>(List.of(3, 1, 2));
System.out.println(ts.getFirst() + " " + ts.getLast() + " " + ts.reversed());
try {
ts.addFirst(0); // TreeSet は位置を指定して追加できない
} catch (Exception e) {
System.out.println(e);
}
try {
new LinkedHashSet<String>().getFirst(); // 空の Set
} catch (Exception e) {
System.out.println(e);
}
System.out.println(new HashSet<>(List.of(1)) instanceof SequencedSet);
}
}
実行結果:
b d
[a, b, c, d, e]
[b, c, d, e, a]
[a, e, d, c, b]
b a [c, d, e]
1 3 [3, 2, 1]
java.lang.UnsupportedOperationException
java.util.NoSuchElementException
false
| メソッド | LinkedHashSet | TreeSet | 補足 |
|---|---|---|---|
getFirst() / getLast() |
使える | 使える | 空なら NoSuchElementException |
removeFirst() / removeLast() |
使える | 使える | 取り出して削除 |
addFirst(e) / addLast(e) |
使える(既存なら移動) | UnsupportedOperationException | TreeSet の位置は比較で決まるため |
reversed() |
使える | 使える | 逆順のビュー。元を変えると反映される |
Java 21 以降は、最後の要素も getLast() の1行で取れます。
containsの速度をListと比較(10万件で実測)
「存在チェックは List より Set が速い」と言われる理由を、数値で確認します。10万件のデータに対して contains を1万回呼びます。
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
public class SetBench {
public static void main(String[] args) {
int n = 100_000, q = 10_000;
List<Integer> src = new ArrayList<>();
for (int i = 0; i < n; i++) src.add(i);
Collections.shuffle(src, new Random(1));
List<Integer> al = new ArrayList<>(src);
Set<Integer> hs = new HashSet<>(src), lhs = new LinkedHashSet<>(src), ts = new TreeSet<>(src);
int[] qs = new Random(2).ints(q, 0, n * 2).toArray(); // 半分は存在しない値
System.out.println(System.getProperty("java.version"));
for (int round = 0; round < 3; round++) { // 3周して最後の周を表示
for (var e : List.of(
Map.entry("ArrayList", (Collection<Integer>) al),
Map.entry("HashSet", (Collection<Integer>) hs),
Map.entry("LinkedHashSet", (Collection<Integer>) lhs),
Map.entry("TreeSet", (Collection<Integer>) ts))) {
long t0 = System.nanoTime();
int hit = 0;
for (int v : qs) if (e.getValue().contains(v)) hit++;
long us = (System.nanoTime() - t0) / 1000;
if (round == 2) {
System.out.printf("%-14s contains x %,d : %,10.3f ms (hit=%d)%n",
e.getKey(), q, us / 1000.0, hit);
}
}
}
}
}
実行結果(OpenJDK 21.0.10、Linux コンテナ上での1回の計測値):
21.0.10
ArrayList contains x 10,000 : 1,367.069 ms (hit=4987)
HashSet contains x 10,000 : 2.105 ms (hit=4987)
LinkedHashSet contains x 10,000 : 1.960 ms (hit=4987)
TreeSet contains x 10,000 : 3.000 ms (hit=4987)
ArrayList が約1.4秒かかる処理が、Set では数ミリ秒で終わりました。 ArrayList の contains は先頭から順に比べる O(n) なので、件数に比例して時間が伸びます。HashSet はハッシュ値で場所を直接引く O(1)、TreeSet は木をたどる O(log n) です。
数値は計測のたびに揺れる簡易計測です。読み取ってほしいのは 「List と Set では桁が違う」 という点で、厳密に比べたいときは JMH のような計測用ツールを使います。
ループの中で「含まれるか」を何度も調べるなら Set に入れてから contains、数件を1回調べるだけなら List のままで十分です。List・Set・Map 全体の選び方は「Java List Set Map 使い分け」の記事で扱っているので、ここでは contains に絞りました。
用途別:どのSetを選ぶかの判断表
| やりたいこと | 選ぶ実装 | 理由 |
|---|---|---|
| 存在チェック・重複削除(順序は不要) | HashSet | 最も軽く速い |
| 重複を除きつつ、追加した順で表示したい | LinkedHashSet | 追加順を保つ |
| 常にソートされた状態で持ちたい | TreeSet | add のたびに正しい位置に入る |
| 「〜以上」「〜未満」で取り出したい | TreeSet | headSet / tailSet / floor がある |
| 定数として宣言したい(変更しない) | Set.of(...) |
変更できず、意図が伝わる |
| enum の集まり | EnumSet.of(...) |
enum 専用で省メモリ・高速 |
| 複数スレッドから同時に追加する | ConcurrentHashMap.newKeySet() |
スレッドセーフ |
EnumSet は追加した順ではなく enum の宣言順で並びます(EnumSet.of(Day.WED, Day.MON) の表示は [MON, WED] でした)。
つまずきやすい点と対処のまとめ
| 起きること | 原因 | 対処 |
|---|---|---|
| 自作クラスの重複が消えない | equals/hashCode を書いていない | 両方をオーバーライドする。または record にする |
| 入っているのに contains が false | 追加後に要素の中身を変えた | 変更されない値を要素にする |
| 表示順が毎回違う・環境で違う | HashSet や Set.of の順序に頼っている |
LinkedHashSet か TreeSet を使う |
IllegalArgumentException: duplicate element |
Set.of に重複した値を渡した |
new HashSet<>(list) か Set.copyOf(list) |
UnsupportedOperationException |
Set.of などの変更できない Set を変更した |
new HashSet<>(set) にコピーしてから変更 |
ClassCastException ... Comparable |
TreeSet に比較できないクラスを入れた | Comparator を渡すか Comparable を実装する |
| TreeSet で要素が減った | Comparator が 0 を返す要素は重複扱い | thenComparing で最後まで区別する |
NullPointerException |
TreeSet / Set.of に null |
null を除いてから入れる。null が必要なら HashSet |
ConcurrentModificationException(実行して確認) |
拡張 for 文の中で set.remove(n) を呼んだ |
set.removeIf(n -> n % 2 == 0) か Iterator.remove() |
set.get(0) が書けない |
Set には番号がない | getFirst()(Java 21)、または List に変換 |
Java 25との差分
同じコードを JDK 25.0.4.1 でも実行しました。この記事の出力は Java 21 と Java 25 ですべて一致し、違いは次の2点だけでした。
- ClassCastException のメッセージに含まれるクラスローダー名(Java 21 は
...launcher.Main$MemoryClassLoader、Java 25 は...launcher.MemoryClassLoader)。単一ファイル実行のしくみに由来する部分で、Set の動きとは関係ありません。 - contains の計測値(Java 25 では ArrayList 868.595 ms / HashSet 2.573 ms / LinkedHashSet 1.364 ms / TreeSet 3.227 ms)。1回ずつの簡易計測なので、「どちらの版でも List と Set で桁が違う」と読むのが適切です。
SequencedSet は Java 21 で加わった機能なので、Java 17 以前では getFirst() などは使えません。
よくある質問
Q. HashSet の順序は毎回同じに見えます。順序に頼ってもよいですか? A. 同じデータ・同じ JDK なら同じ順で出ることが多いですが、仕様として保証されていません。容量の変化や JDK の版で変わりうるので、順序が必要なら LinkedHashSet(追加順)か TreeSet(ソート順)を選びます。
Q. Set から1件だけ取り出すにはどうしますか?
A. Java 21 以降で LinkedHashSet / TreeSet なら getFirst() が使えます。HashSet の場合は set.iterator().next() や set.stream().findFirst() で「どれか1件」を取れます。番号で取りたいときは new ArrayList<>(set) で List に変換します。
Q. List の重複削除は distinct() と LinkedHashSet のどちらがよいですか?
A. 結果はどちらも「最初に出た順」で同じです。Stream の処理の途中なら distinct()、List をそのまま変換するだけなら new ArrayList<>(new LinkedHashSet<>(list)) が読みやすいです。toList() の結果は変更できないので、あとで追加・削除するなら後者を選びます。
まとめ
- HashSet は順序なしで最速、LinkedHashSet は追加順、TreeSet はソート順。変数は
Set<T>で受け、実装クラスで性質を選ぶ - List の重複削除は、順序維持なら LinkedHashSet、ソートなら TreeSet、Stream の途中なら
distinct() - 集合演算(addAll / retainAll / removeAll)は呼び出した側を書き換えるので、コピーしてから使う
- 自作クラスは equals と hashCode の両方が必要。record なら自動で用意される
Set.ofは重複で IllegalArgumentException、変更で UnsupportedOperationException、null で NullPointerException- TreeSet は Comparable か Comparator が必要で、比較結果が 0 の要素は重複として扱われる
- Java 21 の SequencedSet で
getFirst()/getLast()/reversed()が使える(LinkedHashSet・TreeSet)。containsを繰り返すなら List より Set が桁違いに速い
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java HashSetの使い方|重複削除とTreeSetとの違い」
株式会社RJCでは、Javaのコレクションの使い分けを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。