ArrayListとは、要素の数をあとから増やしたり減らしたりできる「長さが変わる配列」です。 new ArrayList<>() で作り、add で追加、get で取得、set で上書き、remove で削除します。配列(int[] など)は作るときに長さを決めますが、ArrayListは入れた分だけ自動で広がるので、件数が前もってわからないデータを扱うときに向いています。

この記事では、ArrayListのメソッドを一覧表にまとめ、すべてのコードをOpenJDK 21.0.10で実行した出力をそのまま載せています。 とくに、ループの中で remove したときに出る ConcurrentModificationException、List.of で作ったリストに add したときの UnsupportedOperationException は、例外メッセージを再現したうえで直し方を並べました。2次元リストとソートも同じ形式で扱います。Java 25での差分は公式APIドキュメントで確認し、該当箇所に注記しています。

ArrayListの早見表

まずは「やりたいこと」から引ける表です。戻り値の列を見ておくと、remove が「消した要素」を返すのか「true/false」を返すのかで迷わなくなります。

やりたいこと 書き方 戻り値 補足
空のリストを作る List<String> a = new ArrayList<>(); ― 変数の型は List にしておくと差し替えやすい
初期値つきで作る new ArrayList<>(List.of("A", "B")) ― あとから追加・削除できる
末尾に追加 a.add("D") true いちばん速い追加
位置を指定して追加 a.add(0, "Z") なし 以降の要素が1つ後ろにずれる
まとめて追加 a.addAll(other) 変化があれば true 位置指定は addAll(1, other)
取得 a.get(2) 要素 範囲外は IndexOutOfBoundsException
上書き a.set(2, "Y") 上書き前の要素 要素数は変わらない
要素数 a.size() int 配列の length に当たる
空かどうか a.isEmpty() boolean size() == 0 と同じ意味
含むかどうか a.contains("A") boolean equals で比べる
位置を探す a.indexOf("A") / a.lastIndexOf("A") 位置。ないときは -1 前から/後ろから
位置で削除 a.remove(0) 消した要素 以降の要素が前に詰まる
値で削除 a.remove("A") 消せたら true 最初に見つかった1つだけ
条件で削除 a.removeIf(s -> s.isEmpty()) 1つでも消せたら true ループ中の削除はこれが手軽
全部削除 a.clear() なし size() が0になる
並べ替え a.sort(null) / Collections.sort(a) なし 降順は Comparator.reverseOrder()
ループ for (String s : a) { ... } ― 位置が要るなら for (int i = 0; i < a.size(); i++)
先頭・末尾 a.getFirst() / a.getLast() 要素 Java 21以降
配列にする a.toArray(new String[0]) 配列
文字列にまとめる String.join(",", a) String 要素が文字列のとき

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java ArrayListの使い方|追加・削除・ループ・ソートを実行結果で」

まず動かす:ArrayListの基本コード

ArrayListを使うには java.util.ArrayList と java.util.List をimportします。 下のコードを Basic.java という名前で保存し、java Basic.java で実行できます(Java 11以降は、1ファイルならコンパイルなしで実行できます)。

import java.util.ArrayList;
import java.util.List;

public class Basic {
    public static void main(String[] args) {
        List<String> items = new ArrayList<>();
        items.add("ペン");            // 末尾に追加
        items.add("ノート");
        items.add(1, "付箋");         // 位置を指定して追加
        System.out.println(items);            // 中身を表示
        System.out.println(items.get(0));     // 取得
        System.out.println(items.size());     // 要素数

        items.set(0, "鉛筆");         // 上書き
        items.remove("ノート");       // 値で削除
        items.remove(0);              // 位置で削除
        System.out.println(items);

        for (String s : items) {      // ループ
            System.out.println("- " + s);
        }
    }
}

実行結果(OpenJDK 21.0.10):

[ペン, 付箋, ノート]
ペン
3
[付箋]
- 付箋

押さえておきたい点は3つです。

  • 位置(インデックス)は0から数えます。 add(1, "付箋") は「2番目に入れる」という意味で、もとの2番目以降が後ろにずれます。
  • <String> の部分は「このリストに入れる型」です。 右辺は new ArrayList<>() と書けば、左辺から型が補われます。
  • System.out.println(items) だけで中身が [ペン, 付箋, ノート] の形で表示されます。 配列と違い、Arrays.toString は要りません。

初期値つきで作るときは new ArrayList<>(List.of("A", "B"))、別のリストをコピーするときは new ArrayList<>(other) と書きます。 List.of と Arrays.asList は ArrayList ではなく別の種類のリストを返します。この違いは「List.ofで作ったリストにaddすると」の節で、例外つきで確認します。

int や double を入れたいとき

ArrayListに入れられるのはオブジェクトだけなので、int は Integer、double は Double、long は Long、boolean は Boolean、char は Character と書きます。

List<Integer> scores = new ArrayList<>();   // List<int> とは書けない
scores.add(80);                             // int から Integer へ自動で変換される
int first = scores.get(0);                  // Integer から int へ自動で変換される

全メソッド一覧と実行結果

ArrayListで使えるメソッドを1つのプログラムで順に呼び、出力を並べました。 左が呼び出したメソッド、右が戻り値とその時点のリストです。各行は System.out.println("add(\"D\") : " + a.add("D") + " " + a); のように「戻り値+その時点のリスト」を表示したものです。最初のリストは new ArrayList<>(List.of("B", "A", "C")) で、上から順に同じリストへ操作を重ねています。

実行結果(OpenJDK 21.0.10):

add("D")        : true [B, A, C, D]
add(0,"Z")      : [Z, B, A, C, D]
addAll          : true [Z, B, A, C, D, E, A]
addAll(1,..)    : [Z, x, y, B, A, C, D, E, A]
get(2)          : y
set(2,"Y")      : y [Z, x, Y, B, A, C, D, E, A]
size()          : 9
isEmpty()       : false
contains("A")   : true
indexOf("A")    : 4
lastIndexOf("A"): 8
indexOf("Q")    : -1
remove(0)       : Z [x, Y, B, A, C, D, E, A]
remove("A")     : true [x, Y, B, C, D, E, A]
remove("Q")     : false
removeIf        : true [Y, B, C, D, E, A]
removeAll       : true [B, C, D, A]
retainAll       : true [B, C, A]
subList(0,2)    : [B, C]
replaceAll      : [b, c, a]
getFirst()      : b
getLast()       : a
addFirst/addLast: [first, b, c, a, last]
removeFirst()   : first
removeLast()    : last [b, c, a]
reversed()      : [a, c, b]
toArray         : [b, c, a]
String.join     : b,c,a
equals          : true
clear()         : [] copy=[b, c, a]

メソッド一覧表(用途別)

分類 メソッド はたらき 導入
追加 add(E) 末尾に追加 1.2
追加 add(int, E) 位置を指定して挿入 1.2
追加 addAll(Collection) / addAll(int, Collection) まとめて追加 1.2
追加 addFirst(E) / addLast(E) 先頭・末尾に追加 21
取得 get(int) 位置の要素を返す 1.2
取得 getFirst() / getLast() 先頭・末尾の要素 21
取得 subList(from, to) from以上to未満の範囲(元とつながったビュー) 1.2
変更 set(int, E) 上書きし、上書き前の要素を返す 1.2
変更 replaceAll(UnaryOperator) 全要素を変換して置き換え 8
変更 sort(Comparator) 並べ替え 8
削除 remove(int) 位置で削除し、その要素を返す 1.2
削除 remove(Object) 値で削除(最初の1つ) 1.2
削除 removeIf(Predicate) 条件に合うものを全部削除 8
削除 removeAll(Collection) 指定した集まりに含まれるものを削除 1.2
削除 retainAll(Collection) 指定した集まりに含まれるものだけ残す 1.2
削除 removeFirst() / removeLast() 先頭・末尾を削除して返す 21
削除 clear() 全部削除 1.2
調べる size() / isEmpty() 要素数/空かどうか 1.2
調べる contains(Object) 含むかどうか 1.2
調べる indexOf(Object) / lastIndexOf(Object) 位置。ないときは -1 1.2
調べる equals(Object) 同じ順で同じ要素なら true 1.2
ループ forEach(Consumer) 各要素に処理をする 8
ループ iterator() / listIterator() 反復子を返す 1.2
ループ stream() Streamに変換 8
ループ reversed() 逆順のビュー 21
変換 toArray() / toArray(T[]) 配列にする 1.2
容量 ensureCapacity(int) / trimToSize() 内部配列を広げる/切り詰める(ArrayList型の変数でのみ呼べる) 1.2

Java 25との差分:公式APIドキュメント(Java SE 25の ArrayList)で確認したところ、Java 22〜25で追加されたメソッドはありません。 上の表はJava 25でもそのまま使えます。getFirst / getLast / addFirst / addLast / removeFirst / removeLast / reversed はJava 21で SequencedCollection として加わったもので、Java 17以前では a.get(0) や a.get(a.size() - 1) と書きます。

追加・取得・上書きで出る例外と直し方

ArrayListでいちばんよく出会う例外は IndexOutOfBoundsException です。 位置が「0以上、size() 未満」に収まっていないときに出ます。実際のメッセージを取りました。

import java.util.*;

public class Errors {
    static void run(String label, Runnable r) {
        try { r.run(); System.out.println(label + " -> OK"); }
        catch (RuntimeException e) { System.out.println(label + " -> " + e); }
    }
    public static void main(String[] args) {
        List<String> a = new ArrayList<>(List.of("A", "B", "C"));
        run("get(3)", () -> a.get(3));
        run("get(-1)", () -> a.get(-1));
        run("add(5,x)", () -> a.add(5, "x"));
        run("set(3,x)", () -> a.set(3, "x"));
        run("empty.getFirst()", () -> new ArrayList<String>().getFirst());
        run("empty.removeLast()", () -> new ArrayList<String>().removeLast());
    }
}

実行結果(OpenJDK 21.0.10):

get(3) -> java.lang.IndexOutOfBoundsException: Index 3 out of bounds for length 3
get(-1) -> java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 3
add(5,x) -> java.lang.IndexOutOfBoundsException: Index: 5, Size: 3
set(3,x) -> java.lang.IndexOutOfBoundsException: Index 3 out of bounds for length 3
empty.getFirst() -> java.util.NoSuchElementException
empty.removeLast() -> java.util.NoSuchElementException
メッセージ 起きていること 直し方
Index 3 out of bounds for length 3 要素が3つ(位置0〜2)のリストで位置3を読んだ 最後の要素は a.get(a.size() - 1) または a.getLast()
Index -1 out of bounds for length 3 indexOf が -1 を返したまま get に渡した、など indexOf の結果が0以上かを先に確かめる
Index: 5, Size: 3 add(位置, 値) で、末尾より先の位置を指定した add(位置, 値) の位置は0〜size() まで。間を空けて入れることはできない
set(3, x) で例外 set は「すでにある位置の上書き」専用 新しく増やすなら add を使う
NoSuchElementException 空のリストで getFirst() などを呼んだ isEmpty() を先に確かめる

new ArrayList<>(10) の10は「内部配列の初期サイズ」で、要素数ではありません。 この直後に set(0, "A") や get(0) を呼ぶと、要素がまだ0個なので IndexOutOfBoundsException になります。決まった個数を初期値で埋めたいときは、new ArrayList<>(Collections.nCopies(10, 0)) と書きます。

ループの書き方5通り

まずは拡張for文(for-each)で書き、位置が必要なときだけ添字つきのfor文にする、という順で選ぶと読みやすくなります。

import java.util.*;

public class Loops {
    public static void main(String[] args) {
        List<String> items = new ArrayList<>(List.of("ペン", "ノート", "付箋"));

        for (String s : items) System.out.print(s + " ");
        System.out.println("← 拡張for");

        for (int i = 0; i < items.size(); i++) System.out.print(i + ":" + items.get(i) + " ");
        System.out.println("← 添字つきfor");

        items.forEach(s -> System.out.print(s + " "));
        System.out.println("← forEach");

        Iterator<String> it = items.iterator();
        while (it.hasNext()) System.out.print(it.next() + " ");
        System.out.println("← Iterator");

        for (String s : items.reversed()) System.out.print(s + " ");
        System.out.println("← reversed()(Java 21以降)");

    }
}

実行結果(OpenJDK 21.0.10):

ペン ノート 付箋 ← 拡張for
0:ペン 1:ノート 2:付箋 ← 添字つきfor
ペン ノート 付箋 ← forEach
ペン ノート 付箋 ← Iterator
付箋 ノート ペン ← reversed()(Java 21以降)
書き方 位置がわかる ループ中の削除 選ぶ場面
拡張for文 わからない できない(例外になる) 全要素を順に読むだけ。最初の選択肢
添字つきfor文 わかる 後ろから回せばできる 番号を表示する、隣の要素と比べる
forEach わからない できない 1行で済む短い処理
Iterator わからない it.remove() でできる ループしながら削除する
reversed() わからない できない 逆順に読むだけ(Java 21以降)

ループ中のremoveで出るConcurrentModificationException

拡張for文の中でリストから要素を削除すると、ConcurrentModificationException が発生します。 「偶数を消したい」という、よくある処理で再現します。

import java.util.*;

public class Crash {
    public static void main(String[] args) {
        List<Integer> a = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
        for (Integer n : a) {
            if (n % 2 == 0) a.remove(n);
        }
    }
}

実行結果(OpenJDK 21.0.10):

Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
    at Crash.main(Crash.java:5)

なぜ起きるのか

拡張for文は、内部で Iterator(反復子)を使ってリストをたどっています。 ArrayListは、追加や削除のたびに「変更回数」を1つ増やします。Iteratorは作られた時点の変更回数を覚えていて、next() のたびに今の値と比べます。ループの途中で a.remove(...) を直接呼ぶと、この2つの数がずれるため、次の next() で例外が出ます。スタックトレースの checkForComodification がその確認をしている場所です。

名前に Concurrent(並行)とありますが、スレッドを使っていなくても起きます。 「たどっている最中に、たどっている相手を別の経路で変えた」ことを知らせる例外です。削除だけでなく、ループ中の add でも同じ例外になります(for (Integer n : h) { if (n == 1) h.add(99); } を実行して確認しました)。

例外が出ないのに結果が違うケース

例外が出ないまま結果が変わる書き方と、直し方3通りを1つのプログラムで比べました。

import java.util.*;

public class LoopRemove {
    public static void main(String[] args) {
        // (2) 後ろから2番目を消すと例外が出ずに終わる
        List<String> b = new ArrayList<>(List.of("A", "B", "C"));
        for (String s : b) {
            System.out.println("(2) 見た: " + s);
            if (s.equals("B")) b.remove(s);
        }
        System.out.println("(2) 結果=" + b + "(Cは一度も見ていない)");

        // (3) 添字の for で前から消す → 1つ飛ばす
        List<Integer> c = new ArrayList<>(List.of(2, 4, 6, 7, 8));
        for (int i = 0; i < c.size(); i++) {
            if (c.get(i) % 2 == 0) c.remove(i);
        }
        System.out.println("(3) 前から消す: " + c);

        // (4) removeIf
        List<Integer> d = new ArrayList<>(List.of(2, 4, 6, 7, 8));
        d.removeIf(n -> n % 2 == 0);
        System.out.println("(4) removeIf: " + d);

        // (5) Iterator.remove
        List<Integer> e = new ArrayList<>(List.of(2, 4, 6, 7, 8));
        Iterator<Integer> it = e.iterator();
        while (it.hasNext()) {
            int n = it.next();
            if (n % 2 == 0) it.remove();
        }
        System.out.println("(5) Iterator: " + e);

        // (6) 後ろから回す
        List<Integer> f = new ArrayList<>(List.of(2, 4, 6, 7, 8));
        for (int i = f.size() - 1; i >= 0; i--) {
            if (f.get(i) % 2 == 0) f.remove(i);
        }
        System.out.println("(6) 後ろから: " + f);
    }
}

実行結果(OpenJDK 21.0.10):

(2) 見た: A
(2) 見た: B
(2) 結果=[A, C](Cは一度も見ていない)
(3) 前から消す: [4, 7]
(4) removeIf: [7]
(5) Iterator: [7]
(6) 後ろから: [7]

読み取れることを整理します。

  • (2) は例外が出ませんが、最後の「C」を一度も見ていません。 後ろから2番目を消すと要素数が1つ減り、「もう次はない」と判定されてループが終わるためです。テストデータによっては気づきにくいので、「例外が出なかったから大丈夫」とは言えない書き方です。
  • (3) は添字つきfor文で前から消した結果、4が残りました。 位置0の「2」を消すと「4」が位置0に詰まりますが、i は1に進むので「4」を調べないまま通り過ぎます。

直し方の比較表

直し方 コード 元のリスト 選ぶ基準
removeIf list.removeIf(n -> n % 2 == 0); 変わる 条件で消すだけなら、まずこれ。1行で意図が伝わる
Iterator.remove if (条件) it.remove(); 変わる 消す以外の処理(ログ出力、件数の集計)も同じループで行う
後ろから回す for (int i = size - 1; i >= 0; i--) 変わる 位置を使った判定をしたい
Streamの filter new ArrayList<>(list.stream().filter(...).toList()) 変わらない 元を残したまま、条件に合うものだけの新しいリストがほしい
消すものを別に集める list.removeAll(toRemove); 変わる 消す対象を先に決めてから、まとめて消す

迷ったら removeIf を選ぶと、短く書けて取りこぼしもありません。

Integerのリストでremoveは「位置」か「値」かに注意

List<Integer> で remove(3) と書くと、「値が3の要素」ではなく「位置3の要素」が消えます。 int を渡すと remove(int index) が選ばれるためです。

List<Integer> k = new ArrayList<>(List.of(10, 20, 30, 3));
k.remove(3);                      // 位置3を削除
System.out.println("(9) remove(3): " + k);
k.remove(Integer.valueOf(10));    // 値10を削除
System.out.println("(9) remove(Integer.valueOf(10)): " + k);

実行結果(OpenJDK 21.0.10):

(9) remove(3): [10, 20, 30]
(9) remove(Integer.valueOf(10)): [20, 30]
書き方 選ばれるメソッド 消えるもの
k.remove(3) remove(int index) 位置3の要素
k.remove(Integer.valueOf(3)) remove(Object o) 値が3の最初の要素
k.removeIf(n -> n == 3) removeIf 値が3の要素すべて

List.ofで作ったリストにaddするとUnsupportedOperationException

List.of(...) が返すのは「変更できないリスト」で、add・set・remove を呼ぶと UnsupportedOperationException になります。 見た目は同じ List<String> 型なので、コンパイルは通り、実行したときに初めてわかります。

List<String> a = new ArrayList<>(List.of("A", "B", "C"));
List<String> fixed = List.of("A", "B");
run("List.of.add", () -> fixed.add("C"));
run("List.of.set", () -> fixed.set(0, "Z"));
run("List.of.remove", () -> fixed.remove(0));
run("List.of(null)", () -> List.of("A", null));
run("List.of.contains(null)", () -> fixed.contains(null));
List<String> asList = Arrays.asList("A", "B");
run("asList.set", () -> asList.set(0, "Z"));
run("asList.add", () -> asList.add("C"));
List<String> ro = Collections.unmodifiableList(a);
run("unmodifiableList.add", () -> ro.add("D"));
run("List.copyOf.add", () -> List.copyOf(a).add("D"));
run("stream.toList().add", () -> a.stream().toList().add("D"));

List<String> ok = new ArrayList<>(List.of("A", "B"));
ok.add("C");
System.out.println("new ArrayList<>(List.of(..)).add -> " + ok);

(run は前の節の Errors.java と同じメソッドです。)

実行結果(OpenJDK 21.0.10):

List.of.add -> java.lang.UnsupportedOperationException
List.of.set -> java.lang.UnsupportedOperationException
List.of.remove -> java.lang.UnsupportedOperationException
List.of(null) -> java.lang.NullPointerException
List.of.contains(null) -> java.lang.NullPointerException
asList.set -> OK
asList.add -> java.lang.UnsupportedOperationException
unmodifiableList.add -> java.lang.UnsupportedOperationException
List.copyOf.add -> java.lang.UnsupportedOperationException
stream.toList().add -> java.lang.UnsupportedOperationException
new ArrayList<>(List.of(..)).add -> [A, B, C]
作り方 add / remove set null 並べ替え(sort)
new ArrayList<>() できる できる 入れられる できる
new ArrayList<>(List.of(...)) できる できる 入れられる(元のList.ofには入れられない) できる
List.of(...) 例外 例外 入れると NullPointerException 例外
Arrays.asList(...) 例外 できる 入れられる できる
Collections.unmodifiableList(a) 例外 例外 元のリストしだい 例外
stream.toList()(Java 16以降) 例外 例外 入れられる 例外

直し方は1つで、new ArrayList<>(...) で包むことです。 これで中身をコピーした、変更できるリストになります。

// 直す前:実行時に UnsupportedOperationException
List<String> tags = List.of("新着", "注目");
tags.add("特集");

// 直した後
List<String> tags = new ArrayList<>(List.of("新着", "注目"));
tags.add("特集");

メソッドの戻り値として受け取ったリストが変更できるかどうかは、型からは判断できません。受け取ったリストに手を加えたいときは、new ArrayList<>(受け取ったリスト) でコピーしてから変更すると、呼び出し元のデータにも影響せず、例外も避けられます。

2次元リスト(リストのリスト)の作り方

2次元リストは List<List<Integer>> のように「リストを要素に持つリスト」として作ります。 2次元配列と違い、行ごとに長さが違ってもよく、行も列もあとから増やせます。

import java.util.*;

public class TwoD {
    public static void main(String[] args) {
        // 行ごとに長さが違ってもよい
        List<List<Integer>> grid = new ArrayList<>();
        grid.add(new ArrayList<>(List.of(1, 2, 3)));
        grid.add(new ArrayList<>(List.of(4, 5)));
        grid.add(new ArrayList<>());
        grid.get(2).add(6);
        System.out.println(grid);
        System.out.println("grid.get(1).get(0) = " + grid.get(1).get(0));
        grid.get(0).set(2, 99);
        System.out.println("行数=" + grid.size() + " 1行目の列数=" + grid.get(0).size());

        for (List<Integer> row : grid) {
            for (int v : row) System.out.print(v + " ");
            System.out.println();
        }

        // 3行4列を0で初期化
        List<List<Integer>> table = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            table.add(new ArrayList<>(Collections.nCopies(4, 0)));
        }
        table.get(1).set(2, 7);
        System.out.println("table = " + table);

        // 同じ行を使い回すと全行が変わる
        List<Integer> shared = new ArrayList<>(Collections.nCopies(4, 0));
        List<List<Integer>> wrong = new ArrayList<>(Collections.nCopies(3, shared));
        wrong.get(1).set(2, 7);
        System.out.println("wrong = " + wrong);

        // コピー:浅いコピーと深いコピー
        List<List<Integer>> shallow = new ArrayList<>(table);
        List<List<Integer>> deep = new ArrayList<>();
        for (List<Integer> row : table) deep.add(new ArrayList<>(row));
        table.get(0).set(0, -1);
        System.out.println("shallow = " + shallow.get(0) + " / deep = " + deep.get(0));
    }
}

実行結果(OpenJDK 21.0.10):

[[1, 2, 3], [4, 5], [6]]
grid.get(1).get(0) = 4
行数=3 1行目の列数=3
1 2 99 
4 5 
6 
table = [[0, 0, 0, 0], [0, 0, 7, 0], [0, 0, 0, 0]]
wrong = [[0, 0, 7, 0], [0, 0, 7, 0], [0, 0, 7, 0]]
shallow = [-1, 0, 0, 0] / deep = [0, 0, 0, 0]
やりたいこと 2次元配列 2次元リスト
宣言 int[][] g = new int[3][4]; List<List<Integer>> g = new ArrayList<>();
読む g[1][2] g.get(1).get(2)
書く g[1][2] = 7; g.get(1).set(2, 7);
行数 g.length g.size()
列数 g[0].length g.get(0).size()
行を増やす できない(作り直す) g.add(new ArrayList<>());
行に値を足す できない g.get(0).add(9);

つまずきやすい点は2つあります。

  • wrong の例では、1か所だけ7にしたつもりが全行が変わりました。 Collections.nCopies(3, shared) は「同じ行リストを3回参照する」リストを作るためです。行ごとに new ArrayList<>(...) を呼ぶ table の書き方にすると、行が独立します。
  • new ArrayList<>(table) は外側だけをコピーする「浅いコピー」です。 行のリストは元と共有されるので、元の行を変えるとコピー側も変わります(shallow の出力)。行までコピーしたいときは、deep のように行ごとに new ArrayList<>(row) します。

また、grid.get(2) で行を取り出す前に、その行が add 済みであることが必要です。外側のリストを作っただけでは行は0個なので、grid.get(0) は IndexOutOfBoundsException になります。

ソート(並べ替え)の書き方

並べ替えは list.sort(...) か Collections.sort(list) で行い、どちらもリスト自体を書き換えます。 昇順・降順・複数の条件・元を残す方法をまとめて実行しました。

import java.util.*;

public class Sorts {
    record Item(String name, int price, int stock) {}

    public static void main(String[] args) {
        List<Integer> nums = new ArrayList<>(List.of(30, 5, 100, 12));
        Collections.sort(nums);
        System.out.println("昇順          : " + nums);
        nums.sort(Comparator.reverseOrder());
        System.out.println("降順          : " + nums);

        List<String> words = new ArrayList<>(List.of("banana", "Cherry", "apple", "Date"));
        words.sort(Comparator.naturalOrder());
        System.out.println("文字列(自然順): " + words);
        words.sort(String.CASE_INSENSITIVE_ORDER);
        System.out.println("大小無視       : " + words);

        List<String> ns = new ArrayList<>(List.of("10", "9", "100"));
        Collections.sort(ns);
        System.out.println("数字の文字列   : " + ns);
        ns.sort(Comparator.comparingInt(Integer::parseInt));
        System.out.println("数値として     : " + ns);

        List<Item> items = new ArrayList<>(List.of(
            new Item("ノート", 300, 12), new Item("ペン", 120, 40),
            new Item("付箋", 300, 5), new Item("消しゴム", 80, 0)));
        items.sort(Comparator.comparingInt(Item::price));
        System.out.println("価格の昇順     : " + items.stream().map(Item::name).toList());
        items.sort(Comparator.comparingInt(Item::price).reversed().thenComparing(Item::stock));
        System.out.println("価格降順→在庫昇順: " + items.stream().map(Item::name).toList());

        List<Integer> src = List.of(3, 1, 2);
        List<Integer> sorted = src.stream().sorted().toList();
        System.out.println("元を残す       : 元=" + src + " 新=" + sorted);
        try { src.sort(null); } catch (UnsupportedOperationException e) {
            System.out.println("List.ofをsort  : " + e);
        }

    }
}

実行結果(OpenJDK 21.0.10):

昇順          : [5, 12, 30, 100]
降順          : [100, 30, 12, 5]
文字列(自然順): [Cherry, Date, apple, banana]
大小無視       : [apple, banana, Cherry, Date]
数字の文字列   : [10, 100, 9]
数値として     : [9, 10, 100]
価格の昇順     : [消しゴム, ペン, ノート, 付箋]
価格降順→在庫昇順: [付箋, ノート, ペン, 消しゴム]
元を残す       : 元=[3, 1, 2] 新=[1, 2, 3]
List.ofをsort  : java.lang.UnsupportedOperationException

ソートの逆引き表

やりたいこと 書き方
昇順 list.sort(null) または Collections.sort(list)
降順 list.sort(Comparator.reverseOrder())
大文字・小文字を区別しない list.sort(String.CASE_INSENSITIVE_ORDER)
オブジェクトを1項目で list.sort(Comparator.comparingInt(Item::price))
降順にする 上の末尾に .reversed()
同点のとき2つ目の条件 .thenComparing(Item::stock)
元を変えずに並べ替えた結果を得る list.stream().sorted().toList()
逆順にするだけ Collections.reverse(list)
ランダムに並べ替える Collections.shuffle(list)

実行結果から読み取れる注意点です。

  • 文字列の自然順では、大文字が小文字より前に並びます(Cherry が apple より前)。辞書のように並べたいときは String.CASE_INSENSITIVE_ORDER を使います。
  • 数字を文字列のまま並べると [10, 100, 9] になります。 1文字目から順に比べるためです。数値として並べたいときは Comparator.comparingInt(Integer::parseInt) を渡すか、最初から List<Integer> で持ちます。
  • List.of で作ったリストは sort も例外になります。 並べ替えは中身を書き換える操作だからです。new ArrayList<>(...) で包むか、stream().sorted().toList() で新しいリストを作ります。
  • 価格降順→在庫昇順 では、同じ価格300の「付箋(在庫5)」「ノート(在庫12)」が在庫の少ない順に並びました。 thenComparing は同点のときだけ使われます。

配列との変換・重複の削除・集計

よく組み合わせて使う処理を、短いコードと実行結果でまとめます(import java.util.*; と import java.util.stream.*; が必要です)。

// 配列との変換
String[] arr = {"A", "B", "C"};
List<String> fromArr = new ArrayList<>(Arrays.asList(arr));      // 配列→List(変更できる)
String[] back = fromArr.toArray(new String[0]);                  // List→配列
int[] prim = {3, 1, 2};
List<Integer> boxed = Arrays.stream(prim).boxed()
        .collect(Collectors.toCollection(ArrayList::new));       // int[]→List<Integer>
int[] unboxed = boxed.stream().mapToInt(Integer::intValue).toArray();

// 重複を取り除く(順番はそのまま)
List<String> dup = new ArrayList<>(List.of("B", "A", "B", "C", "A"));
System.out.println("distinct: " + dup.stream().distinct().toList());
System.out.println("LinkedHashSet: " + new ArrayList<>(new LinkedHashSet<>(dup)));

// 合計・平均・最大
List<Integer> nums = List.of(70, 85, 90);
IntSummaryStatistics st = nums.stream().mapToInt(Integer::intValue).summaryStatistics();
System.out.println("合計=" + st.getSum() + " 平均=" + st.getAverage() + " 最大=" + st.getMax());

// Integer の == 比較
List<Integer> big = new ArrayList<>(List.of(127, 127, 1000, 1000));
System.out.println("127==127: " + (big.get(0) == big.get(1)));
System.out.println("1000==1000: " + (big.get(2) == big.get(3)));
System.out.println("1000.equals(1000): " + big.get(2).equals(big.get(3)));

// subList はビュー
List<Integer> base = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> view = base.subList(1, 3);
view.set(0, 20);
System.out.println("subList変更後の元: " + base);
view.clear();
System.out.println("subList.clear後の元: " + base);

実行結果(OpenJDK 21.0.10):

distinct: [B, A, C]
LinkedHashSet: [B, A, C]
合計=245 平均=81.66666666666667 最大=90
127==127: true
1000==1000: false
1000.equals(1000): true
subList変更後の元: [1, 20, 3, 4, 5]
subList.clear後の元: [1, 4, 5]
  • List<Integer> の要素どうしを == で比べると、1000どうしでも false になりました。 == は「同じオブジェクトか」を比べるためです。-128〜127は同じオブジェクトが使い回されるので true になりますが、値の比較には equals を使うか、int の変数に取り出してから比べます。
  • subList は切り出したコピーではなく、元のリストの一部を見ている「ビュー」です。 view.set も view.clear() も元のリストに反映されました。この性質を使うと、base.subList(1, 3).clear() で「位置1〜2をまとめて削除」と書けます。独立したリストがほしいときは new ArrayList<>(base.subList(1, 3)) にします。
  • int[] を Arrays.asList に渡すと List<int[]>(要素が1つ)になるので、Arrays.stream(prim).boxed() を使います。

速さの目安:末尾は速く、先頭は要素数に比例する

ArrayListは内部に配列を持っているので、get と末尾への add は速く、先頭や途中への挿入・削除は「後ろの要素を全部ずらす」分だけ時間がかかります。 10万件で計測しました。

計測方法:for で10万回 a.add(i)(末尾)/b.add(0, i)(先頭)、続けて10万件のリストが空になるまで c.remove(c.size() - 1)(末尾)/d.remove(0)(先頭)を繰り返し、System.nanoTime() で時間を測りました。

実行結果(OpenJDK 21.0.10、2コアの検証用コンテナ、3周目の値):

末尾にadd      :    6.0 ms
先頭にadd(0,x) :  914.9 ms
末尾からremove :    0.3 ms
先頭からremove :  891.1 ms

数値は環境で変わりますが、先頭への挿入・削除は末尾の100倍以上の時間がかかるという傾向は共通です。

操作 計算量の目安 ひとこと
get(i) / set(i, x) 一定 位置を指定した読み書きは得意
add(x)(末尾) ほぼ一定 内部配列がいっぱいになると約1.5倍に広げてコピーする
add(0, x) / remove(0) 要素数に比例 後ろの要素を全部ずらす
contains / indexOf 要素数に比例 先頭から順に探す
removeIf 要素数に比例(1回の走査) ループで1つずつ remove するより効率がよい

先頭での出し入れが中心なら ArrayDeque、「含むかどうか」の判定が中心なら HashSet が合います。「順番に並べて、位置で読み、末尾に足す」という使い方であれば、ArrayListが第一の選択肢です。

よくある質問

Q. List<String> a = new ArrayList<>(); と ArrayList<String> a = new ArrayList<>(); はどちらで書くのがよいですか? A. 変数の型は List にしておくのがおすすめです。あとから LinkedList や List.of に差し替えても、その変数を使っているコードを書き換えずに済みます。ensureCapacity や trimToSize のように ArrayList だけが持つメソッドを使う場合に限り、ArrayList 型で宣言します。

Q. ArrayListと配列はどう使い分けますか? A. 件数があとから変わるならArrayList、件数が決まっていて int などの基本型を大量に扱うなら配列が向いています。ArrayListは add・remove・contains・sort などのメソッドがそろっていて、println でそのまま中身を表示できる点も便利です。

Q. ループ中に要素を削除する、いちばん簡単な方法は何ですか? A. list.removeIf(条件) です。list.removeIf(n -> n % 2 == 0) のように1行で書けて、ConcurrentModificationException も取りこぼしも起きません。削除と同時に別の処理もしたいときは Iterator の it.remove() を使います。

まとめ

  • ArrayListは長さが変わる配列。 add で追加、get で取得、set で上書き、remove で削除、size で要素数。
  • remove は2種類ある。 remove(int) は位置で消して要素を返し、remove(Object) は値で消して true/false を返す。List<Integer> では Integer.valueOf で区別する。
  • 拡張for文の中で remove すると ConcurrentModificationException。 removeIf が最短で、Iterator.remove・後ろから回すfor文・Streamの filter も使える。
  • List.of・Arrays.asList・toList() のリストは add できない。 new ArrayList<>(...) で包めば変更できる。
  • 2次元リストは行ごとに new ArrayList<>() を作る。 同じ行を共有すると全行が一緒に変わる。
  • ソートは list.sort(Comparator)。 降順は reverseOrder()、複数条件は thenComparing、元を残すなら stream().sorted().toList()。
  • 末尾の追加・削除は速く、先頭は要素数に比例して時間がかかる。 Java 22〜25でArrayListに追加されたメソッドはなく、この記事の内容はJava 25でも同じ。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java ArrayListの使い方|追加・削除・ループ・ソートを実行結果で」

株式会社RJCでは、ArrayListをはじめとするJavaのコレクションの使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?