Javaのメソッド引数は、すべて「値渡し」です。基本型なら値そのもののコピーが、オブジェクトなら「オブジェクトの場所を指す参照」のコピーが渡されます。 後者は「参照の値渡し」と呼ばれることもあります。

このため、呼び出し元に変更が反映されるかどうかは、次の1行で判断できます。「引数が指すオブジェクトの中身を書き換えたら反映される。引数の変数に別のものを代入(再代入)しても反映されない。」 この記事では、int・String・Integer・配列・List・StringBuilder・自作クラスの7パターンを同じ形のコードで動かし、結果を一覧表にまとめます。swap が効かない例と代わりの書き方、変更できないリストを渡したときの例外まで、すべて実行結果つきです。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10(javac 21.0.10)。

引数の変更が呼び出し元に反映されるかの早見表

渡すもの メソッド内で再代入(x = ...) メソッド内で中身を変更 補足
int などの基本型 反映されない (中身という概念がない) 値のコピーが渡る
String 反映されない 変更するメソッドがない toUpperCase() などは新しい String を返す
Integer などのラッパー型 反映されない 変更するメソッドがない n++ は再代入と同じ
配列 反映されない 反映される(a[0] = 99) 要素の書き換えは呼び出し元にも見える
List(ArrayList) 反映されない 反映される(add・set・remove) List.of で作ったものは変更時に例外
StringBuilder 反映されない 反映される(append) 可変の文字列
自作クラス 反映されない 反映される(フィールドの書き換え) フィールドが final なら変更できない

判断のしかたは次の3つです。

確認すること 答え 結果
メソッド内でしているのは 引数 = 新しい値 か はい 呼び出し元は変わらない
引数.メソッド() や 引数[i] = ... で中身を書き換えているか はい 呼び出し元から見ても変わっている
その型に中身を書き換える手段があるか(String・Integer・List.of にはない) ない 変えた結果は戻り値で受け取る

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

(記載例)出典:株式会社RJC「Javaは値渡し?参照渡し?引数の変更が反映される条件」

値渡しと参照渡しの違い

言葉の意味を先にそろえておきます。

用語 意味 Javaでは
値渡し 引数の値をコピーして、メソッド側の新しい変数に入れる すべての引数がこれ
参照渡し 呼び出し元の変数そのものをメソッド側と共有する。メソッド内の代入が呼び出し元の変数を書き換える 用意されていない
参照の値渡し オブジェクトを指す参照(場所の情報)をコピーして渡す。値渡しの一種 オブジェクトを渡すときの動き

Java言語仕様(JLS 8.4.1)には、メソッドが呼び出されると、実引数の式の値が、新しく作られた引数用の変数を初期化する、という趣旨の記述があります。メソッドの引数は、呼び出すたびに作られる別の変数です。そこに入るのが「値のコピー」である、というのが値渡しの意味です。

オブジェクト型の変数に入っているのは、オブジェクトそのものではなく、オブジェクトの場所を指す参照です。メソッドに渡すと、その参照がコピーされます。

flowchart LR
    subgraph 呼び出し元
        L[変数 list]
    end
    subgraph メソッド
        P[引数 param]
    end
    L --> O[(ArrayList の実体)]
    P --> O

呼び出し元の list とメソッドの param は別の変数ですが、同じ実体を指しています。だから param.add("x") は呼び出し元からも見え、param = 別のリスト は param の矢印を付け替えるだけで list には影響しません。

7パターンを同じ形のコードで試す

それぞれの型について、「再代入するメソッド」と「中身を変更するメソッド」を用意して呼び出します。SevenPatterns.java として保存してください。

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

public class SevenPatterns {
    static class Counter {
        int count;
        Counter(int count) { this.count = count; }
        @Override public String toString() { return "Counter(" + count + ")"; }
    }

    // --- 再代入:引数の変数に別の値を入れる ---
    static void reassign(int x)           { x = 99; }
    static void reassign(String s)        { s = "changed"; }
    static void reassign(Integer n)       { n = 99; }
    static void reassign(int[] a)         { a = new int[] {9, 9, 9}; }
    static void reassign(List<String> l)  { l = new ArrayList<>(List.of("x")); }
    static void reassign(StringBuilder b) { b = new StringBuilder("changed"); }
    static void reassign(Counter c)       { c = new Counter(99); }

    // --- 中身の変更:引数が指すオブジェクトを書き換える ---
    static void mutate(String s)          { s.toUpperCase(); s.concat("!"); }   // 新しい String が返るだけ
    static void mutate(Integer n)         { n++; }                               // n = n + 1 と同じ(再代入)
    static void mutate(int[] a)           { a[0] = 99; }
    static void mutate(List<String> l)    { l.add("x"); }
    static void mutate(StringBuilder b)   { b.append("!"); }
    static void mutate(Counter c)         { c.count++; }

    public static void main(String[] args) {
        int x = 1;                       reassign(x);
        System.out.println("int            再代入後: " + x);

        String s = "hello";              reassign(s);
        System.out.println("String         再代入後: " + s);
        mutate(s);
        System.out.println("String         変更後  : " + s);

        Integer n = 1;                   reassign(n);
        System.out.println("Integer        再代入後: " + n);
        mutate(n);
        System.out.println("Integer        変更後  : " + n);

        int[] a = {1, 2, 3};             reassign(a);
        System.out.println("配列           再代入後: " + Arrays.toString(a));
        mutate(a);
        System.out.println("配列           変更後  : " + Arrays.toString(a));

        List<String> l = new ArrayList<>(List.of("a", "b"));
        reassign(l);
        System.out.println("List           再代入後: " + l);
        mutate(l);
        System.out.println("List           変更後  : " + l);

        StringBuilder b = new StringBuilder("hello");
        reassign(b);
        System.out.println("StringBuilder  再代入後: " + b);
        mutate(b);
        System.out.println("StringBuilder  変更後  : " + b);

        Counter c = new Counter(1);      reassign(c);
        System.out.println("自作クラス     再代入後: " + c);
        mutate(c);
        System.out.println("自作クラス     変更後  : " + c);
    }
}
javac -d out SevenPatterns.java
java -cp out SevenPatterns

実行結果:

int            再代入後: 1
String         再代入後: hello
String         変更後  : hello
Integer        再代入後: 1
Integer        変更後  : 1
配列           再代入後: [1, 2, 3]
配列           変更後  : [99, 2, 3]
List           再代入後: [a, b]
List           変更後  : [a, b, x]
StringBuilder  再代入後: hello
StringBuilder  変更後  : hello!
自作クラス     再代入後: Counter(1)
自作クラス     変更後  : Counter(2)

結果一覧表

型 再代入したあとの呼び出し元 中身を変更したあとの呼び出し元 反映されたか
int 1(そのまま) - 再代入:反映されない
String hello(そのまま) hello(そのまま) どちらも反映されない
Integer 1(そのまま) 1(そのまま) どちらも反映されない
配列 [1, 2, 3](そのまま) [99, 2, 3] 中身の変更だけ反映
List [a, b](そのまま) [a, b, x] 中身の変更だけ反映
StringBuilder hello(そのまま) hello! 中身の変更だけ反映
自作クラス Counter(1)(そのまま) Counter(2) 中身の変更だけ反映

7パターンすべてで、再代入は呼び出し元に反映されませんでした。 反映されたのは、配列・List・StringBuilder・自作クラスの「中身の変更」だけです。

StringとIntegerが「変わらない」理由

String と Integer もオブジェクトなので、渡されているのは参照のコピーです。配列や List と違う結果になるのは、渡し方が違うからではなく、中身を書き換えるメソッドを持っていない(不変である)からです。

  • s.toUpperCase() や s.concat("!") は、元の String を書き換えず、新しい String を作って返します。戻り値を受け取らなければ、何も起こらなかったのと同じです。
  • n++ は、Integer の中の数を増やしているように見えますが、実際は n = n + 1 の省略形です。新しい Integer を作って引数の変数に入れ直す、つまり再代入です。

String や Integer を「変えた結果」がほしいときは、戻り値で受け取ります。

static String upper(String s) { return s.toUpperCase(); }

String s = "hello";
s = upper(s);            // 戻り値を呼び出し元で代入し直す
System.out.println(s);   // HELLO

同じオブジェクトかどうかを == で確かめる

「参照のコピーが渡っている」ことは、==(同じオブジェクトかどうかの比較)で確かめられます。

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

public class Identity {
    static void work(List<String> param, List<String> original) {
        System.out.println("1. 受け取った直後 同じオブジェクトか: " + (param == original));
        param.add("b");                                   // 同じオブジェクトを書き換える
        param = new ArrayList<>(List.of("別のリスト"));   // 引数の変数を付け替える
        System.out.println("2. 再代入のあと   同じオブジェクトか: " + (param == original));
        param.add("c");                                   // 付け替えた先だけが変わる
        System.out.println("3. メソッド内の param = " + param);
    }
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("a"));
        work(list, list);
        System.out.println("4. 呼び出し元の list  = " + list);
    }
}

実行結果:

1. 受け取った直後 同じオブジェクトか: true
2. 再代入のあと   同じオブジェクトか: false
3. メソッド内の param = [別のリスト, c]
4. 呼び出し元の list  = [a, b]

流れを順に追うと次のようになります。

段階 メソッド内の param が指すもの 呼び出し元の list
受け取った直後 呼び出し元と同じリスト [a]
param.add("b") 同じリスト(中身が変わる) [a, b]
param = new ArrayList<>(...) 別のリストに付け替わる [a, b](変わらない)
param.add("c") 別のリスト [a, b](変わらない)

再代入より前の変更は反映され、再代入よりあとの変更は反映されません。 「途中までは反映されていたのに、あるところから反映されなくなった」というときは、メソッドの中で引数に = で代入している行がないかを探すと原因が見つかります。

null を代入した場合も同じです。メソッド内で sb = null; と書いても、呼び出し元の変数は元のオブジェクトを指したままです(実行結果は keep のままでした)。

swapが効かない例と、代わりの書き方

値渡しであることがはっきり表れるのが、2つの値を入れ替える swap です。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class Swap {
    // うまくいかない例:引数の変数どうしを入れ替えても呼び出し元は変わらない
    static void swap(int a, int b) { int t = a; a = b; b = t; }
    static void swap(Integer a, Integer b) { Integer t = a; a = b; b = t; }
    static void swap(String a, String b) { String t = a; a = b; b = t; }

    // 書き方1:配列の要素を入れ替える
    static void swap(int[] arr, int i, int j) { int t = arr[i]; arr[i] = arr[j]; arr[j] = t; }

    // 書き方2:結果を戻り値で返す(record でまとめる)
    record Pair(int first, int second) {}
    static Pair swapped(int a, int b) { return new Pair(b, a); }

    // 書き方3:可変オブジェクトの中身を入れ替える
    static class Box { int value; Box(int v) { value = v; } }
    static void swap(Box a, Box b) { int t = a.value; a.value = b.value; b.value = t; }

    // 数を増やすだけなら戻り値か AtomicInteger
    static int increment(int n) { return n + 1; }
    static void increment(AtomicInteger n) { n.incrementAndGet(); }

    public static void main(String[] args) {
        int x = 1, y = 2;
        swap(x, y);
        System.out.println("int の swap        : x=" + x + ", y=" + y);
        Integer p = 1, q = 2;
        swap(p, q);
        System.out.println("Integer の swap    : p=" + p + ", q=" + q);
        String s = "left", t = "right";
        swap(s, t);
        System.out.println("String の swap     : s=" + s + ", t=" + t);

        int[] arr = {1, 2};
        swap(arr, 0, 1);
        System.out.println("配列の要素を交換   : " + Arrays.toString(arr));

        Pair r = swapped(x, y);
        x = r.first(); y = r.second();
        System.out.println("戻り値で受け取る   : x=" + x + ", y=" + y);

        Box b1 = new Box(1), b2 = new Box(2);
        swap(b1, b2);
        System.out.println("可変オブジェクト   : b1=" + b1.value + ", b2=" + b2.value);

        List<String> list = new ArrayList<>(List.of("a", "b", "c"));
        Collections.swap(list, 0, 2);
        System.out.println("Collections.swap   : " + list);

        int n = 1;
        n = increment(n);
        AtomicInteger an = new AtomicInteger(1);
        increment(an);
        System.out.println("戻り値で増やす     : " + n + " / AtomicInteger: " + an);
    }
}

実行結果:

int の swap        : x=1, y=2
Integer の swap    : p=1, q=2
String の swap     : s=left, t=right
配列の要素を交換   : [2, 1]
戻り値で受け取る   : x=2, y=1
可変オブジェクト   : b1=2, b2=1
Collections.swap   : [c, b, a]
戻り値で増やす     : 2 / AtomicInteger: 2

最初の3つは、int でも Integer でも String でも入れ替わりませんでした。メソッド内の a と b は呼び出し元の変数のコピーなので、コピー同士を入れ替えても元の変数には届きません。基本型をラッパー型(Integer)に変えても結果は同じである点がポイントです。

代わりの書き方の選び方

やりたいこと 書き方 コード
配列の2要素を入れ替える 配列と添字を渡す swap(arr, 0, 1)
リストの2要素を入れ替える 標準の Collections.swap Collections.swap(list, 0, 2)
2つの値を入れ替えて返す record などにまとめて戻り値で返す Pair r = swapped(x, y);
オブジェクト同士の中身を入れ替える フィールドを入れ替える swap(b1, b2)
メソッドで数を増やす 戻り値で受け取る n = increment(n);
複数のスレッドやラムダ式から数を増やす AtomicInteger を渡す an.incrementAndGet()

迷ったら「戻り値で返す」を選ぶと、どの変数がいつ変わるかがコードから読み取りやすくなります。複数の値を返したいときは、record を使うと数行で入れ物を作れます。

List.ofを渡して変更するとUnsupportedOperationException

List は中身を変更できる、と説明しましたが、作り方によっては変更できないリストがあります。引数で受け取ったリストに add するメソッドに、List.of で作ったリストを渡した例です。

import java.util.List;

public class Trace {
    static void addItem(List<String> items) {
        items.add("x");
    }
    public static void main(String[] args) {
        List<String> tags = List.of("java", "sql");
        addItem(tags);
        System.out.println(tags);
    }
}

実行結果:

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
    at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:147)
    at Trace.addItem(Trace.java:5)
    at Trace.main(Trace.java:9)

コンパイルは通り、実行時に UnsupportedOperationException で止まります。メッセージは付かず、例外のクラス名だけが表示されます。スタックトレースに ImmutableCollections とあれば、変更できないリストに対して変更を試みた、と読み取れます。

List の API ドキュメント(Java SE 21)にも、List.of と List.copyOf が返すリストは変更不可で、要素の追加・削除・置き換えを行うメソッドを呼ぶと常に UnsupportedOperationException が投げられる、と書かれています。List.of は Java 9、List.copyOf は Java 10 で追加されたメソッドです。

リストの作り方ごとの結果

リストの作り方を変えて、同じメソッドに渡した結果です。

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

public class Unmodifiable {
    static void addItem(List<String> items) { items.add("x"); }
    static void setFirst(List<String> items) { items.set(0, "x"); }

    static void tryRun(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"));
        List<String> b = List.of("a", "b");
        List<String> c = Arrays.asList("a", "b");
        List<String> d = Collections.unmodifiableList(a);
        List<String> e = List.copyOf(a);
        List<String> f = a.stream().toList();

        tryRun("new ArrayList      add", () -> addItem(a));
        tryRun("List.of            add", () -> addItem(b));
        tryRun("List.of            set", () -> setFirst(b));
        tryRun("Arrays.asList      add", () -> addItem(c));
        tryRun("Arrays.asList      set", () -> setFirst(c));
        tryRun("unmodifiableList   add", () -> addItem(d));
        tryRun("List.copyOf        add", () -> addItem(e));
        tryRun("stream().toList()  add", () -> addItem(f));
        System.out.println("a = " + a + " / c = " + c);
        System.out.println("d(a のビュー)= " + d + " / e(コピー)= " + e);
    }
}

実行結果:

new ArrayList      add -> OK
List.of            add -> java.lang.UnsupportedOperationException
List.of            set -> java.lang.UnsupportedOperationException
Arrays.asList      add -> java.lang.UnsupportedOperationException
Arrays.asList      set -> OK
unmodifiableList   add -> java.lang.UnsupportedOperationException
List.copyOf        add -> java.lang.UnsupportedOperationException
stream().toList()  add -> java.lang.UnsupportedOperationException
a = [a, b, x] / c = [x, b]
d(a のビュー)= [a, b, x] / e(コピー)= [a, b]
作り方 add set 特徴
new ArrayList<>(...) できる できる 自由に変更できる
List.of(...) 例外 例外 変更できないリスト
Arrays.asList(...) 例外 できる 長さは固定、要素の置き換えはできる
Collections.unmodifiableList(a) 例外 例外 元のリスト a を映す窓。a が変わると表示も変わる
List.copyOf(a) 例外 例外 その時点の内容をコピーした、変更できないリスト
stream().toList() 例外 例外 変更できないリスト

最後の行に注目してください。unmodifiableList(a) で作った d は、あとから a に追加した x が見えています([a, b, x])。一方、List.copyOf(a) で作った e は、作った時点の [a, b] のままです。「窓」か「コピー」かで動きが変わります。

なお、変更できないリストでも、要素そのものが可変オブジェクトなら、その中身は書き換えられます。List.of(new StringBuilder("a")) を受け取って各要素に append("!") すると、呼び出し元から見ても [a!] になりました。守られているのは「リストに何が入っているか」であって、「入っているオブジェクトの中身」ではありません。

引数を書き換えないメソッドの書き方

「引数の中身を変更すると呼び出し元にも反映される」という性質は、便利な反面、呼び出した側が予想していない変化につながることもあります。受け取ったリストを並べ替えるメソッドで、直す前と後を比べます。

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

public class Defensive {
    // 直す前:受け取ったリストをそのまま並べ替える(呼び出し元も変わる)
    static String topBefore(List<Integer> scores) {
        Collections.sort(scores, Collections.reverseOrder());
        return "最高点 " + scores.get(0);
    }
    // 直した後:コピーしてから並べ替える(呼び出し元はそのまま)
    static String topAfter(List<Integer> scores) {
        List<Integer> copy = new ArrayList<>(scores);
        copy.sort(Collections.reverseOrder());
        return "最高点 " + copy.get(0);
    }
    // 追加した結果は新しいリストで返す
    static List<String> withItem(List<String> items, String item) {
        List<String> copy = new ArrayList<>(items);
        copy.add(item);
        return List.copyOf(copy);
    }
    static class Team {
        private final List<String> members;
        Team(List<String> members) { this.members = List.copyOf(members); }   // 受け取るときにコピー
        List<String> members() { return members; }                            // 変更できないリストを返す
    }
    public static void main(String[] args) {
        List<Integer> s1 = new ArrayList<>(List.of(70, 95, 80));
        System.out.println(topBefore(s1) + " / 呼び出し元: " + s1);
        List<Integer> s2 = new ArrayList<>(List.of(70, 95, 80));
        System.out.println(topAfter(s2) + " / 呼び出し元: " + s2);
        System.out.println(topAfter(List.of(70, 95, 80)) + " / List.of を渡しても動く");

        List<String> tags = List.of("java", "sql");
        List<String> more = withItem(tags, "git");
        System.out.println(tags + " -> " + more);

        List<String> src = new ArrayList<>(List.of("田中", "佐藤"));
        Team team = new Team(src);
        src.add("鈴木");                    // 渡したあとで元のリストを変えても
        System.out.println("Team: " + team.members() + " / src: " + src);
    }
}

実行結果:

最高点 95 / 呼び出し元: [95, 80, 70]
最高点 95 / 呼び出し元: [70, 95, 80]
最高点 95 / List.of を渡しても動く
[java, sql] -> [java, sql, git]
Team: [田中, 佐藤] / src: [田中, 佐藤, 鈴木]

「最高点を調べる」だけのつもりのメソッドが、直す前は呼び出し元のリストの並び順まで変えていました。メソッドの先頭でコピーを作ると、呼び出し元はそのままになり、List.of で作ったリストを渡されても例外になりません。

方針 書き方 効果
受け取った引数は書き換えない 先頭で new ArrayList<>(引数) してから加工する 呼び出し元に影響しない。変更できないリストも受け取れる
結果は戻り値で返す 新しいリストやオブジェクトを return する 何が変わるかがメソッドの形から読み取れる
フィールドに保持するときはコピーする コンストラクタで List.copyOf(引数) あとで呼び出し元がリストを変えても影響を受けない
書き換えるメソッドは名前で伝える sortInPlace、addTo... など 呼び出す側が変化を予想できる

コピーは1段だけ

new ArrayList<>(list) や配列の clone() は、入れ物だけをコピーします。要素がオブジェクトの場合、要素は元と同じものを指します(浅いコピー)。次の結果は同じプログラムで確かめたものです。

操作 結果 説明
int[] copy = a.clone(); のあと copy を 0 で埋める a=[1, 2, 3]、copy=[0, 0, 0] 基本型の配列は、コピーすれば独立する
int[][] shallow = m.clone(); のあと shallow[0][0] = 99 元の m も [[99, 2], [3, 4]] 2次元配列は内側の配列を共有している
record Order(String id, List<String> items) を渡して o.items().add("追加") Order[id=A-1, items=[ペン, 追加]] record でも、持っているリストは変更できる
String[] を渡して names[0] = names[0].toUpperCase() [TARO] String は不変でも、配列の要素の入れ替えは反映される
可変長引数 int... nums に配列を渡して nums[0] = 99 [99, 2] 可変長引数の正体は配列

record は「フィールドに再代入できない」だけで、フィールドが指すリストの中身までは守りません。中身も固定したいときは、コンストラクタで List.copyOf を通します。

拡張for文の変数も「コピー」

同じ考え方は、拡張 for 文にも当てはまります。

static void eachInt(int[] a) { for (int v : a) v = v * 2; }                        // 反映されない
static void eachIdx(int[] a) { for (int i = 0; i < a.length; i++) a[i] = a[i] * 2; }  // 反映される

{1, 2, 3} を渡した結果は、eachInt のあとが [1, 2, 3]、eachIdx のあとが [2, 4, 6] でした。for (int v : a) の v は要素の値をコピーした変数なので、v に代入しても配列は変わりません。配列の要素を書き換えるときは、添字を使って a[i] = ... と書きます。

finalを付けた引数とラムダ式

finalは再代入を止めるが、中身の変更は止めない

引数に final を付けると、メソッド内での再代入がコンパイルエラーになります。

import java.util.List;
public class FinalParam {
    static void add(final List<String> items) {
        items.add("x");                 // 中身の変更は final でも書ける
        items = List.of();              // 再代入はコンパイルエラー
    }
}
FinalParam.java:5: error: final parameter items may not be assigned
        items = List.of();              // 再代入はコンパイルエラー
        ^
1 error

日本語表示は「finalパラメータitemsに値を代入することはできません」です。エラーになったのは5行目だけで、4行目の items.add("x") は通っています。final が固定するのは「変数がどのオブジェクトを指すか」であり、オブジェクトの中身ではありません。 「反映されない再代入」をうっかり書かないための目印として使えます。

ラムダ式からローカル変数を書き換えたいとき

ラムダ式の中から、外側のローカル変数に代入することはできません。

public class Lambda {
    public static void main(String[] args) {
        int count = 0;
        Runnable r = () -> count++;
    }
}
Lambda.java:4: error: local variables referenced from a lambda expression must be final or effectively final
        Runnable r = () -> count++;
                           ^
1 error

ここでも「変数への再代入はできないが、オブジェクトの中身は変更できる」という同じ考え方が使えます。

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class Lambda2 {
    public static void main(String[] args) {
        int[] count = {0};
        AtomicInteger total = new AtomicInteger();
        List.of(3, 4, 5).forEach(v -> { count[0]++; total.addAndGet(v); });
        System.out.println(count[0] + " 件 / 合計 " + total);
    }
}
3 件 / 合計 12

要素が1つの配列や AtomicInteger を入れ物にすれば、ラムダ式の中から値を更新できます。

よくある質問

Q. Javaに参照渡しはありますか?

A. ありません。引数はすべて値渡しです。オブジェクトを渡したときに中身の変更が反映されるのは、参照のコピーが同じオブジェクトを指しているからで、呼び出し元の変数そのものを共有しているわけではありません。その証拠に、メソッド内で引数に再代入しても呼び出し元の変数は変わりません。

Q. Stringを引数で渡して変更したのに、呼び出し元が変わりません。

A. String は中身を書き換えるメソッドを持たない不変のクラスです。toUpperCase() や concat()、+ での連結は、新しい String を作って返します。s = upper(s); のように、戻り値を呼び出し元で受け取ってください。メソッド内で何度も文字を足したいなら、StringBuilder を渡して append する方法もあります。

Q. 配列やListを渡すと、コピーが作られて遅くなりませんか?

A. コピーされるのは参照だけで、配列やリストの要素はコピーされません。要素が100万個あっても、渡すときの手間は変わりません。呼び出し元と同じ実体を扱うことになるので、書き換えたくない場合は、メソッド側で new ArrayList<>(list) や clone() を使って明示的にコピーします。

Q. UnsupportedOperationExceptionが出たときは、どこを直せばよいですか?

A. 渡しているリストが List.of・List.copyOf・stream().toList()・Collections.unmodifiableList などで作られていないかを確認します。メソッド側で new ArrayList<>(引数) とコピーしてから変更するか、呼び出し側で new ArrayList<>(List.of(...)) として変更できるリストを渡すと解決します。

まとめ

  • Javaの引数はすべて値渡し。基本型は値のコピー、オブジェクトは参照のコピーが渡る(参照の値渡し)。
  • 引数への再代入(x = ...)は、7パターンすべてで呼び出し元に反映されない。
  • 配列・List・StringBuilder・自作クラスは、中身の変更(a[0] = ...、add、append、フィールドの書き換え)が反映される。
  • String と Integer は不変なので、変えた結果は戻り値で受け取る。n++ は再代入と同じ。
  • swap は引数の入れ替えでは実現できない。配列の要素を入れ替える、戻り値で返す、可変オブジェクトの中身を入れ替える、のいずれかで書く。
  • List.of などの変更できないリストに add すると UnsupportedOperationException。メソッドの先頭で new ArrayList<>(引数) とコピーすれば、どんなリストを渡されても動く。
  • final を付けた引数は再代入がコンパイルエラーになるが、中身の変更は止めない。

参考・出典

  • The Java Language Specification, Java SE 21 Edition「Chapter 8. Classes」(8.4.1 Formal Parameters)https://docs.oracle.com/javase/specs/jls/se21/html/jls-8.html
  • The Java Language Specification, Java SE 21 Edition「Chapter 15. Expressions」(15.12.4 Run-Time Evaluation of Method Invocation)https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html
  • Java SE 21 API ドキュメント「Interface List」(Unmodifiable Lists、List.of、List.copyOf)https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/List.html

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

(記載例)出典:株式会社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で一緒に開発しながら、 成長を楽しみませんか?