Javaの配列コピーとは、元の配列と同じ中身を持つ別の配列を作ることで、主な方法は clone()・Arrays.copyOf・Arrays.copyOfRange・System.arraycopy の4つです。 全体をそのまま写すなら clone()、長さを変えながら写すなら Arrays.copyOf、一部分だけ取り出すなら Arrays.copyOfRange、すでにある配列の好きな位置へ写すなら System.arraycopy が合います。

この記事では、4つの方法を引数・長さ変更の可否・用途で比べる早見表を最初に置き、すべてのコードを OpenJDK 21.0.10 で動かした出力つきで説明します。2次元配列やオブジェクト配列で「写しを書き換えたら元も変わった」という動きの再現、深いコピーの書き方、2つの配列を結合するコード(arraycopy版とStream版)まで、コピーして使える形でまとめました。

配列コピー4つの方法の早見表

方法 書き方 引数 長さを変えられるか コピー先の配列 向いている用途
clone() src.clone() なし 変えられない(元と同じ) 自動で作られる 全体をそのまま写す
Arrays.copyOf Arrays.copyOf(src, 長さ) 元の配列、新しい長さ 変えられる(短く・長く) 自動で作られる 先頭から写す、配列を広げる
Arrays.copyOfRange Arrays.copyOfRange(src, 開始, 終了) 元の配列、開始位置、終了位置(その手前まで) 変えられる(範囲で決まる) 自動で作られる 一部分を取り出す
System.arraycopy System.arraycopy(src, 元の位置, dest, 先の位置, 個数) 元の配列、元の開始位置、コピー先、先の開始位置、個数 コピー先の長さによる 自分で用意する 既存の配列へ写す、結合、ずらす

やりたいことから選ぶときは、次の表を使ってください。

やりたいこと コード
全体をコピーする int[] copy = src.clone();
先頭3件を取り出す Arrays.copyOf(src, 3)
末尾に空きを作って広げる Arrays.copyOf(src, src.length + 1)
インデックス1〜3を取り出す Arrays.copyOfRange(src, 1, 4)
2つの配列を結合する Arrays.copyOf で広げてから System.arraycopy
2次元配列を行ごと独立させてコピーする Arrays.stream(grid).map(int[]::clone).toArray(int[][]::new)
オブジェクト配列を要素ごと独立させてコピーする 要素ごとに new して入れ直す

4つの方法はどれも「浅いコピー」です。 int や double などの基本型の配列ならそのままで独立した写しになりますが、2次元配列やオブジェクト配列では、後半で説明する深いコピーが必要になる場合があります。

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

(記載例)出典:株式会社RJC「Java配列コピー4つの方法|copyOf・arraycopy・clone」

代入はコピーではないことを確認する

最初に押さえておきたいのは、int[] same = src; という代入は配列のコピーではないという点です。配列の変数には配列そのものではなく「配列の場所(参照)」が入っているので、代入すると同じ配列を指す変数が2つになるだけです。

import java.util.Arrays;

public class Assign {
    public static void main(String[] args) {
        int[] src = {10, 20, 30};

        int[] same = src;          // 代入:同じ配列を指す変数が2つになるだけ
        int[] copy = src.clone();  // コピー:中身が同じ別の配列ができる

        same[0] = -1;
        copy[1] = 999;

        System.out.println("src  : " + Arrays.toString(src));
        System.out.println("same : " + Arrays.toString(same));
        System.out.println("copy : " + Arrays.toString(copy));
        System.out.println("same == src ? " + (same == src));
        System.out.println("copy == src ? " + (copy == src));
    }
}

実行結果(OpenJDK 21.0.10、java Assign.java):

src  : [-1, 20, 30]
same : [-1, 20, 30]
copy : [10, 999, 30]
same == src ? true
copy == src ? false

same[0] を書き換えると src[0] も -1 になりました。一方、clone() で作った copy を書き換えても src は変わっていません。== で比べると、代入は true(同じ配列)、コピーは false(別の配列) になります。

flowchart LR
    src[変数 src] --> A["配列 10, 20, 30"]
    same[変数 same] --> A
    copy[変数 copy] --> B["別の配列 10, 20, 30"]

メソッドに配列を渡すときも同じ仕組みです。受け取った側で要素を書き換えると、呼び出し元の配列も変わります。元の配列を残したいときは、渡す前か受け取った直後にコピーを作ります。

4つの方法を実行結果で比べる

同じ配列を4つの方法でコピーし、結果を並べます。

import java.util.Arrays;

public class CopyFour {
    public static void main(String[] args) {
        int[] src = {10, 20, 30, 40, 50};

        // 1. clone:配列全体を同じ長さでコピー
        int[] a = src.clone();

        // 2. Arrays.copyOf:先頭から、指定した長さでコピー
        int[] b1 = Arrays.copyOf(src, src.length);
        int[] b2 = Arrays.copyOf(src, 3);     // 短くする
        int[] b3 = Arrays.copyOf(src, 7);     // 長くする(余りは 0)

        // 3. Arrays.copyOfRange:開始位置〜終了位置の手前までをコピー
        int[] c1 = Arrays.copyOfRange(src, 1, 4);
        int[] c2 = Arrays.copyOfRange(src, 3, 8);   // 終了位置が長さを超えると 0 で埋まる

        // 4. System.arraycopy:用意済みの配列の、指定した位置へコピー
        int[] d1 = new int[src.length];
        System.arraycopy(src, 0, d1, 0, src.length);
        int[] d2 = new int[8];
        System.arraycopy(src, 1, d2, 2, 3);   // src[1]から3個を、d2[2]以降へ

        System.out.println("clone()              : " + Arrays.toString(a));
        System.out.println("copyOf(src, 5)       : " + Arrays.toString(b1));
        System.out.println("copyOf(src, 3)       : " + Arrays.toString(b2));
        System.out.println("copyOf(src, 7)       : " + Arrays.toString(b3));
        System.out.println("copyOfRange(src,1,4) : " + Arrays.toString(c1));
        System.out.println("copyOfRange(src,3,8) : " + Arrays.toString(c2));
        System.out.println("arraycopy 全体       : " + Arrays.toString(d1));
        System.out.println("arraycopy 一部       : " + Arrays.toString(d2));
    }
}

実行結果:

clone()              : [10, 20, 30, 40, 50]
copyOf(src, 5)       : [10, 20, 30, 40, 50]
copyOf(src, 3)       : [10, 20, 30]
copyOf(src, 7)       : [10, 20, 30, 40, 50, 0, 0]
copyOfRange(src,1,4) : [20, 30, 40]
copyOfRange(src,3,8) : [40, 50, 0, 0, 0]
arraycopy 全体       : [10, 20, 30, 40, 50]
arraycopy 一部       : [0, 0, 20, 30, 40, 0, 0, 0]

cloneで全体をコピーする

clone() は引数がなく、元と同じ長さ・同じ型の配列を1行で作れます。 配列の clone() は戻り値の型が元の配列と同じなので、キャストも例外処理も必要ありません。全体をそのまま写したいときに、いちばん短く書ける方法です。

Arrays.copyOfで長さを変えてコピーする

Arrays.copyOf(src, 長さ) は、先頭から指定した長さぶんをコピーします。元より短い長さを指定すると後ろが切り捨てられ、長い長さを指定すると余った部分が初期値で埋まります。使うには import java.util.Arrays; が必要です。

余った部分に入る値は、配列の型によって決まります。

import java.util.Arrays;

public class Padding {
    public static void main(String[] args) {
        int[] nums = {1, 2};
        double[] rates = {1.5};
        boolean[] flags = {true};
        char[] chars = {'A'};
        String[] names = {"佐藤", "鈴木"};

        System.out.println("int[]     : " + Arrays.toString(Arrays.copyOf(nums, 4)));
        System.out.println("double[]  : " + Arrays.toString(Arrays.copyOf(rates, 3)));
        System.out.println("boolean[] : " + Arrays.toString(Arrays.copyOf(flags, 3)));
        System.out.println("char[]    : " + (int) Arrays.copyOf(chars, 2)[1] + "(文字コード)");
        System.out.println("String[]  : " + Arrays.toString(Arrays.copyOf(names, 4)));
    }
}

実行結果:

int[]     : [1, 2, 0, 0]
double[]  : [1.5, 0.0, 0.0]
boolean[] : [true, false, false]
char[]    : 0(文字コード)
String[]  : [佐藤, 鈴木, null, null]
配列の型 余った部分に入る値
int[]、long[]、short[]、byte[] 0
double[]、float[] 0.0
boolean[] false
char[] 文字コード0の文字
String[] などの参照型 null

参照型の配列を広げると、余った部分は null になります。 広げた直後に要素のメソッドを呼ぶと NullPointerException になるので、値を入れてから使います。

Arrays.copyOfRangeで一部分を取り出す

Arrays.copyOfRange(src, 開始, 終了) は、開始位置から「終了位置の手前」までをコピーします。copyOfRange(src, 1, 4) なら、インデックス1・2・3の3件です。取り出される個数は「終了 − 開始」になります。

終了位置は元の配列の長さを超えてもかまいません。実行結果の copyOfRange(src,3,8) のように、超えた部分は0で埋まります。Java 21 の公式APIドキュメント(Arrays クラス)を開いて確認したところ、終了位置は配列の外にあってもよいこと、開始位置が終了位置より大きいと IllegalArgumentException になることが書かれています。

System.arraycopyで既存の配列へコピーする

System.arraycopy は引数が5つあります。ほかの3つと違い、コピー先の配列を自分で用意し、戻り値はありません。

System.arraycopy(src, 1, dest, 2, 3);
//               ①   ②   ③    ④  ⑤
番号 引数 意味 上の例
① src コピー元の配列 src
② srcPos コピー元の開始位置 1(src[1] から)
③ dest コピー先の配列 dest
④ destPos コピー先の開始位置 2(dest[2] から)
⑤ length コピーする個数 3個

実行結果の「arraycopy 一部」では、src[1]〜src[3] の3個(20, 30, 40)が、長さ8の配列のインデックス2〜4に入っています。コピー先の途中の位置を指定できるのは System.arraycopy だけなので、配列の結合や、要素をずらす処理に向いています。import は不要です。

どの方法を選ぶかの判断表

条件 選ぶ方法
全体を同じ長さで写したい clone()
先頭からN件ほしい、または長さを広げたい Arrays.copyOf
途中の範囲だけほしい Arrays.copyOfRange
すでにある配列の途中へ書き込みたい System.arraycopy
複数の配列を1つにまとめたい Arrays.copyOf と System.arraycopy の組み合わせ
条件に合う要素だけを写したい for文、または Stream の filter

for文で1つずつ代入する方法でもコピーはできます。要素ごとに値を変換したり、条件で選んだりするときは for文が素直です。単純に写すだけなら、4つの方法のほうが短く、範囲の書き間違いも減らせます。処理時間の比較は、今回は安定した計測ができなかったため載せていません(未実行扱い)。

コピーで出会う例外とメッセージ

引数を取り違えたときに、どのような例外とメッセージが出るかを実行して確認しました。

import java.util.Arrays;

public class Errs {
    static void test(String label, Runnable r) {
        try {
            r.run();
            System.out.println(label + " -> 例外なし");
        } catch (RuntimeException e) {
            System.out.println(label + "\n  -> " + e.getClass().getName() + ": " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        int[] src = {10, 20, 30, 40, 50};

        test("arraycopy:コピー先が短い", () -> System.arraycopy(src, 0, new int[3], 0, 5));
        test("arraycopy:型が違う", () -> System.arraycopy(src, 0, new long[5], 0, 5));
        test("arraycopy:コピー先が null", () -> System.arraycopy(src, 0, null, 0, 5));
        test("copyOf:長さが負の数", () -> Arrays.copyOf(src, -1));
        test("copyOfRange:開始 > 終了", () -> Arrays.copyOfRange(src, 4, 2));
        test("copyOfRange:開始が長さを超える", () -> Arrays.copyOfRange(src, 6, 8));
    }
}

実行結果:

arraycopy:コピー先が短い
  -> java.lang.ArrayIndexOutOfBoundsException: arraycopy: last destination index 5 out of bounds for int[3]
arraycopy:型が違う
  -> java.lang.ArrayStoreException: arraycopy: type mismatch: can not copy int[] into long[]
arraycopy:コピー先が null
  -> java.lang.NullPointerException: null
copyOf:長さが負の数
  -> java.lang.NegativeArraySizeException: -1
copyOfRange:開始 > 終了
  -> java.lang.IllegalArgumentException: 4 > 2
copyOfRange:開始が長さを超える
  -> java.lang.ArrayIndexOutOfBoundsException: arraycopy: length -1 is negative
例外 起きる状況 直し方
ArrayIndexOutOfBoundsException arraycopy でコピー先が足りない コピー先を「先の開始位置 + 個数」以上の長さで用意する
ArrayStoreException arraycopy で元と先の型が違う 同じ型の配列を用意する
NullPointerException 元またはコピー先が null 配列を new で作ってから渡す
NegativeArraySizeException copyOf に負の長さを渡した 長さの計算式を見直す
IllegalArgumentException copyOfRange で開始が終了より大きい 開始と終了の順番を見直す

last destination index 5 out of bounds for int[3] は「コピー先の最後の位置が5になるが、配列の長さは3」という意味です。メッセージに数字が入っているので、どの引数を直せばよいかが読み取れます。

浅いコピーでコピー元まで書き換わる例

ここからが、配列コピーでいちばんつまずきやすい部分です。4つの方法は、配列の「1段目」だけを新しく作ります。 要素が配列やオブジェクトの場合、写しの要素は元と同じものを指したままになります。これを浅いコピー(シャローコピー)と呼びます。

import java.util.Arrays;

public class ShallowDemo {
    static class Item {
        String name;
        int stock;
        Item(String name, int stock) { this.name = name; this.stock = stock; }
        @Override public String toString() { return name + ":" + stock; }
    }

    public static void main(String[] args) {
        // (1) 2次元配列を clone でコピーして、写しを書き換える
        int[][] grid = { {1, 2, 3}, {4, 5, 6} };
        int[][] gridCopy = grid.clone();
        gridCopy[0][0] = 99;
        System.out.println("(1) 元  : " + Arrays.deepToString(grid));
        System.out.println("    写し: " + Arrays.deepToString(gridCopy));
        System.out.println("    gridCopy == grid ?       " + (gridCopy == grid));
        System.out.println("    gridCopy[0] == grid[0] ? " + (gridCopy[0] == grid[0]));

        // (2) オブジェクト配列を Arrays.copyOf でコピーして、写しの要素の中身を書き換える
        Item[] items = { new Item("ペン", 10), new Item("ノート", 5) };
        Item[] itemsCopy = Arrays.copyOf(items, items.length);
        itemsCopy[0].stock = 0;
        System.out.println("(2) 元  : " + Arrays.toString(items));
        System.out.println("    写し: " + Arrays.toString(itemsCopy));

        // (3) 写しの要素を「別のオブジェクトに差し替える」場合は、元に影響しない
        itemsCopy[1] = new Item("消しゴム", 3);
        System.out.println("(3) 元  : " + Arrays.toString(items));
        System.out.println("    写し: " + Arrays.toString(itemsCopy));
    }
}

実行結果:

(1) 元  : [[99, 2, 3], [4, 5, 6]]
    写し: [[99, 2, 3], [4, 5, 6]]
    gridCopy == grid ?       false
    gridCopy[0] == grid[0] ? true
(2) 元  : [ペン:0, ノート:5]
    写し: [ペン:0, ノート:5]
(3) 元  : [ペン:0, ノート:5]
    写し: [ペン:0, 消しゴム:3]

(1) では、写しの gridCopy[0][0] を99に変えただけなのに、元の grid[0][0] も99になりました。 gridCopy == grid は false なので外側の配列は別物ですが、gridCopy[0] == grid[0] は true で、行の配列は同じものを指しています。

(2) も同じ仕組みです。写しの itemsCopy[0].stock を0にすると、元の items[0] の在庫も0になりました。配列の中に入っているのは Item そのものではなく Item の場所なので、コピーしても同じ Item を指します。

(3) は、写しの要素を別のオブジェクトに差し替えた場合です。このときは元の配列に影響しません。「要素の中身を書き換える」と元に伝わり、「要素を差し替える」と伝わらない、と整理すると覚えやすくなります。

flowchart LR
    G[grid] --> R0["行0: 1, 2, 3"]
    G --> R1["行1: 4, 5, 6"]
    C[gridCopy] --> R0
    C --> R1

4つの方法はどれも浅いコピーになる

「clone() 以外なら深くコピーされるのでは」と思うかもしれませんが、4つとも同じ動きです。

import java.util.Arrays;

public class AllShallow {
    public static void main(String[] args) {
        int[][] grid = { {1, 2}, {3, 4} };

        int[][] a = grid.clone();
        int[][] b = Arrays.copyOf(grid, grid.length);
        int[][] c = Arrays.copyOfRange(grid, 0, grid.length);
        int[][] d = new int[grid.length][];
        System.arraycopy(grid, 0, d, 0, grid.length);

        a[0][0] = 10;   // clone の写しを書き換え
        b[0][1] = 20;   // copyOf の写しを書き換え
        c[1][0] = 30;   // copyOfRange の写しを書き換え
        d[1][1] = 40;   // arraycopy の写しを書き換え

        System.out.println("元の配列: " + Arrays.deepToString(grid));
    }
}

実行結果:

元の配列: [[10, 20], [30, 40]]

4つの写しをそれぞれ1か所ずつ書き換えたところ、元の配列の4か所すべてが変わりました。 方法を変えても、浅いコピーであることは変わりません。

配列の種類 浅いコピーで独立するか 理由
int[] などの基本型 独立する 要素に値そのものが入っている
String[] 独立して使える String は中身を書き換えられない
record や不変クラスの配列 独立して使える フィールドを書き換えられない
int[][] などの多次元配列 行を共有する 要素が配列の参照
フィールドを書き換えられるクラスの配列 オブジェクトを共有する 要素がオブジェクトの参照

深いコピーの書き方

要素まで含めて独立した写しを作ることを、深いコピー(ディープコピー)と呼びます。Javaには配列を自動で深くコピーする標準メソッドがないので、要素ごとにコピーを作って入れ直します。

import java.util.Arrays;

public class DeepCopy {
    static class Item {
        String name;
        int stock;
        Item(String name, int stock) { this.name = name; this.stock = stock; }
        Item(Item other) { this(other.name, other.stock); }   // コピーコンストラクタ
        @Override public String toString() { return name + ":" + stock; }
    }

    public static void main(String[] args) {
        // (1) 2次元配列:行ごとに clone する
        int[][] grid = { {1, 2, 3}, {4, 5, 6} };
        int[][] deep = new int[grid.length][];
        for (int i = 0; i < grid.length; i++) {
            deep[i] = grid[i].clone();
        }
        deep[0][0] = 99;
        System.out.println("(1) 元  : " + Arrays.deepToString(grid));
        System.out.println("    写し: " + Arrays.deepToString(deep));

        // (2) 2次元配列:Stream で1行にまとめる
        int[][] deep2 = Arrays.stream(grid).map(int[]::clone).toArray(int[][]::new);
        deep2[1][2] = -1;
        System.out.println("(2) 元  : " + Arrays.deepToString(grid));
        System.out.println("    写し: " + Arrays.deepToString(deep2));

        // (3) オブジェクト配列:要素ごとに新しいオブジェクトを作る
        Item[] items = { new Item("ペン", 10), new Item("ノート", 5) };
        Item[] deepItems = new Item[items.length];
        for (int i = 0; i < items.length; i++) {
            deepItems[i] = new Item(items[i]);
        }
        deepItems[0].stock = 0;
        System.out.println("(3) 元  : " + Arrays.toString(items));
        System.out.println("    写し: " + Arrays.toString(deepItems));

        // (4) オブジェクト配列:Stream で1行にまとめる
        Item[] deepItems2 = Arrays.stream(items).map(Item::new).toArray(Item[]::new);
        deepItems2[1].stock = 77;
        System.out.println("(4) 元  : " + Arrays.toString(items));
        System.out.println("    写し: " + Arrays.toString(deepItems2));
    }
}

実行結果:

(1) 元  : [[1, 2, 3], [4, 5, 6]]
    写し: [[99, 2, 3], [4, 5, 6]]
(2) 元  : [[1, 2, 3], [4, 5, 6]]
    写し: [[1, 2, 3], [4, 5, -1]]
(3) 元  : [ペン:10, ノート:5]
    写し: [ペン:0, ノート:5]
(4) 元  : [ペン:10, ノート:5]
    写し: [ペン:10, ノート:77]

4つとも、写しを書き換えても元の配列はそのままです。

対象 ループで書く Stream で書く
2次元配列 行ごとに grid[i].clone() Arrays.stream(grid).map(int[]::clone).toArray(int[][]::new)
オブジェクト配列 要素ごとに new Item(items[i]) Arrays.stream(items).map(Item::new).toArray(Item[]::new)

オブジェクト配列では、クラスに「同じ型を受け取って中身を写すコンストラクタ」(コピーコンストラクタ)を用意しておくと、深いコピーを短く書けます。上の例では Item(Item other) がそれに当たります。フィールドの中にさらに配列やオブジェクトがある場合は、コピーコンストラクタの中でそれらもコピーします。

浅いコピーで十分な場合

深いコピーは、いつも必要なわけではありません。要素の中身を書き換えられない型なら、浅いコピーのままで困りません。

import java.util.Arrays;

public class Immutable {
    record Point(int x, int y) {}

    public static void main(String[] args) {
        // String は中身を書き換えられないので、浅いコピーで十分
        String[] names = {"佐藤", "鈴木"};
        String[] namesCopy = names.clone();
        namesCopy[0] = namesCopy[0] + "さん";
        System.out.println("元  : " + Arrays.toString(names));
        System.out.println("写し: " + Arrays.toString(namesCopy));

        // record もフィールドを書き換えられないので、浅いコピーで十分
        Point[] points = { new Point(1, 2) };
        Point[] pointsCopy = points.clone();
        pointsCopy[0] = new Point(9, 9);
        System.out.println("元  : " + Arrays.toString(points));
        System.out.println("写し: " + Arrays.toString(pointsCopy));
    }
}

実行結果:

元  : [佐藤, 鈴木]
写し: [佐藤さん, 鈴木]
元  : [Point[x=1, y=2]]
写し: [Point[x=9, y=9]]

String は連結しても新しい String ができるだけで、元の文字列は変わりません。record もフィールドを書き換えられないので、写し側でできるのは要素の差し替えだけです。配列に入れるクラスを書き換えられない作りにしておくと、深いコピーを考える場面そのものが減ります。

2つの配列を結合する

Javaの配列には結合用のメソッドがないので、コピーを組み合わせて作ります。arraycopy版とStream版の両方を載せます。

import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class Concat {
    // arraycopy 版:int[] 同士を結合する
    static int[] concat(int[] a, int[] b) {
        int[] result = Arrays.copyOf(a, a.length + b.length);   // a をコピーしつつ長さを広げる
        System.arraycopy(b, 0, result, a.length, b.length);     // 後ろに b を入れる
        return result;
    }

    // arraycopy 版:String[] などの参照型の配列を結合する
    static <T> T[] concat(T[] a, T[] b) {
        T[] result = Arrays.copyOf(a, a.length + b.length);
        System.arraycopy(b, 0, result, a.length, b.length);
        return result;
    }

    // arraycopy 版:3つ以上の int[] をまとめて結合する
    static int[] concatAll(int[]... arrays) {
        int total = 0;
        for (int[] arr : arrays) {
            total += arr.length;
        }
        int[] result = new int[total];
        int pos = 0;
        for (int[] arr : arrays) {
            System.arraycopy(arr, 0, result, pos, arr.length);
            pos += arr.length;
        }
        return result;
    }

    public static void main(String[] args) {
        int[] a = {1, 2, 3};
        int[] b = {4, 5};
        String[] x = {"A", "B"};
        String[] y = {"C"};

        System.out.println("arraycopy版 int[]    : " + Arrays.toString(concat(a, b)));
        System.out.println("arraycopy版 String[] : " + Arrays.toString(concat(x, y)));
        System.out.println("arraycopy版 3つ以上  : " + Arrays.toString(concatAll(a, b, new int[]{6})));

        // Stream 版
        int[] s1 = IntStream.concat(Arrays.stream(a), Arrays.stream(b)).toArray();
        String[] s2 = Stream.concat(Arrays.stream(x), Arrays.stream(y)).toArray(String[]::new);
        int[] s3 = Stream.of(a, b, new int[]{6}).flatMapToInt(Arrays::stream).toArray();

        System.out.println("Stream版 int[]       : " + Arrays.toString(s1));
        System.out.println("Stream版 String[]    : " + Arrays.toString(s2));
        System.out.println("Stream版 3つ以上     : " + Arrays.toString(s3));
        System.out.println("元の a と b          : " + Arrays.toString(a) + " " + Arrays.toString(b));
    }
}

実行結果:

arraycopy版 int[]    : [1, 2, 3, 4, 5]
arraycopy版 String[] : [A, B, C]
arraycopy版 3つ以上  : [1, 2, 3, 4, 5, 6]
Stream版 int[]       : [1, 2, 3, 4, 5]
Stream版 String[]    : [A, B, C]
Stream版 3つ以上     : [1, 2, 3, 4, 5, 6]
元の a と b          : [1, 2, 3] [4, 5]

arraycopy版は、Arrays.copyOf で1つ目の配列を写しながら長さを広げ、空いた後ろの部分へ System.arraycopy で2つ目を入れる、という2行でできています。元の a と b は変わりません。

項目 arraycopy版 Stream版
行数 2行(メソッドにまとめると呼び出しは1行) 1行
基本型の配列 型ごとにメソッドが必要 IntStream.concat など型ごとのクラスを使う
参照型の配列 ジェネリクスで1つにまとめられる Stream.concat と toArray(String[]::new)
3つ以上の結合 合計の長さを数えてから順に入れる Stream.of(...) と flatMapToInt
向いている場面 回数の多い処理、位置を細かく決めたい処理 結合のあとに絞り込みや並び替えを続ける処理

結合した配列の要素も浅いコピーです。オブジェクト配列を結合した場合、結果の配列と元の配列は同じオブジェクトを指します。

要素の追加・削除・差し込みもコピーで書ける

配列は作ったあとに長さを変えられないので、要素を増やしたり減らしたりするときも、新しい配列へのコピーで表します。

import java.util.Arrays;

public class AddRemove {
    public static void main(String[] args) {
        int[] src = {10, 20, 30, 40, 50};

        // 末尾に1つ追加する
        int[] added = Arrays.copyOf(src, src.length + 1);
        added[src.length] = 60;
        System.out.println("末尾に追加        : " + Arrays.toString(added));

        // インデックス2の要素を取り除く
        int index = 2;
        int[] removed = new int[src.length - 1];
        System.arraycopy(src, 0, removed, 0, index);
        System.arraycopy(src, index + 1, removed, index, src.length - index - 1);
        System.out.println("index 2 を取り除く: " + Arrays.toString(removed));

        // インデックス2の位置に 25 を差し込む
        int[] inserted = new int[src.length + 1];
        System.arraycopy(src, 0, inserted, 0, index);
        inserted[index] = 25;
        System.arraycopy(src, index, inserted, index + 1, src.length - index);
        System.out.println("index 2 に差し込む: " + Arrays.toString(inserted));

        // 同じ配列の中で、要素を1つ後ろへずらす
        int[] shift = {1, 2, 3, 4, 5};
        System.arraycopy(shift, 0, shift, 1, 4);
        System.out.println("同じ配列でずらす  : " + Arrays.toString(shift));

        // 先頭3件、末尾2件を取り出す
        System.out.println("先頭3件           : " + Arrays.toString(Arrays.copyOf(src, 3)));
        System.out.println("末尾2件           : " + Arrays.toString(Arrays.copyOfRange(src, src.length - 2, src.length)));
    }
}

実行結果:

末尾に追加        : [10, 20, 30, 40, 50, 60]
index 2 を取り除く: [10, 20, 40, 50]
index 2 に差し込む: [10, 20, 25, 30, 40, 50]
同じ配列でずらす  : [1, 1, 2, 3, 4]
先頭3件           : [10, 20, 30]
末尾2件           : [40, 50]

System.arraycopy は、コピー元とコピー先に同じ配列を指定できます。「同じ配列でずらす」の結果のように、範囲が重なっていても、いったん別の場所に写してからコピーしたのと同じ結果になります。

追加や削除を何度も行うなら、配列より ArrayList のほうが向いています。 配列と List の間のコピーは次のように書けます。

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

public class ToList {
    public static void main(String[] args) {
        String[] names = {"佐藤", "鈴木"};

        // 配列 → 追加や削除ができる List へコピー
        List<String> list = new ArrayList<>(Arrays.asList(names));
        list.add("高橋");

        // List → 配列へコピー
        String[] back = list.toArray(new String[0]);

        System.out.println("元の配列 : " + Arrays.toString(names));
        System.out.println("List     : " + list);
        System.out.println("新しい配列: " + Arrays.toString(back));
    }
}

実行結果:

元の配列 : [佐藤, 鈴木]
List     : [佐藤, 鈴木, 高橋]
新しい配列: [佐藤, 鈴木, 高橋]

コピーできたかを確かめる方法

コピーの結果を確認するときは、比べ方によって意味が変わります。

import java.util.Arrays;

public class Compare {
    public static void main(String[] args) {
        int[] src = {1, 2, 3};
        int[] copy = src.clone();
        System.out.println("copy == src           : " + (copy == src));
        System.out.println("copy.equals(src)      : " + copy.equals(src));
        System.out.println("Arrays.equals         : " + Arrays.equals(copy, src));

        int[][] grid = { {1, 2}, {3, 4} };
        int[][] deep = Arrays.stream(grid).map(int[]::clone).toArray(int[][]::new);
        System.out.println("2次元 Arrays.equals     : " + Arrays.equals(deep, grid));
        System.out.println("2次元 Arrays.deepEquals : " + Arrays.deepEquals(deep, grid));
    }
}

実行結果:

copy == src           : false
copy.equals(src)      : false
Arrays.equals         : true
2次元 Arrays.equals     : false
2次元 Arrays.deepEquals : true
比べ方 何を比べるか 使いどころ
copy == src 同じ配列かどうか 別の配列になっているかの確認(false ならコピーできている)
copy.equals(src) == と同じ 配列の中身の比較には使わない
Arrays.equals(copy, src) 1次元の中身が同じか 1次元配列の内容確認
Arrays.deepEquals(copy, src) 入れ子の中身まで同じか 多次元配列の内容確認

「== が false」かつ「Arrays.equals が true」なら、中身が同じ別の配列ができています。 深いコピーにした2次元配列は行の配列が別物になるので、Arrays.equals では false、Arrays.deepEquals で true になります。中身を目で見たいときは、1次元は Arrays.toString、多次元は Arrays.deepToString を使います。

つまずきやすい点と直し方

状況 直す前 直した後
写しを変えたら元も変わった(1次元) int[] copy = src; int[] copy = src.clone();
写しを変えたら元も変わった(2次元) int[][] copy = grid.clone(); Arrays.stream(grid).map(int[]::clone).toArray(int[][]::new)
範囲の最後の1件が入らない Arrays.copyOfRange(src, 1, 3) で1〜3のつもり Arrays.copyOfRange(src, 1, 4)(終了はその手前まで)
arraycopy で例外になる コピー先を new int[3] にして5個コピー コピー先を必要な長さで用意する
広げた配列で NullPointerException Arrays.copyOf(names, 4) の直後に names[3].length() 値を入れてから使う
arraycopy の結果を変数で受けようとした int[] d = System.arraycopy(...) 先に int[] d = new int[n]; を作る
中身が同じなのに false になる copy.equals(src) Arrays.equals(copy, src)

確認用のチェックリスト

  • 代入(=)ではなく、4つの方法のどれかでコピーしている
  • 要素が配列やオブジェクトのとき、浅いコピーでよいか、深いコピーが必要かを決めている
  • copyOfRange の終了位置が「その手前まで」であることを踏まえている
  • System.arraycopy のコピー先を、必要な長さで用意している
  • 参照型の配列を広げたあと、null の要素に値を入れている
  • コピーの確認に Arrays.equals または Arrays.deepEquals を使っている

よくある質問

Q. cloneとArrays.copyOfは、どちらを使えばよいですか? A. 全体を同じ長さで写すなら、どちらでも同じ結果になります。短く書けるのは clone() です。長さを変えたい場合や、先頭の一部だけがほしい場合は Arrays.copyOf を選びます。どちらも浅いコピーである点は同じです。

Q. System.arraycopyはどんなときに使いますか? A. コピー先の配列をすでに持っていて、その途中の位置へ書き込みたいときです。配列の結合、要素の差し込み、同じ配列の中で要素をずらす処理に向いています。新しい配列を作るだけなら、clone() や Arrays.copyOf のほうが引数が少なく読みやすくなります。

Q. 2次元配列をcloneしたのに、元の配列まで変わるのはなぜですか? A. clone() が新しく作るのは外側の配列だけで、中の行の配列は元と同じものを指しているためです。行ごとに clone() する深いコピーにすると、写しと元が独立します。この記事の「深いコピーの書き方」のコードをそのまま使えます。

Q. 配列を結合するメソッドはありますか? A. Javaの標準ライブラリには、配列同士を結合する専用のメソッドはありません。Arrays.copyOf で広げてから System.arraycopy で後ろに入れる方法か、IntStream.concat・Stream.concat を使う方法で結合します。どちらも元の配列は変わりません。

まとめ

  • 配列のコピーは clone()・Arrays.copyOf・Arrays.copyOfRange・System.arraycopy の4つから、長さと範囲で選ぶ
  • int[] b = a; は代入であり、コピーではない。片方を変えるともう片方も変わる
  • Arrays.copyOf は長さを変えられ、余った部分は 0・false・null で埋まる
  • copyOfRange の終了位置は「その手前まで」、System.arraycopy はコピー先を自分で用意する
  • 4つの方法はどれも浅いコピー。2次元配列やオブジェクト配列は、要素ごとにコピーして深いコピーにする
  • 配列の結合は Arrays.copyOf と System.arraycopy の2行、または IntStream.concat・Stream.concat の1行で書ける
  • 確認には ==(別の配列か)と Arrays.equals・Arrays.deepEquals(中身が同じか)を使い分ける

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

(記載例)出典:株式会社RJC「Java配列コピー4つの方法|copyOf・arraycopy・clone」

株式会社RJCでは、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で一緒に開発しながら、 成長を楽しみませんか?