- JAVA
Java LinkedListとArrayListの違い|速度比較と使い分け
ArrayList は「配列」で要素を持つリスト、LinkedList は要素どうしを「前後のつながり」で持つリストです。 どちらも List として同じように使えますが、番号での読み取り(get)は ArrayList が速く、先頭への追加と削除は LinkedList が速い、という違いがあります。迷ったら ArrayList、先頭と末尾だけを出し入れするなら ArrayDeque、というのが基本の選び方です。
この記事では、先頭・中間・末尾への追加と削除、get(i)、全件ループを ArrayList・LinkedList・ArrayDeque の3つで計測するコードを載せ、JDK 21 での実測値を表にしました。LinkedList を get(i) で回したときに時間が大きく伸びる例と直し方、固有メソッドの一覧、メモリの実測もまとめています。
確認環境:Ubuntu 24.04、OpenJDK 21.0.10(JVM オプションは既定のまま)、CPU は Intel Xeon 2.10GHz の2コア、メモリ 8GB。計測値はこの環境での結果で、機器や件数によって変わります。
LinkedList と ArrayList の違い 早見表
| 比べる点 | ArrayList | LinkedList | ArrayDeque(参考) |
|---|---|---|---|
| 要素の持ち方 | 配列 | 前後の要素への参照でつなぐ(双方向) | 配列(先頭と末尾の位置を持つ) |
| 実装しているもの | List | List と Deque | Deque(List ではない) |
get(i) 番号で読む |
速い(O(1)) | 件数に比例(O(n)) | 使えない |
| 末尾に追加 | 速い | 速い | 速い |
| 先頭に追加・削除 | 件数に比例(O(n)) | 速い(O(1)) | 速い(O(1)) |
| 中間に追加・削除(番号指定) | O(n)(後ろをずらす) | O(n)(その位置までたどる) | 使えない |
| Iterator でたどりながら削除 | 1件ごとに O(n) | 1件ごとに O(1) | 1件ごとに O(n) |
| メモリ(100万件の実測) | 約 5.0 MB | 約 22.9 MB | 約 5.0 MB |
| null を入れられるか | 入れられる | 入れられる | 入れられない |
| 向いている用途 | ほとんどの場面 | たどりながらの追加・削除が多い処理 | キュー、スタック |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java LinkedListとArrayListの違い|速度比較と使い分け」
仕組みの違い:配列と、前後のつながり
速度の差は、要素の持ち方から説明できます。
| 操作 | ArrayList(配列) | LinkedList(つながり) |
|---|---|---|
| 5番目を読む | 配列の5番目を直接読む | 端から順に5つたどる |
| 先頭に入れる | 全要素を1つずつ後ろへずらす | 先頭の参照を付け替えるだけ |
| 中間に入れる | 後ろ半分をずらす | 入れる位置まで順にたどり、参照を付け替える |
| 末尾に入れる | 空きがあれば置くだけ。足りなければ大きい配列に移す | 末尾の参照を付け替えるだけ |
Java SE 21 の API ドキュメントには、LinkedList について「インデックスを指定する操作は、先頭か末尾のうち近い方からリストをたどる」と書かれています。LinkedList の「中間への追加は速い」は、追加する位置にすでにいる場合の話で、番号で位置を指定すると、そこまでたどる時間がかかります。この点は次の計測ではっきり表れます。
速度を計測するコードと実測結果
10万件を対象に、10 種類の操作を3つのクラスで計測します。各操作を7回実行し、最初の2回をウォームアップとして捨て、残り5回の中央値を表示します。java ListBench.java でそのまま実行できます(この環境では全体で数分かかります)。
import java.util.*;
import java.util.function.Supplier;
public class ListBench {
static final int N = 100_000;
static final int RUNS = 5;
static long sink; // 計算結果を使って、最適化で処理ごと消えるのを防ぐ
interface Task { void run(); }
// RUNS 回測って中央値(ミリ秒)を返す。setup は計測時間に含めない
static double measure(Runnable setup, Task task) {
long[] times = new long[RUNS];
for (int r = 0; r < RUNS + 2; r++) { // 最初の2回はウォームアップ
setup.run();
long t0 = System.nanoTime();
task.run();
long t1 = System.nanoTime();
if (r >= 2) times[r - 2] = t1 - t0;
}
Arrays.sort(times);
return times[RUNS / 2] / 1_000_000.0;
}
static ArrayList<Integer> al;
static LinkedList<Integer> ll;
static ArrayDeque<Integer> ad;
static void fill(Collection<Integer> c) { for (int i = 0; i < N; i++) c.add(i); }
static void newEmpty() { al = new ArrayList<>(); ll = new LinkedList<>(); ad = new ArrayDeque<>(); }
static void newFull() { newEmpty(); fill(al); fill(ll); fill(ad); }
static void row(String name, Double a, Double l, Double d) {
System.out.printf("%-28s %12s %12s %12s%n", name, fmt(a), fmt(l), fmt(d));
}
static String fmt(Double v) { return v == null ? "-" : String.format("%.2f", v); }
public static void main(String[] args) {
System.out.println("N = " + N + "、単位はミリ秒(" + RUNS + "回の中央値)");
System.out.printf("%-28s %12s %12s %12s%n", "操作", "ArrayList", "LinkedList", "ArrayDeque");
row("末尾に追加 x N",
measure(ListBench::newEmpty, () -> { for (int i = 0; i < N; i++) al.add(i); }),
measure(ListBench::newEmpty, () -> { for (int i = 0; i < N; i++) ll.add(i); }),
measure(ListBench::newEmpty, () -> { for (int i = 0; i < N; i++) ad.addLast(i); }));
row("先頭に追加 x N",
measure(ListBench::newEmpty, () -> { for (int i = 0; i < N; i++) al.add(0, i); }),
measure(ListBench::newEmpty, () -> { for (int i = 0; i < N; i++) ll.addFirst(i); }),
measure(ListBench::newEmpty, () -> { for (int i = 0; i < N; i++) ad.addFirst(i); }));
row("中間に追加 x N",
measure(ListBench::newEmpty, () -> { for (int i = 0; i < N; i++) al.add(al.size() / 2, i); }),
measure(ListBench::newEmpty, () -> { for (int i = 0; i < N; i++) ll.add(ll.size() / 2, i); }),
null);
row("get(i) で全件",
measure(ListBench::newFull, () -> { long s = 0; for (int i = 0; i < N; i++) s += al.get(i); sink += s; }),
measure(ListBench::newFull, () -> { long s = 0; for (int i = 0; i < N; i++) s += ll.get(i); sink += s; }),
null);
row("拡張for で全件",
measure(ListBench::newFull, () -> { long s = 0; for (int v : al) s += v; sink += s; }),
measure(ListBench::newFull, () -> { long s = 0; for (int v : ll) s += v; sink += s; }),
measure(ListBench::newFull, () -> { long s = 0; for (int v : ad) s += v; sink += s; }));
row("先頭から削除 x N",
measure(ListBench::newFull, () -> { while (!al.isEmpty()) sink += al.remove(0); }),
measure(ListBench::newFull, () -> { while (!ll.isEmpty()) sink += ll.removeFirst(); }),
measure(ListBench::newFull, () -> { while (!ad.isEmpty()) sink += ad.pollFirst(); }));
row("中間から削除 x N",
measure(ListBench::newFull, () -> { while (!al.isEmpty()) sink += al.remove(al.size() / 2); }),
measure(ListBench::newFull, () -> { while (!ll.isEmpty()) sink += ll.remove(ll.size() / 2); }),
null);
row("末尾から削除 x N",
measure(ListBench::newFull, () -> { while (!al.isEmpty()) sink += al.remove(al.size() - 1); }),
measure(ListBench::newFull, () -> { while (!ll.isEmpty()) sink += ll.removeLast(); }),
measure(ListBench::newFull, () -> { while (!ad.isEmpty()) sink += ad.pollLast(); }));
row("Iterator で偶数を削除",
measure(ListBench::newFull, () -> { for (Iterator<Integer> it = al.iterator(); it.hasNext();) if (it.next() % 2 == 0) it.remove(); }),
measure(ListBench::newFull, () -> { for (Iterator<Integer> it = ll.iterator(); it.hasNext();) if (it.next() % 2 == 0) it.remove(); }),
measure(ListBench::newFull, () -> { for (Iterator<Integer> it = ad.iterator(); it.hasNext();) if (it.next() % 2 == 0) it.remove(); }));
row("removeIf で偶数を削除",
measure(ListBench::newFull, () -> al.removeIf(v -> v % 2 == 0)),
measure(ListBench::newFull, () -> ll.removeIf(v -> v % 2 == 0)),
measure(ListBench::newFull, () -> ad.removeIf(v -> v % 2 == 0)));
System.out.println("(sink=" + sink + ")");
}
}
実行結果です。
N = 100000、単位はミリ秒(5回の中央値)
操作 ArrayList LinkedList ArrayDeque
末尾に追加 x N 2.71 1.73 3.00
先頭に追加 x N 419.51 1.36 1.84
中間に追加 x N 203.00 6193.25 -
get(i) で全件 0.31 4350.44 -
拡張for で全件 0.42 0.84 0.18
先頭から削除 x N 424.57 0.92 0.22
中間から削除 x N 210.35 4285.59 -
末尾から削除 x N 0.36 1.02 0.27
Iterator で偶数を削除 212.68 1.20 65.49
removeIf で偶数を削除 0.91 1.42 1.74
(sink=454995450000)
実測結果の表(10万件、単位はミリ秒)
| 操作(10万回) | ArrayList | LinkedList | ArrayDeque | 速かったもの |
|---|---|---|---|---|
| 末尾に追加 | 2.71 | 1.73 | 3.00 | ほぼ同じ |
| 先頭に追加 | 419.51 | 1.36 | 1.84 | LinkedList、ArrayDeque |
| 中間に追加(番号指定) | 203.00 | 6193.25 | ― | ArrayList |
| get(i) で全件 | 0.31 | 4350.44 | ― | ArrayList |
| 拡張 for で全件 | 0.42 | 0.84 | 0.18 | ほぼ同じ |
| 先頭から削除 | 424.57 | 0.92 | 0.22 | ArrayDeque、LinkedList |
| 中間から削除(番号指定) | 210.35 | 4285.59 | ― | ArrayList |
| 末尾から削除 | 0.36 | 1.02 | 0.27 | ほぼ同じ |
| Iterator でたどりながら偶数を削除 | 212.68 | 1.20 | 65.49 | LinkedList |
| removeIf で偶数を削除 | 0.91 | 1.42 | 1.74 | ほぼ同じ |
結果から読み取れること
- 先頭への追加と削除は、ArrayList だけが 400 ミリ秒台で、LinkedList と ArrayDeque は 2 ミリ秒未満でした。ArrayList は毎回、全要素を1つずらすためです。
- 番号を指定した中間への追加と削除は、LinkedList の方が時間がかかりました(追加で約 6.2 秒、ArrayList は約 0.2 秒)。位置までたどる時間が、配列をずらす時間を上回っています。
- get(i) での全件読み取りは、LinkedList が約 4.4 秒、ArrayList が 0.31 ミリ秒でした。同じ全件読み取りでも、拡張 for 文なら LinkedList も 0.84 ミリ秒です。
- Iterator でたどりながらの削除は LinkedList が最も速く、1.20 ミリ秒でした。LinkedList の強みが出るのはこの使い方です。
- 条件に合う要素を消すだけなら removeIf を使うと、3つとも 2 ミリ秒未満に収まりました。
- 末尾への追加・削除と拡張 for 文は、どれを選んでも1ミリ秒前後の差です。
API ドキュメントでも、ArrayList の説明に「定数係数は LinkedList 実装に比べて低い」、ArrayDeque の説明に「キューとして使う場合は LinkedList より速い可能性が高い」と書かれており、実測と同じ方向です。
LinkedList を get(i) で回すと時間が大きく伸びる
件数を増やしたときの実測
for 文と get(i) の組み合わせは ArrayList ではよく使う形ですが、LinkedList では get(i) のたびに端からたどり直します。件数を2倍ずつ増やして、3つの書き方を比べます。
import java.util.*;
public class GetLoop {
static long sum;
static double median(Runnable r) {
long[] t = new long[3];
for (int k = 0; k < 3; k++) {
long t0 = System.nanoTime();
r.run();
t[k] = System.nanoTime() - t0;
}
Arrays.sort(t);
return t[1] / 1e6;
}
static void run(int n, boolean print) {
List<Integer> list = new LinkedList<>();
for (int i = 0; i < n; i++) list.add(i);
double byGet = median(() -> {
for (int i = 0; i < list.size(); i++) {
sum += list.get(i); // 毎回、端からたどり直す
}
});
double byFor = median(() -> {
for (int v : list) { // 内部で Iterator を使う
sum += v;
}
});
double byIter = median(() -> {
for (ListIterator<Integer> it = list.listIterator(); it.hasNext();) {
sum += it.next();
}
});
if (print) {
System.out.printf("%8d | %10.2f ms | %6.2f ms | %6.2f ms%n", n, byGet, byFor, byIter);
}
}
public static void main(String[] args) {
run(5_000, false); // ウォームアップ
run(5_000, false);
System.out.println(" 件数 | get(i) ループ | 拡張for | ListIterator");
for (int n : new int[] {10_000, 20_000, 40_000, 80_000}) {
run(n, true);
}
System.out.println("(sum=" + sum + ")");
}
}
件数 | get(i) ループ | 拡張for | ListIterator
10000 | 44.32 ms | 0.89 ms | 0.82 ms
20000 | 167.09 ms | 0.25 ms | 0.63 ms
40000 | 681.29 ms | 0.30 ms | 0.40 ms
80000 | 2754.81 ms | 0.49 ms | 0.42 ms
(sum=38474280000)
| 件数 | get(i) ループ | 拡張 for | ListIterator |
|---|---|---|---|
| 1万件 | 44.32 ms | 0.89 ms | 0.82 ms |
| 2万件 | 167.09 ms | 0.25 ms | 0.63 ms |
| 4万件 | 681.29 ms | 0.30 ms | 0.40 ms |
| 8万件 | 2754.81 ms | 0.49 ms | 0.42 ms |
get(i) ループは、件数が2倍になるたびに時間が約4倍になっています(44 → 167 → 681 → 2755 ミリ秒)。1回の get(i) が件数に比例し、それを件数ぶん繰り返すので、全体では件数の2乗に比例するためです。拡張 for 文と ListIterator は、8万件でも 0.5 ミリ秒前後でした。
Iterator・ListIterator で直す書き方
直し方は、番号で取り直さず、たどっている位置をそのまま使うことです。読むだけなら拡張 for 文、たどりながら削除するなら Iterator、置き換えや追加もするなら ListIterator を使います。
import java.util.*;
public class IterEdit {
public static void main(String[] args) {
List<String> tasks = new LinkedList<>(List.of("設計", "一時メモ", "開発", "一時メモ", "テスト"));
// 直す前: get(i) と remove(i) で回す(件数が増えると時間が大きく伸びる)
List<String> before = new LinkedList<>(tasks);
for (int i = 0; i < before.size(); i++) {
if (before.get(i).equals("一時メモ")) {
before.remove(i);
i--;
}
}
System.out.println("get(i) で削除 : " + before);
// 直した後1: Iterator でたどりながら削除
List<String> a = new LinkedList<>(tasks);
for (Iterator<String> it = a.iterator(); it.hasNext();) {
if (it.next().equals("一時メモ")) {
it.remove();
}
}
System.out.println("Iterator.remove : " + a);
// 直した後2: ListIterator なら置き換え・追加もその場でできる
List<String> b = new LinkedList<>(tasks);
for (ListIterator<String> it = b.listIterator(); it.hasNext();) {
int index = it.nextIndex();
String s = it.next();
if (s.equals("一時メモ")) {
it.remove(); // 今の要素を削除
} else if (s.equals("開発")) {
it.set("開発(" + index + "番目)"); // 今の要素を置き換え
it.add("レビュー"); // 今の要素の直後に追加
}
}
System.out.println("ListIterator : " + b);
// 直した後3: 条件で消すだけなら removeIf
List<String> c = new LinkedList<>(tasks);
c.removeIf(s -> s.equals("一時メモ"));
System.out.println("removeIf : " + c);
// 後ろからたどる
StringBuilder sb = new StringBuilder();
for (ListIterator<String> it = c.listIterator(c.size()); it.hasPrevious();) {
sb.append(it.previous()).append(' ');
}
System.out.println("hasPrevious/previous : " + sb.toString().trim());
// 拡張for の中で list.remove を呼ぶと例外になる
try {
for (String s : tasks) {
if (s.equals("設計")) tasks.remove(s);
}
} catch (ConcurrentModificationException | UnsupportedOperationException e) {
System.out.println("拡張for 内で remove : " + e);
}
}
}
get(i) で削除 : [設計, 開発, テスト]
Iterator.remove : [設計, 開発, テスト]
ListIterator : [設計, 開発(1番目), レビュー, テスト]
removeIf : [設計, 開発, テスト]
hasPrevious/previous : テスト 開発 設計
拡張for 内で remove : java.util.ConcurrentModificationException
| やりたいこと | 書き方 |
|---|---|
| 全件を順に読む | 拡張 for 文(for (String s : list)) |
| たどりながら削除する | Iterator の it.remove() |
| たどりながら置き換える・追加する | ListIterator の it.set(値)、it.add(値) |
| 今の番号を知りたい | ListIterator の it.nextIndex() |
| 後ろからたどる | list.listIterator(list.size()) と hasPrevious()、previous() |
| 条件に合うものをまとめて消す | list.removeIf(条件) |
実行結果の最後の行のとおり、拡張 for 文の中で list.remove(...) を直接呼ぶと ConcurrentModificationException になります。たどっている途中の削除は、Iterator の remove() か removeIf で行います。
受け取ったリストが ArrayList か LinkedList かわからないメソッドでは、get(i) を使わず拡張 for 文で書いておくと、どちらが渡されても速度が安定します。
メモリ使用量の実測
100万件を入れたときに、入れ物そのものが使うメモリを測ります。要素の Integer は先に作っておき、入れ物の増加分だけを見ています。
import java.util.*;
import java.util.function.Supplier;
public class MemBench {
static final int N = 1_000_000;
static Object keep; // 計測中に回収されないよう参照を持っておく
static long used() {
for (int i = 0; i < 5; i++) {
System.gc();
try { Thread.sleep(100); } catch (InterruptedException e) { }
}
Runtime rt = Runtime.getRuntime();
return rt.totalMemory() - rt.freeMemory();
}
static void measure(String name, Integer[] src, Supplier<Collection<Integer>> factory) {
long[] diff = new long[3];
for (int k = 0; k < 3; k++) {
keep = null;
long before = used();
Collection<Integer> c = factory.get();
for (Integer v : src) c.add(v);
keep = c;
diff[k] = used() - before;
}
Arrays.sort(diff);
System.out.printf("%-24s %6.1f MB (1要素あたり約 %.1f バイト)%n",
name, diff[1] / 1024.0 / 1024.0, diff[1] / (double) N);
}
public static void main(String[] args) {
// 要素(Integer)は先に作っておき、入れ物だけの増加分を測る
Integer[] src = new Integer[N];
for (int i = 0; i < N; i++) src[i] = i;
System.out.println("N = " + N + "、要素の Integer 本体は含まない(3回の中央値)");
measure("ArrayList", src, ArrayList::new);
measure("ArrayList(初期容量N)", src, () -> new ArrayList<>(N));
measure("LinkedList", src, LinkedList::new);
measure("ArrayDeque", src, ArrayDeque::new);
measure("ArrayDeque(初期容量N)", src, () -> new ArrayDeque<>(N));
}
}
N = 1000000、要素の Integer 本体は含まない(3回の中央値)
ArrayList 5.0 MB (1要素あたり約 5.2 バイト)
ArrayList(初期容量N) 4.0 MB (1要素あたり約 4.2 バイト)
LinkedList 22.9 MB (1要素あたり約 24.0 バイト)
ArrayDeque 5.0 MB (1要素あたり約 5.2 バイト)
ArrayDeque(初期容量N) 4.0 MB (1要素あたり約 4.2 バイト)
| クラス | 100万件での増加分 | 1要素あたり |
|---|---|---|
| ArrayList | 5.0 MB | 約 5.2 バイト |
| ArrayList(初期容量を指定) | 4.0 MB | 約 4.2 バイト |
| LinkedList | 22.9 MB | 約 24.0 バイト |
| ArrayDeque | 5.0 MB | 約 5.2 バイト |
| ArrayDeque(初期容量を指定) | 4.0 MB | 約 4.2 バイト |
LinkedList は、ArrayList の4倍以上のメモリを使いました。 LinkedList は要素1つごとに、要素への参照に加えて「前の要素」「次の要素」への参照を持つオブジェクトを作るためです。ArrayList と ArrayDeque は配列に参照を並べるだけで、余分は配列の空き部分だけです。件数がわかっているときは new ArrayList<>(件数) と初期容量を渡すと、空きと配列の作り直しを減らせます。
LinkedList の固有メソッド一覧と使い方
LinkedList は List に加えて Deque(両端キュー)も実装しているため、先頭と末尾を操作するメソッドが使えます。
import java.util.*;
public class Methods {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>(List.of("B", "C"));
// 両端への追加
list.addFirst("A");
list.addLast("D");
list.offerFirst("0");
list.offerLast("E");
System.out.println("追加後 : " + list);
// 両端を見る(削除しない)
System.out.println("getFirst : " + list.getFirst());
System.out.println("getLast : " + list.getLast());
System.out.println("peek : " + list.peek());
System.out.println("peekLast : " + list.peekLast());
// 両端から取り出す(削除する)
System.out.println("removeFirst : " + list.removeFirst());
System.out.println("pollLast : " + list.pollLast());
System.out.println("poll : " + list.poll());
System.out.println("取り出し後 : " + list);
// スタックとして使う(先頭に積んで先頭から取る)
list.push("X");
System.out.println("push(X) 後 : " + list);
System.out.println("pop : " + list.pop());
// List としての操作もそのまま使える
list.add(1, "中間");
System.out.println("add(1, 中間) : " + list);
System.out.println("get(1) : " + list.get(1));
System.out.println("indexOf(D) : " + list.indexOf("D"));
System.out.println("reversed : " + list.reversed());
// 逆順にたどる
StringBuilder sb = new StringBuilder();
for (Iterator<String> it = list.descendingIterator(); it.hasNext();) {
sb.append(it.next()).append(' ');
}
System.out.println("descending : " + sb.toString().trim());
// 空のときの違い
LinkedList<String> empty = new LinkedList<>();
System.out.println("空の peek : " + empty.peek());
System.out.println("空の pollLast : " + empty.pollLast());
try {
empty.getFirst();
} catch (NoSuchElementException e) {
System.out.println("空の getFirst : " + e);
}
// null の扱い
empty.add(null);
System.out.println("LinkedList に null : " + empty);
try {
new ArrayDeque<String>().add(null);
} catch (NullPointerException e) {
System.out.println("ArrayDeque に null : " + e.getClass().getSimpleName());
}
}
}
追加後 : [0, A, B, C, D, E]
getFirst : 0
getLast : E
peek : 0
peekLast : E
removeFirst : 0
pollLast : E
poll : A
取り出し後 : [B, C, D]
push(X) 後 : [X, B, C, D]
pop : X
add(1, 中間) : [B, 中間, C, D]
get(1) : 中間
indexOf(D) : 3
reversed : [D, C, 中間, B]
descending : D C 中間 B
空の peek : null
空の pollLast : null
空の getFirst : java.util.NoSuchElementException
LinkedList に null : [null]
ArrayDeque に null : NullPointerException
| やりたいこと | 空のとき例外を投げる | 空のとき null(または false)を返す |
|---|---|---|
| 先頭に追加 | addFirst(e)、push(e) |
offerFirst(e) |
| 末尾に追加 | addLast(e)、add(e) |
offerLast(e)、offer(e) |
| 先頭を見る(削除しない) | getFirst()、element() |
peekFirst()、peek() |
| 末尾を見る | getLast() |
peekLast() |
| 先頭を取り出す(削除する) | removeFirst()、remove()、pop() |
pollFirst()、poll() |
| 末尾を取り出す | removeLast() |
pollLast() |
| 逆順にたどる | descendingIterator()、reversed()(Java 21) |
― |
空になりうるときは peek、poll 系を使い、null かどうかで分岐します。getFirst() や removeFirst() は、空のとき NoSuchElementException になります。
Java 21 では addFirst、getFirst、removeLast、reversed などが List 全体で使えるようになったため、ArrayList でも同じ名前のメソッドを呼べます。ただし速度の特性は変わらず、ArrayList の addFirst は計測した「先頭に追加」と同じく件数に比例します。peek、poll、push、pop は Deque のメソッドなので、ArrayList にはありません。
キューやスタックには ArrayDeque が第一候補
先頭と末尾だけを出し入れする用途では、ArrayDeque が同じメソッド名で使えます。計測でも、先頭からの削除は ArrayDeque が 0.22 ミリ秒、LinkedList が 0.92 ミリ秒で、メモリは約4分の1でした。
import java.util.*;
public class QueueUse {
public static void main(String[] args) {
// キュー(先に入れたものから取り出す)は ArrayDeque が第一候補
Deque<String> queue = new ArrayDeque<>();
queue.offer("注文A");
queue.offer("注文B");
queue.offer("注文C");
String order;
while ((order = queue.poll()) != null) {
System.out.println("処理: " + order);
}
// スタック(後から入れたものを先に取り出す)
Deque<String> stack = new ArrayDeque<>();
stack.push("画面1");
stack.push("画面2");
stack.push("画面3");
System.out.println("pop : " + stack.pop());
System.out.println("peek: " + stack.peek());
// 変数の型を Deque にしておけば、実装の入れ替えは new の部分だけ
Deque<String> linked = new LinkedList<>(List.of("a", "b"));
linked.addFirst("z");
System.out.println(linked);
// 番号で読むことが多いなら ArrayList
List<String> names = new ArrayList<>(List.of("佐藤", "鈴木", "高橋"));
System.out.println(names.get(1));
}
}
処理: 注文A
処理: 注文B
処理: 注文C
pop : 画面3
peek: 画面2
[z, a, b]
鈴木
| 比べる点 | LinkedList | ArrayDeque |
|---|---|---|
| 両端の追加・取り出し | 速い | 速い |
番号での読み書き(get(i)、add(i, e)) |
できる(件数に比例) | できない |
| null を入れる | できる | できない(NullPointerException) |
| メモリ(100万件の実測) | 22.9 MB | 5.0 MB |
| たどりながらの削除 | 速い | 件数に比例 |
変数の型を Deque や List といったインターフェースにしておくと、あとから実装を替えるときに new の部分だけを書き換えれば済みます。
使い分けの判断表
| 使い方 | 選ぶクラス | 理由 |
|---|---|---|
| どれにするか決めかねている | ArrayList | 読み取りが速く、メモリも少ない |
| 番号で読むことが多い | ArrayList | get(i) が O(1) |
| 末尾への追加が中心で、あとは順に読む | ArrayList | 末尾追加はどれも速い |
| 先に入れたものから取り出す(キュー) | ArrayDeque | 両端が速く、メモリが少ない |
| 後から入れたものを先に取り出す(スタック) | ArrayDeque | push、pop、peek が使える |
| 両端の出し入れに加えて、番号での操作や null も必要 | LinkedList | List と Deque の両方を実装している |
| たどりながら、途中の要素を何度も追加・削除する | LinkedList | Iterator 経由の追加・削除が O(1) |
| 条件に合う要素をまとめて消すだけ | どれでもよい(removeIf) |
計測では3つとも 2 ミリ秒未満 |
計算量・メモリ・用途の早見表
| 操作 | ArrayList | LinkedList | ArrayDeque |
|---|---|---|---|
get(i)、set(i, e) |
O(1) | O(n) | ― |
| 末尾に追加 | O(1)(ならし) | O(1) | O(1)(ならし) |
| 先頭に追加・先頭から削除 | O(n) | O(1) | O(1)(ならし) |
| 番号指定で中間に追加・削除 | O(n) | O(n) | ― |
Iterator の remove() |
O(n) | O(1) | O(n) |
contains、indexOf |
O(n) | O(n) | O(n)(contains のみ) |
| 全件ループ(拡張 for) | O(n) | O(n) | O(n) |
| 1要素あたりのメモリ(実測) | 約 4〜5 バイト | 約 24 バイト | 約 4〜5 バイト |
| 主な用途 | 一覧、検索結果、集計対象 | 編集しながらたどるリスト | キュー、スタック、履歴 |
「ならし」は、配列を広げる回がたまにあるものの、平均すると1回あたり一定時間で済むという意味です。
よくある質問
Q. 追加や削除が多いなら LinkedList の方が速いのですか? A. 場所によります。先頭への追加・削除と、Iterator でたどりながらの追加・削除は LinkedList が速く、計測では先頭への追加が ArrayList の 419.51 ミリ秒に対して 1.36 ミリ秒でした。一方、番号を指定した中間への追加は LinkedList が約 6.2 秒、ArrayList が約 0.2 秒で、ArrayList の方が短い結果でした。
Q. LinkedList のループが極端に遅いのですが、どう直せばよいですか?
A. for (int i = 0; i < list.size(); i++) と list.get(i) の組み合わせになっていないかを見ます。拡張 for 文か Iterator に替えると、8万件の計測で 2754.81 ミリ秒が 0.49 ミリ秒になりました。
Q. キューを作るとき、LinkedList と ArrayDeque のどちらを使えばよいですか? A. 先頭と末尾の出し入れだけなら ArrayDeque が第一候補です。計測では取り出しが速く、メモリも約4分の1でした。null を入れたい場合や、番号での操作も必要な場合は LinkedList を選びます。
Q. ArrayList から LinkedList に替えるとき、コードはどこを直しますか?
A. 変数の型を List にしていれば、new ArrayList<>() を new LinkedList<>() に替えるだけで動きます。あわせて、get(i) を使ったループが残っていないかを確かめ、拡張 for 文や Iterator に替えておきます。
まとめ
- ArrayList は配列、LinkedList は前後のつながりで要素を持ちます。使えるメソッドは似ていても、速い操作が異なります。
- 番号で読む get(i) は ArrayList が速く、10万件の全件読み取りで 0.31 ミリ秒と約 4.4 秒の差がありました。
- 先頭への追加と削除は LinkedList と ArrayDeque が速く、ArrayList は件数に比例して時間がかかります。
- 番号を指定した中間への追加・削除は、LinkedList の方が時間がかかりました。LinkedList が速いのは、Iterator でたどりながら追加・削除する使い方です。
- LinkedList を get(i) で回すと、件数が2倍で時間が約4倍になります。拡張 for 文、Iterator、ListIterator、removeIf に替えます。
- メモリは LinkedList が1要素あたり約 24 バイト、ArrayList と ArrayDeque は約 4〜5 バイトでした。
- 迷ったら ArrayList、キューやスタックには ArrayDeque、両端の操作と番号での操作を両方使うなら LinkedList を選びます。
参考・出典
- Java SE 21 API ドキュメント:java.util.LinkedList(双方向リスト、インデックス指定の操作のたどり方)
- Java SE 21 API ドキュメント:java.util.ArrayList(各操作の実行時間、容量)
- Java SE 21 API ドキュメント:java.util.ArrayDeque(null の扱い、LinkedList との比較)
- Java SE 21 API ドキュメント:java.util.SequencedCollection(addFirst、getFirst、reversed など)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java LinkedListとArrayListの違い|速度比較と使い分け」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。