NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    【絵とコードで理解する】 Java の 参照の値渡し

    こんにちは。清水です。
    以前書いた自分の記事を久々に見てみたら長すぎて読む気が失せたので、
    この反省点を活かしてコンパクトにまとめていこうと思います。

    今回はちょっとニッチな Java の メソッドの仮引数への値の渡し方についてです。

    そもそも仮引数とは

    仮引数とは、プログラム中で関数やメソッドを呼び出して実行する際、関数側で値を受け取るために宣言された変数のこと。

    仮引数とは - 意味をわかりやすく - IT用語辞典 e-Words から引用

    ふむふむ。呼び出される側で受け取る変数のことですね。
    下のコードにおいての a とか b のことです。

       public int add(int a, int b) {
            return a + b;
        }
    

    これに対して呼び出す側に指定する引数を「実引数」といいます。
    次のコードでいう 1 とか 2 のことです。

       add(1, 2);
    

    プリミティブ型とオブジェクト型

    今回の話では知っておく必要があるのでざっと説明します。
    プリミティブ型 は 箱に値が入っています。
    オブジェクト型 は 箱に参照が入っています。
    コードと絵を見ていきましょう。

    int id = 1;
    Student student = new Student("Bob");
    

    厳密には違うかもしれませんが、だいたいこんなかんじです。
    オブジェクト型の本体は参照先をたどっていった先にあります。
    つまりオブジェクト型の変数には参照値 (=0x9001) が入っています。

    仮引数への値の渡し方

    ここからがメインの話です。
    Java でメソッド呼び出しをしたとき、引数に入れた値はどのように渡されるのでしょうか。 具体例を見ていきましょう。

    なお、この章以降は引数にオブジェクト型を指定した場合の話になります。

    先生は生徒をみんなに紹介します。
    生徒は自己紹介をします。


    ※イメージ(転校してきた生徒と先生)

    生徒は名前を持っていて自分の名前を使って自己紹介をします。

    // 生徒クラス
    @Getter
    @Setter
    public class Student {
        private String name;
    
        public Student(String name) {
            this.name = name;
        }
    
        // 自己紹介
        public void introduce() {
            System.out.println("【Student】I'm " + name + ".");
        }
    }
    

    先生は生徒を受け取り、名前を紹介します。

    // 先生クラス
    public class Teacher {
        // 生徒を紹介する
        public void introduce(Student student) {
            System.out.println("【Teacher】He is " + student.getName() + ".");
        }
    }
    

    さっそく紹介をしてもらいましょう!

    public static void main(String[] args) {
        Student bob = new Student("Bob"); // Bob
        Teacher teacher = new Teacher(); // 先生
        teacher.introduce(bob);  // 先生が Bob を紹介
        bob.introduce(); // Bob が自己紹介
    }
    

    結果は・・・?

    【Teacher】He is Bob.
    【Student】I'm Bob.

    想定通りですね。 先生はボブを紹介し、生徒は自己紹介しています。

    オブジェクトの持っている値を変更した場合

    ここで先生にアクションを付けます。
    先生は生徒を間違った名前でみんなに紹介します。(=生徒の名前を変える)

    // 先生クラス
    public class Teacher {
        public void introduceAfterChangeName(Student student) {
            student.setName("Mike"); // 生徒の名前を変える
            System.out.println("【Teacher】He is " + student.getName() + ".");
        }
    }
    

    さっきのように先生と生徒に紹介をしてもらいましょう!

    public static void main(String[] args) {
        Student bob = new Student("Bob"); // Bob
        Teacher teacher = new Teacher(); // 先生
        teacher.introduceAfterChangeName(bob); // 名前を変えて紹介
        bob.introduce();  // Bob(?) が自己紹介
    }
    

    結果は・・・?

    【Teacher】He is Mike.
    【Student】I'm Mike.

    先生によって生徒の名前が変更されてしまいました。
    そして生徒も空気を読んだのか、もはや自分自身を Mike と名乗っています。
    どうしてこうなったのでしょうか。

    先生が生徒の名前を変更したことよって生徒自身の名前も変更されてしまったんですね。
    同じインスタンス(ボブ)を見ているがゆえに名前を変えられてしまいました。

    オブジェクト自体を変更した場合

    次は、先生は生徒の名前を間違えてしまったのではなく
    先生は生徒を他の生徒と間違ってしまった場合どうなるでしょうか。(=生徒ごと変える)

    // 先生クラス
    public class Teacher {
        public void introduceAfterReplaceStudent(Student student) {
            student = new Student("Mike"); // 生徒ごと変える
            System.out.println("【Teacher】He is " + student.getName() + ".");
        }
    }
    

    同じく自己紹介をしてもらいましょう!

    public static void main(String[] args) {
        Student bob = new Student("Bob"); // Bob
        Teacher teacher = new Teacher(); // 先生
        teacher.introduceAfterReplaceStudent(bob); // 生徒ごと変えて紹介
        bob.introduce();  // Bob(?) が自己紹介
    }
    

    結果は・・・?

    【Teacher】He is Mike.
    【Student】I'm Bob.

    おや・・・?
    先生は Mike と言っているのに、生徒は Bob と名乗っています。
    どうなっているのでしょうか?
    実は仮引数には変数そのものが渡されるのではなく、仮引数が別で用意されて実引数と同じ参照値をみるだけなのです。

    変数の値はどのように変わっているのか

    変数の値が変わっていく様子を図で見てみましょう。

    まずメソッド呼び出し前の Bob を生成した直後の状態。

       Student bob = new Student("Bob"); // Bob
    


    Bobが生成されていますね。

    次にメソッドを呼び出した直後の状態。

       public void introduceAfterReplaceStudent(Student student) {
    


    2行目の student が仮引数のほうです。
    変数は別で用意されて値は同じになっています。(※重要)

    続いて仮引数の student に Mike を生成した直後の状態。

           student = new Student("Mike"); // 生徒ごと変える
    


    こうなります。
    というわけで仮引数に対して別オブジェクトを生成しただけであって
    もはや元の変数 student と仮引数の student には何の関係もありません。

    そのため実引数の bob は何も変更されず、名前が "Bob" のままあいさつをしているというわけです。

       bob.introduce();  // Bob(?) が自己紹介
    

    仮引数=渡す側の変数ではなく、 仮引数と渡す側の変数は同じオブジェクトを見ているだけで変数自体は別ということですね。

    まとめ

    Java の場合は参照をそのまま渡すのではなく参照先を渡しているにすぎません。
    一般的な参照渡しとは少し違うところがありますので、しっかりと理解して事故を起こさないようにしましょう。

    ただ、メソッド引数には final を付けて再代入不可にするのが基本です。
    その場合、仮引数への代入はできなくなります。(今回でいうと student ごと変えるのは不可能になる)
    ですが、オブジェクトの中身(今回でいうと student.name)は変更できるので、そのあたりは注意が必要です。

    なんとか短くしてみたつもりでしたが、また長くなってしまいました。
    記事を書くのって難しいですね。これからも精進します。