Featured image of post Java の「参照渡し」は訳語の罠である

Java の「参照渡し」は訳語の罠である

「reference を渡す」を「参照渡し」と短絡した誤解が、なぜ根深いのかを整理する

要点

Java のメソッド引数はすべて 値渡し(pass by value) である。
オブジェクトを渡す場合も、渡されるのは 「参照の値(reference value)のコピー」 であり、参照そのものではない。

渡し方 渡されるもの 呼び出し元への影響 Java
値渡し 値のコピー なし int, boolean
参照値渡し 1 参照(アドレス)のコピー フィールド変更は反映、再代入は反映されない オブジェクト型
参照渡し 変数そのもの(エイリアス) 再代入も反映される 存在しない

誤解の出どころ

Java はすべて pass by value である(JLS 2 の定義)。オブジェクト型の場合、渡される「値」が reference value(参照値 = オブジェクトへのポインタ)である。つまり「reference を value で渡す」のであって、“pass by reference” という渡し方ではない。

しかし「reference を渡している」→「参照渡しだ」という短絡が起きやすく、日本語の技術書や Web 記事の多くが「オブジェクトは参照渡し」と記載してきた。学習者が最初に触れる情報がそもそも誤っていることが少なくない。

検証

「参照渡し」が正しいなら、メソッド内での再代入が呼び出し元に反映されるはずである。
以下の2つのコードで、それが起こらないことを確認する。

再代入が反映されない

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class MyObject {
    String value;
    MyObject(String value) { this.value = value; }
}

public class Test {
    public static void main(String[] args) {
        MyObject obj = new MyObject("初期値");
        System.out.println("Before: " + obj.value); // → 初期値

        changeReference(obj);
        System.out.println("After: " + obj.value);  // → 初期値(変更されない)
    }

    static void changeReference(MyObject obj) {
        // 参照値のコピーに新しいオブジェクトを代入しているだけ
        obj = new MyObject("変更後");
    }
}

null 代入が反映されない

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Test {
    public static void main(String[] args) {
        MyObject obj = new MyObject("初期値");
        clear(obj);
        System.out.println(obj == null ? "null" : "not null"); // → not null
    }

    static void clear(MyObject obj) {
        obj = null; // ローカルコピーを null にしているだけ
    }
}

いずれも、メソッド内での再代入が呼び出し元に反映されていない。
「参照渡し」であればこの挙動は説明できない。「参照値渡し」であれば自然に説明がつく。

内部動作 — C++ で理解する

Java の参照型変数は、内部的には C のポインタに近い概念である。
C++ のポインタ渡しとダブルポインタ渡しを比較すると、違いが明確になる。以下のコード例で使用するアドレス(100 番地、A 番地 等)は説明のための仮の値である。

ポインタ渡し(= Java の参照値渡しに相当)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int main() {
    MyObject* obj = new MyObject("初期値");
    // obj の値: 100(オブジェクトのアドレス)
    // obj 自体の格納先: A 番地
    changeReference(obj); // 100 がコピーされて渡される(A 番地は渡されない)
    // obj は依然として 100 番地を指す → "初期値"
}

void changeReference(MyObject* obj) {
    // obj の値: 100(コピー)
    // obj 自体の格納先: B 番地(main 側の A 番地とは別の場所)
    obj = new MyObject("変更後"); // B 番地の中身を 200 に変えただけ。A 番地には影響しない
}

ポインタの(アドレス 100)がコピーされて渡されるため、関数内でポインタ変数を差し替えても呼び出し元には影響しない。
これが Java の挙動と同じである。

ダブルポインタ渡し(= 真の参照渡しに相当)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int main() {
    MyObject* obj = new MyObject("初期値");
    // obj の値: 100(オブジェクトのアドレス)
    // obj 自体の格納先: A 番地
    changeReference(&obj); // A 番地を渡す(ポインタ変数の格納先そのもの)
    // obj は 200 番地を指すようになった → "変更後"
}

void changeReference(MyObject** obj) {
    // *obj で A 番地の中身にアクセスできる
    *obj = new MyObject("変更後"); // A 番地の中身を 200 に書き換える → main 側に反映
}

ポインタ変数そのもののアドレス(A 番地)を渡すことで、呼び出し元のポインタが指す先を変更できる。
これが C# の ref キーワードに相当する真の「参照渡し」である。

参照渡しが可能な言語の例

Java には参照渡しが存在しないが、言語によっては明示的にサポートしている。

言語 機構 備考
C# ref キーワード 呼び出し側・定義側の両方に ref が必要
C++ 参照型 &、ダブルポインタ ** 参照型は言語レベルのエイリアス
C ダブルポインタ ** 言語仕様上は値渡しのみだが、ポインタで模倣可能

C# の例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static void Swap(ref int x, ref int y) {
    int temp = x;
    x = y;
    y = temp;
}

static void Main() {
    int a = 10, b = 20;
    Swap(ref a, ref b);
    Console.WriteLine($"a={a}, b={b}"); // → a=20, b=10
}

まとめ

Java はすべて値渡しである。オブジェクト型の場合は「参照の値」が渡される。
「参照渡し」という表現は、「reference を渡している」を「参照渡し」と短絡したことに起因する誤解であり、言語仕様としては存在しない。

この区別を意識することで、メソッド設計において「戻り値で結果を返す」という Java らしいイディオムが自然に導かれる。
なお、Kotlin も同様に pass by value であり、参照渡しは存在しない。


  1. Java 公式リファレンスでは “reference value” と記載されている。本記事では「参照値渡し」と表記する。 ↩︎

  2. Java Language Specification。Java の言語仕様を定める公式文書。 ↩︎

Hugo で構築されています。
テーマ StackJimmy によって設計されています。