- JAVA
- 技術 💻
Java List Set Map 使い分け|選び方フローと実測つき
JavaのList・Set・Mapの使い分けは、「順番に並べたいならList、重複をなくしたいならSet、キーで値を引きたいならMap」です。 そのうえで実装クラス(ArrayList、HashSet、HashMapなど)を「順序が必要か」「並べ替えが必要か」「複数スレッドから使うか」で選びます。
この記事では、選び方のフローチャートと、実装クラス11種の比較表(順序・重複・null可否・計算量・スレッド安全性)を最初に示し、よく使う操作をコピペできるコードで紹介します。すべてのコードはJava 21.0.10で実行し、出力をそのまま掲載しました。List.ofが変更やnullで例外になる場面、Java 21で追加されたgetFirst/getLast/reversed、ループ中の削除で起きるConcurrentModificationException、そして「10万件のcontains」「先頭への挿入」をこのマシンで実測した結果まで、上位記事にない実例で確認します。
List・Set・Mapの早見表
| 種類 | ひと言で | 順序 | 重複 | 取り出し方 | 迷ったらこれ |
|---|---|---|---|---|---|
| List | 順番つきの並び | 追加した順(番号つき) | できる | get(番号) |
ArrayList |
| Set | 重複のない集まり | 実装による | できない | contains(値) |
HashSet(順序が要るならLinkedHashSet) |
| Map | キーと値の対応表 | 実装による | キーは重複不可 | get(キー) |
HashMap(順序が要るならLinkedHashMap) |
やりたいこと別の逆引きです。
| やりたいこと | 選ぶクラス | 主なメソッド |
|---|---|---|
| 追加した順に並べて、番号で取り出す | ArrayList |
add get set remove |
| 重複を除きたい(順序は不要) | HashSet |
add contains remove |
| 重複を除き、追加した順も保ちたい | LinkedHashSet |
add getFirst reversed |
| 重複を除き、並べ替えた状態で持ちたい | TreeSet |
first last ceiling floor |
| IDやコードで値を引きたい | HashMap |
put get getOrDefault merge |
| キーを追加順に保ちたい | LinkedHashMap |
put firstEntry lastEntry |
| キーを並べ替えて持ちたい・範囲で引きたい | TreeMap |
floorEntry headMap tailMap |
| キーがenum | EnumMap |
put get |
| 複数スレッドから更新する | ConcurrentHashMap |
merge compute putIfAbsent |
| 先頭・末尾から出し入れする(キュー・スタック) | ArrayDeque |
offer poll push pop |
| 変更しない固定のデータ | List.of Set.of Map.of |
― |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java List Set Map 使い分け|選び方フローと実測つき」
選び方のフローチャート
最初の分岐は「キーで値を引くか」、次に「重複を許すか」です。
flowchart TD
A[データをまとめて持ちたい] --> B{キーで値を引きたい?}
B -- はい --> M{キーの順序は?}
M -- 不要 --> M1{複数スレッドで更新?}
M1 -- いいえ --> HM[HashMap]
M1 -- はい --> CHM[ConcurrentHashMap]
M -- 追加した順 --> LHM[LinkedHashMap]
M -- 並べ替えた順・範囲検索 --> TM[TreeMap]
M -- キーがenum --> EM[EnumMap]
B -- いいえ --> C{重複を除きたい?}
C -- はい --> S{要素の順序は?}
S -- 不要 --> HS[HashSet]
S -- 追加した順 --> LHS[LinkedHashSet]
S -- 並べ替えた順 --> TS[TreeSet]
C -- いいえ --> L{主な操作は?}
L -- 番号で取得・末尾に追加 --> AL[ArrayList]
L -- 先頭と末尾から出し入れ --> AD[ArrayDeque]
L -- 変更しない --> LO[List.of]
迷ったらArrayList・HashSet・HashMapから始め、順序が必要になったらLinked〜、並べ替えが必要になったらTree〜に替えると覚えておくと、ほとんどの場面に対応できます。変数の型をList・Set・Map(インターフェース)で宣言しておけば、実装クラスの差し替えは1か所で済みます。
実装クラスの比較表(順序・重複・null・計算量・スレッド安全性)
計算量は各クラスのJavadocの記載に基づきます。nは要素数です。
| クラス | 種類 | 順序 | 重複 | null | 追加 | 検索(contains/get) | 削除 | スレッド安全 |
|---|---|---|---|---|---|---|---|---|
ArrayList |
List | 追加順 | 可 | 可 | 末尾O(1)※償却、先頭・途中O(n) | get(i) O(1)、contains O(n) |
O(n) | いいえ |
LinkedList |
List/Deque | 追加順 | 可 | 可 | 先頭・末尾O(1) | get(i) O(n)、contains O(n) |
先頭・末尾O(1) | いいえ |
HashSet |
Set | なし | 不可 | 1つ可 | O(1)平均 | O(1)平均 | O(1)平均 | いいえ |
LinkedHashSet |
Set | 追加順 | 不可 | 1つ可 | O(1)平均 | O(1)平均 | O(1)平均 | いいえ |
TreeSet |
Set | 並べ替え順 | 不可 | 不可(自然順序の場合) | O(log n) | O(log n) | O(log n) | いいえ |
HashMap |
Map | なし | キー不可 | キー1つ・値可 | O(1)平均 | O(1)平均 | O(1)平均 | いいえ |
LinkedHashMap |
Map | 追加順 | キー不可 | キー1つ・値可 | O(1)平均 | O(1)平均 | O(1)平均 | いいえ |
TreeMap |
Map | キーの並べ替え順 | キー不可 | キー不可(自然順序の場合)・値可 | O(log n) | O(log n) | O(log n) | いいえ |
EnumMap |
Map | enumの定義順 | キー不可 | キー不可・値可 | O(1) | O(1) | O(1) | いいえ |
ConcurrentHashMap |
Map | なし | キー不可 | キー・値とも不可 | O(1)平均 | O(1)平均 | O(1)平均 | はい |
ArrayDeque |
Deque | 追加順 | 可 | 不可 | 先頭・末尾O(1)※償却 | contains O(n) |
先頭・末尾O(1) | いいえ |
「平均O(1)」のハッシュ系は、要素のhashCodeが適切に分散していることが前提です。後半でequals/hashCodeを実装しない場合の挙動を確認します。
Listの基本操作(ArrayList)
Listは「番号(インデックス)つきの並び」で、同じ値を何度でも入れられます。
import java.util.*;
public class ListBasics {
public static void main(String[] args) {
List<String> tasks = new ArrayList<>();
tasks.add("design"); tasks.add("build"); tasks.add("test"); tasks.add("build");
tasks.add(1, "review"); // 1番目に挿入
System.out.println(tasks + " size=" + tasks.size());
System.out.println("get(0)=" + tasks.get(0) + " indexOf(build)=" + tasks.indexOf("build")
+ " contains(test)=" + tasks.contains("test"));
tasks.set(0, "plan"); // 置き換え
tasks.remove("build"); // 最初に見つかった1つを削除
System.out.println("after set/remove: " + tasks);
}
}
[design, review, build, test, build] size=5
get(0)=design indexOf(build)=2 contains(test)=true
after set/remove: [plan, review, test, build]
remove(Object)は最初に見つかった1つだけを削除します。すべて削除したいときは後述のremoveIfを使います。
Setの基本操作(HashSet・LinkedHashSet・TreeSet)
Setは重複を持たない集まりで、実装によって取り出す順序が違います。 同じ4つの文字列を入れて比べます。
import java.util.*;
public class SetBasics {
public static void main(String[] args) {
Set<String> hash = new HashSet<>(List.of("pear", "apple", "banana", "cherry"));
Set<String> linked = new LinkedHashSet<>(List.of("pear", "apple", "banana", "cherry"));
Set<String> tree = new TreeSet<>(List.of("pear", "apple", "banana", "cherry"));
System.out.println("HashSet " + hash);
System.out.println("LinkedHashSet " + linked);
System.out.println("TreeSet " + tree);
System.out.println("add duplicate apple -> " + linked.add("apple") + " " + linked);
}
}
HashSet [banana, apple, cherry, pear]
LinkedHashSet [pear, apple, banana, cherry]
TreeSet [apple, banana, cherry, pear]
add duplicate apple -> false [pear, apple, banana, cherry]
HashSetの順序は保証されません(上の順序はこの実行でたまたまそうなったものです)。画面表示やファイル出力で順序を安定させたいときはLinkedHashSet(追加順)かTreeSet(並べ替え順)を選びます。addは追加できたかをbooleanで返すので、重複チェックと追加を1行で書けます。
Mapの基本操作(HashMap・TreeMap)
Mapはキーと値の対応表で、同じキーにputすると値が上書きされます。
import java.util.*;
public class MapBasics {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("pen", 10); stock.put("note", 5); stock.put("pen", 12); // penは上書き
System.out.println(stock + " get(pen)=" + stock.get("pen") + " get(eraser)=" + stock.get("eraser"));
System.out.println("getOrDefault(eraser,0)=" + stock.getOrDefault("eraser", 0));
System.out.println("containsKey(note)=" + stock.containsKey("note"));
Map<String, Integer> sortedStock = new TreeMap<>(stock);
System.out.println("TreeMap " + sortedStock);
for (Map.Entry<String, Integer> e : sortedStock.entrySet()) {
System.out.println(" " + e.getKey() + "=" + e.getValue());
}
}
}
{note=5, pen=12} get(pen)=12 get(eraser)=null
getOrDefault(eraser,0)=0
containsKey(note)=true
TreeMap {note=5, pen=12}
note=5
pen=12
存在しないキーのgetはnullです。そのままintに代入するとNullPointerExceptionになるため、既定値が決まっているならgetOrDefaultを使います。キーと値の両方を使うループはentrySet()が基本です。
merge・computeIfAbsent・putIfAbsentで集計を短く書く
「件数を数える」「グループごとにリストを作る」は、Mapの便利メソッドで1行にできます。
import java.util.*;
public class MapAggregate {
public static void main(String[] args) {
// 件数を数える
List<String> words = List.of("pen", "note", "pen", "clip", "pen", "note");
Map<String, Integer> count = new TreeMap<>();
for (String w : words) count.merge(w, 1, Integer::sum);
System.out.println("count=" + count);
// カテゴリごとにリストへ振り分ける
Map<String, List<String>> byCategory = new TreeMap<>();
String[][] items = {{"stationery", "pen"}, {"furniture", "desk"}, {"stationery", "note"}};
for (String[] it : items) byCategory.computeIfAbsent(it[0], k -> new ArrayList<>()).add(it[1]);
System.out.println("byCategory=" + byCategory);
// なければ入れる
Map<String, Integer> m = new HashMap<>();
System.out.println("putIfAbsent first=" + m.putIfAbsent("a", 1)
+ " second=" + m.putIfAbsent("a", 2) + " map=" + m);
}
}
count={clip=1, note=2, pen=3}
byCategory={furniture=[desk], stationery=[pen, note]}
putIfAbsent first=null second=1 map={a=1}
| メソッド | 動き | 向いている処理 |
|---|---|---|
getOrDefault(k, d) |
なければ既定値を返す(Mapは変えない) | 参照だけしたい |
putIfAbsent(k, v) |
なければ入れる。戻り値は以前の値 | 初期値を入れる |
computeIfAbsent(k, f) |
なければfで作って入れ、その値を返す |
グループ化(値がList) |
merge(k, v, f) |
なければv、あればf(旧値, v) |
件数・合計の集計 |
if (map.containsKey(k)) {...} else {...}の分岐は、ほとんどこの4つで置き換えられます。
List.of・Set.of・Map.ofは変更できない(不変コレクション)
List.ofなどで作ったコレクションは変更できず、nullも入れられません。 実行して例外を確認します。
import java.util.*;
public class ImmutableDemo {
static void tryRun(String label, Runnable r) {
try { r.run(); System.out.println(label + " -> OK"); }
catch (Exception e) {
System.out.println(label + " -> " + e.getClass().getName()
+ (e.getMessage() == null ? "" : ": " + e.getMessage()));
}
}
public static void main(String[] args) {
List<String> fixed = List.of("a", "b");
tryRun("List.of(...).add", () -> fixed.add("c"));
tryRun("List.of(\"a\", null)", () -> List.of("a", null));
tryRun("List.of(...).contains(null)", () -> fixed.contains(null));
tryRun("Set.of(\"a\", \"a\")", () -> Set.of("a", "a"));
tryRun("Map.of(\"k\",1,\"k\",2)", () -> Map.of("k", 1, "k", 2));
List<String> copy = new ArrayList<>(fixed); // 変更したいならコピーする
copy.add("c");
System.out.println("new ArrayList<>(List.of(...)) then add -> " + copy);
Map<String, Integer> many = Map.ofEntries(Map.entry("pen", 1), Map.entry("note", 2));
System.out.println("Map.ofEntries get(note)=" + many.get("note"));
List<String> tasks = new ArrayList<>(List.of("x"));
tryRun("Stream.toList().add", () -> tasks.stream().toList().add("x"));
List<String> src = new ArrayList<>(List.of("x"));
List<String> view = Collections.unmodifiableList(src); // 読み取り専用の「窓」
List<String> snap = List.copyOf(src); // その時点のコピー
src.add("y");
System.out.println("unmodifiableList view=" + view + " List.copyOf=" + snap);
}
}
List.of(...).add -> java.lang.UnsupportedOperationException
List.of("a", null) -> java.lang.NullPointerException
List.of(...).contains(null) -> java.lang.NullPointerException
Set.of("a", "a") -> java.lang.IllegalArgumentException: duplicate element: a
Map.of("k",1,"k",2) -> java.lang.IllegalArgumentException: duplicate key: k
new ArrayList<>(List.of(...)) then add -> [a, b, c]
Map.ofEntries get(note)=2
Stream.toList().add -> java.lang.UnsupportedOperationException
unmodifiableList view=[x, y] List.copyOf=[x]
意外に見落としやすいのはList.of(...).contains(null)もNullPointerExceptionになる点です。外部から受け取った値でcontainsするときは、nullを先に除いておきます。
| 作り方 | 変更 | null | 元データの変更の影響 |
|---|---|---|---|
List.of / Set.of / Map.of |
不可 | 不可 | ― |
List.copyOf(src) |
不可 | 不可 | 受けない(コピー) |
Collections.unmodifiableList(src) |
不可 | 元に従う | 受ける(読み取り専用の窓) |
stream().toList()(Java 16以降) |
不可 | 可 | ― |
new ArrayList<>(...) |
可 | 可 | 受けない |
Map.ofは10組までなので、それより多いときはMap.ofEntries(Map.entry(...), ...)を使います。Set.ofやMap.ofの反復順序は実行ごとに変わることがあるため、順序に依存しないようにします。
SequencedCollectionで先頭・末尾を扱う(Java 21)
Java 21でSequencedCollection・SequencedMapが追加され、List・LinkedHashSet・LinkedHashMapなどで先頭と末尾を同じ書き方で扱えるようになりました。
import java.util.*;
public class SequencedDemo {
public static void main(String[] args) {
List<String> seq = new ArrayList<>(List.of("first", "middle", "last"));
System.out.println("getFirst=" + seq.getFirst() + " getLast=" + seq.getLast()
+ " reversed=" + seq.reversed());
seq.addFirst("zero"); seq.addLast("end");
System.out.println("after addFirst/addLast: " + seq);
LinkedHashSet<String> lhs = new LinkedHashSet<>(List.of("b", "a", "c"));
System.out.println("LinkedHashSet getFirst=" + lhs.getFirst() + " reversed=" + lhs.reversed());
LinkedHashMap<String, Integer> lhm = new LinkedHashMap<>();
lhm.put("one", 1); lhm.put("two", 2); lhm.put("three", 3);
System.out.println("LinkedHashMap firstEntry=" + lhm.firstEntry() + " lastEntry=" + lhm.lastEntry()
+ " reversed=" + lhm.reversed());
lhm.putFirst("zero", 0);
System.out.println("after putFirst: " + lhm);
try { List.of().getFirst(); }
catch (Exception e) { System.out.println("List.of().getFirst() -> " + e.getClass().getName()); }
}
}
getFirst=first getLast=last reversed=[last, middle, first]
after addFirst/addLast: [zero, first, middle, last, end]
LinkedHashSet getFirst=b reversed=[c, a, b]
LinkedHashMap firstEntry=one=1 lastEntry=three=3 reversed={three=3, two=2, one=1}
after putFirst: {zero=0, one=1, two=2, three=3}
List.of().getFirst() -> java.util.NoSuchElementException
| 以前の書き方 | Java 21以降 |
|---|---|
list.get(0) |
list.getFirst() |
list.get(list.size() - 1) |
list.getLast() |
Collections.reverse(copy)でコピーを反転 |
list.reversed()(逆順の「窓」を返す) |
linkedHashSet.iterator().next() |
linkedHashSet.getFirst() |
linkedHashMap.entrySet().iterator().next() |
linkedHashMap.firstEntry() |
空のコレクションにgetFirst()を呼ぶとNoSuchElementExceptionになります(get(0)はIndexOutOfBoundsException)。空の可能性があるならisEmpty()で先に確認します。
ループ中の削除はremoveIfで(ConcurrentModificationException)
拡張for文で回しながらlist.removeするとConcurrentModificationExceptionになります。 条件に合う要素を消すならremoveIfが簡潔です。
import java.util.*;
public class RemoveDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("pen", "note", "pen", "clip"));
try {
for (String s : list) if (s.equals("note")) list.remove(s); // 直す前
} catch (Exception e) {
System.out.println("for-each + remove -> " + e.getClass().getName());
}
List<String> list2 = new ArrayList<>(List.of("pen", "note", "pen", "clip"));
list2.removeIf(s -> s.equals("pen")); // 直した後
System.out.println("removeIf -> " + list2);
List<String> list3 = new ArrayList<>(List.of("pen", "note", "pen", "clip"));
for (Iterator<String> it = list3.iterator(); it.hasNext();) {
if (it.next().equals("pen")) it.remove(); // Iteratorで消す方法
}
System.out.println("Iterator.remove -> " + list3);
Map<String, Integer> stock = new HashMap<>(Map.of("pen", 0, "note", 3, "clip", 0));
stock.entrySet().removeIf(e -> e.getValue() == 0); // Mapも同様
System.out.println("map entrySet().removeIf -> " + stock);
}
}
for-each + remove -> java.util.ConcurrentModificationException
removeIf -> [note, clip]
Iterator.remove -> [note, clip]
map entrySet().removeIf -> {note=3}
拡張for文は内部でIteratorを使っており、Iteratorを通さずに構造を変えると次のnext()で変更を検知して例外になります。Mapで値が0の要素を消すならentrySet().removeIf、ループ中に追加もしたいなら新しいリストに集めてから差し替えます。
equals/hashCodeを実装しないとSetで重複する
HashSetやHashMapのキーはequalsとhashCodeで「同じ」を判定します。 実装していないクラスは、中身が同じでも別物として扱われます。
import java.util.*;
public class EqualsDemo {
static class Member { // equals/hashCodeなし
final int id; final String name;
Member(int id, String name) { this.id = id; this.name = name; }
}
static class MemberEq { // idで同一とみなす
final int id; final String name;
MemberEq(int id, String name) { this.id = id; this.name = name; }
@Override public boolean equals(Object o) { return o instanceof MemberEq m && m.id == id; }
@Override public int hashCode() { return Integer.hashCode(id); }
}
record Item(String code, String name) {} // recordは自動で実装される
public static void main(String[] args) {
Set<Member> s1 = new HashSet<>();
s1.add(new Member(1, "Sato")); s1.add(new Member(1, "Sato"));
System.out.println("without equals/hashCode size=" + s1.size());
Set<MemberEq> s2 = new HashSet<>();
s2.add(new MemberEq(1, "Sato")); s2.add(new MemberEq(1, "Sato"));
System.out.println("with equals/hashCode size=" + s2.size());
Set<Item> s3 = new HashSet<>();
s3.add(new Item("A1", "pen")); s3.add(new Item("A1", "pen"));
System.out.println("record size=" + s3.size() + " contains=" + s3.contains(new Item("A1", "pen")));
}
}
without equals/hashCode size=2
with equals/hashCode size=1
record size=1 contains=true
Setの要素やMapのキーにする独自クラスは、equalsとhashCodeを必ず対で実装します。recordなら全項目で比較するequals/hashCodeが自動で作られるので、値をまとめるだけのクラスには手軽です。TreeSet・TreeMapはequalsではなくcompareTo(またはComparator)で同一性を判定する点も押さえておきます。
Arrays.asListとremoveの落とし穴
Arrays.asListは配列を「固定長のList」として見せるもので、追加や削除はできません。
import java.util.*;
public class AsListDemo {
public static void main(String[] args) {
String[] arr = {"a", "b", "c"};
List<String> asList = Arrays.asList(arr);
try { asList.add("d"); }
catch (Exception e) { System.out.println("Arrays.asList(...).add -> " + e.getClass().getName()); }
asList.set(0, "Z");
System.out.println("after asList.set(0,Z): array[0]=" + arr[0]); // 元の配列も変わる
int[] nums = {1, 2, 3};
System.out.println("Arrays.asList(int[]).size()=" + Arrays.asList(nums).size());
System.out.println("Arrays.stream(int[]).boxed().toList()=" + Arrays.stream(nums).boxed().toList());
List<Integer> ids = new ArrayList<>(List.of(10, 20, 1, 30));
ids.remove(1); // 番号1(2番目)を削除
System.out.println("remove(1) -> " + ids);
ids.remove(Integer.valueOf(1)); // 値1を削除
System.out.println("remove(Integer.valueOf(1)) -> " + ids);
}
}
Arrays.asList(...).add -> java.lang.UnsupportedOperationException
after asList.set(0,Z): array[0]=Z
Arrays.asList(int[]).size()=1
Arrays.stream(int[]).boxed().toList()=[1, 2, 3]
remove(1) -> [10, 1, 30]
remove(Integer.valueOf(1)) -> [10, 30]
| つまずき | 起きること | こうするとよい |
|---|---|---|
Arrays.asList(...).add |
UnsupportedOperationException |
new ArrayList<>(Arrays.asList(...)) |
asList.set |
元の配列も書き換わる | 独立させたいならコピーする |
Arrays.asList(int[]) |
要素1つ(配列そのもの)のListになる | Arrays.stream(nums).boxed().toList() |
List<Integer>のremove(1) |
番号1が消える | 値を消すならremove(Integer.valueOf(1)) |
nullを入れられるか(クラスごとの違い)
nullの扱いはクラスによって違います。 実行結果です。
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class NullDemo {
static void tryRun(String label, Runnable r) {
try { r.run(); System.out.println(label + " -> OK"); }
catch (Exception e) {
System.out.println(label + " -> " + e.getClass().getName()
+ (e.getMessage() == null ? "" : ": " + e.getMessage()));
}
}
public static void main(String[] args) {
tryRun("HashMap.put(null, 1)", () -> new HashMap<String, Integer>().put(null, 1));
tryRun("TreeMap.put(null, 1)", () -> new TreeMap<String, Integer>().put(null, 1));
tryRun("ConcurrentHashMap.put(\"k\", null)", () -> new ConcurrentHashMap<String, Integer>().put("k", null));
tryRun("HashSet.add(null)", () -> new HashSet<String>().add(null));
tryRun("TreeSet.add(null)", () -> new TreeSet<String>().add(null));
tryRun("ArrayDeque.add(null)", () -> new ArrayDeque<String>().add(null));
}
}
HashMap.put(null, 1) -> OK
TreeMap.put(null, 1) -> java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "k1" is null
ConcurrentHashMap.put("k", null) -> java.lang.NullPointerException
HashSet.add(null) -> OK
TreeSet.add(null) -> java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "k1" is null
ArrayDeque.add(null) -> java.lang.NullPointerException
ConcurrentHashMapがnullを受け付けないのは、getの結果がnullのとき「キーがない」と一意に判断できるようにするためです。HashMapでもgetのnullが「キーがない」のか「値がnull」なのか区別しにくいので、値にnullを入れない設計にしておくと扱いやすくなります。
EnumMap・ArrayDeque・TreeMapの便利な使い方
import java.util.*;
public class MoreDemo {
enum Status { TODO, DOING, DONE }
public static void main(String[] args) {
EnumMap<Status, Integer> byStatus = new EnumMap<>(Status.class);
byStatus.put(Status.DONE, 4); byStatus.put(Status.TODO, 7); byStatus.put(Status.DOING, 2);
System.out.println("EnumMap " + byStatus); // enumの定義順
Deque<String> stack = new ArrayDeque<>(); // スタック(後入れ先出し)
stack.push("a"); stack.push("b"); stack.push("c");
System.out.println("stack pop=" + stack.pop() + " rest=" + stack);
Deque<String> queue = new ArrayDeque<>(); // キュー(先入れ先出し)
queue.offer("a"); queue.offer("b"); queue.offer("c");
System.out.println("queue poll=" + queue.poll() + " rest=" + queue);
TreeMap<Integer, String> rank = new TreeMap<>(Map.of(0, "C", 60, "B", 80, "A"));
System.out.println("score 75 -> floorEntry=" + rank.floorEntry(75)
+ " headMap(60)=" + rank.headMap(60) + " tailMap(60)=" + rank.tailMap(60));
}
}
EnumMap {TODO=7, DOING=2, DONE=4}
stack pop=c rest=[b, a]
queue poll=a rest=[b, c]
score 75 -> floorEntry=60=B headMap(60)={0=C} tailMap(60)={60=B, 80=A}
EnumMapはenumの定義順で並び、内部は配列なので高速です。ステータス別の件数集計に向いています。- スタックやキューには
ArrayDequeが使えます(StackクラスよりDequeの利用がJavadocで推奨されています)。 TreeMap.floorEntryは「指定値以下で最大のキー」を返すので、点数から評価ランクを引くような「区間の検索」が1行で書けます。
性能を実測する(contains・先頭挿入・get)
計算量の違いが実際どれくらいの差になるかを、10万件で測りました。 各処理を2回の予熱のあと5回計測した中央値です。
| 項目 | 内容 |
|---|---|
| Java | 21.0.10(java CollectionsBench.javaで単一ファイル実行) |
| マシン | この記事の検証用コンテナ(Intel Xeon 2.10GHz、2コア、メモリ7GB) |
| 注意 | 値は環境によって変わります。クラス間の桁の違いを見る目安としてご覧ください |
import java.util.*;
import java.util.function.Supplier;
public class CollectionsBench {
static final int N = 100_000;
static long sink;
static double medianMs(int rounds, Supplier<Long> task) {
for (int i = 0; i < 2; i++) sink += task.get(); // 予熱
double[] t = new double[rounds];
for (int i = 0; i < rounds; i++) {
long s = System.nanoTime();
sink += task.get();
t[i] = (System.nanoTime() - s) / 1_000_000.0;
}
Arrays.sort(t);
return t[rounds / 2];
}
public static void main(String[] args) {
System.out.println("Java " + System.getProperty("java.version") + ", N=" + N);
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < N; i++) arrayList.add(i);
Set<Integer> hashSet = new HashSet<>(arrayList);
Set<Integer> treeSet = new TreeSet<>(arrayList);
Random r = new Random(42);
int[] keys = new int[10_000];
for (int i = 0; i < keys.length; i++) keys[i] = r.nextInt(N * 2); // 約半分が見つかる
System.out.printf("contains x10,000 ArrayList : %.2f ms%n", medianMs(5, () -> { long c = 0; for (int k : keys) if (arrayList.contains(k)) c++; return c; }));
System.out.printf("contains x10,000 HashSet : %.2f ms%n", medianMs(5, () -> { long c = 0; for (int k : keys) if (hashSet.contains(k)) c++; return c; }));
System.out.printf("contains x10,000 TreeSet : %.2f ms%n", medianMs(5, () -> { long c = 0; for (int k : keys) if (treeSet.contains(k)) c++; return c; }));
System.out.printf("add at head x%d ArrayList.add(0,x) : %.2f ms%n", N, medianMs(5, () -> { List<Integer> l = new ArrayList<>(); for (int i = 0; i < N; i++) l.add(0, i); return (long) l.size(); }));
System.out.printf("add at head x%d LinkedList.addFirst: %.2f ms%n", N, medianMs(5, () -> { LinkedList<Integer> l = new LinkedList<>(); for (int i = 0; i < N; i++) l.addFirst(i); return (long) l.size(); }));
System.out.printf("add at head x%d ArrayDeque.addFirst: %.2f ms%n", N, medianMs(5, () -> { ArrayDeque<Integer> d = new ArrayDeque<>(); for (int i = 0; i < N; i++) d.addFirst(i); return (long) d.size(); }));
System.out.printf("add at tail x%d ArrayList.add : %.2f ms%n", N, medianMs(5, () -> { List<Integer> l = new ArrayList<>(); for (int i = 0; i < N; i++) l.add(i); return (long) l.size(); }));
System.out.printf("add at tail x%d LinkedList.add : %.2f ms%n", N, medianMs(5, () -> { List<Integer> l = new LinkedList<>(); for (int i = 0; i < N; i++) l.add(i); return (long) l.size(); }));
List<Integer> linkedList = new LinkedList<>(arrayList);
int[] idx = new int[10_000];
for (int i = 0; i < idx.length; i++) idx[i] = r.nextInt(N);
System.out.printf("get(i) x10,000 ArrayList : %.2f ms%n", medianMs(5, () -> { long c = 0; for (int i : idx) c += arrayList.get(i); return c; }));
System.out.printf("get(i) x10,000 LinkedList: %.2f ms%n", medianMs(5, () -> { long c = 0; for (int i : idx) c += linkedList.get(i); return c; }));
}
}
Java 21.0.10, N=100000
contains x10,000 ArrayList : 565.63 ms
contains x10,000 HashSet : 1.16 ms
contains x10,000 TreeSet : 2.62 ms
add at head x100000 ArrayList.add(0,x) : 403.91 ms
add at head x100000 LinkedList.addFirst: 1.44 ms
add at head x100000 ArrayDeque.addFirst: 1.33 ms
add at tail x100000 ArrayList.add : 1.63 ms
add at tail x100000 LinkedList.add : 0.93 ms
get(i) x10,000 ArrayList : 0.57 ms
get(i) x10,000 LinkedList: 434.35 ms
| 操作(このマシンでの中央値) | 速い側 | 遅い側 | 差 |
|---|---|---|---|
10万件からcontainsを1万回 |
HashSet 1.16 ms |
ArrayList 565.63 ms |
約490倍 |
| 先頭への挿入を10万回 | ArrayDeque 1.33 ms |
ArrayList.add(0, x) 403.91 ms |
約300倍 |
10万件からget(i)を1万回 |
ArrayList 0.57 ms |
LinkedList 434.35 ms |
約760倍 |
| 末尾への追加を10万回 | どちらも1〜2 ms程度 | ― | 差は小さい |
「存在チェックが多いならSet」「先頭から出し入れするならArrayDeque」「番号で取り出すならArrayList」という比較表の計算量が、そのまま数百倍の差として表れました。LinkedListは先頭挿入は速いもののget(i)が遅いため、キューやスタックの用途ならArrayDequeを選ぶとどちらの操作も速く動きます。
よくある失敗と直し方のチェックリスト
| よくある書き方 | 起きること | こうするとよい |
|---|---|---|
大きなListでcontainsを繰り返す |
件数に比例して遅くなる | HashSetに入れてからcontains |
ArrayListの先頭に挿入を繰り返す |
毎回全要素がずれる | ArrayDeque.addFirst |
LinkedListをget(i)で回す |
毎回先頭からたどる | ArrayListか拡張for文 |
拡張for文の中でremove |
ConcurrentModificationException |
removeIfまたはIterator.remove |
List.ofの結果にadd |
UnsupportedOperationException |
new ArrayList<>(List.of(...)) |
| 独自クラスをSetに入れる | 同じ内容が重複する | equals/hashCodeを実装、またはrecord |
HashSetの順序に依存する |
表示順が安定しない | LinkedHashSet/TreeSet |
複数スレッドからHashMapを更新 |
更新が失われるなど結果が不定 | ConcurrentHashMapのmerge/compute |
よくある質問
Q. ListとSetはどちらを使えばよいですか?
A. 重複を許さない・存在チェックが多いならSet、順番や重複に意味があるならListです。実測では10万件のcontainsを1万回行うと、ArrayListが565.63 ms、HashSetが1.16 msでした。
Q. HashMapとTreeMapとLinkedHashMapの違いは何ですか?
A. キーの順序が違います。HashMapは順序なしで最も手軽、LinkedHashMapは追加した順、TreeMapはキーの並べ替え順でfloorEntryなどの範囲検索ができます。TreeMapの操作はO(log n)です。
Q. LinkedListはどんなときに使いますか?
A. 先頭・末尾の追加削除はO(1)ですが、キューやスタックの用途ならArrayDequeでも同等以上に速く動きます(実測で先頭挿入10万回:ArrayDeque 1.33 ms、LinkedList 1.44 ms)。途中の要素をIteratorで頻繁に挿入・削除するような特殊な場面以外は、ArrayListかArrayDequeで足ります。
Q. List.ofとArrays.asListの違いは何ですか?
A. List.ofは完全に変更不可でnullも不可、Arrays.asListは長さ固定だがsetで書き換えでき、元の配列とつながっている点が違います。変更したい場合はどちらもnew ArrayList<>(...)でコピーします。
まとめ
- 順番ならList、重複なしならSet、キーで引くならMap。迷ったら
ArrayList・HashSet・HashMap - 順序が要るなら
Linked〜、並べ替えや範囲検索ならTree〜、enumのキーならEnumMap List.of/Set.of/Map.ofは変更不可・null不可。contains(null)でも例外になる- Java 21の
getFirst/getLast/reversedで先頭・末尾を統一した書き方で扱える - ループ中の削除は
removeIf、集計はmerge、グループ化はcomputeIfAbsent - Setの要素・Mapのキーには
equals/hashCode(またはrecord) - 実測で
contains・先頭挿入・get(i)はクラス選びで数百倍の差が出た
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java List Set Map 使い分け|選び方フローと実測つき」
株式会社RJCでは、コードレビューや日々の開発の中で、データ構造の選び方のような基本を仲間と一緒に確かめ合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。