Javaにおける値渡しと参照渡し

 しばしば、値渡しと参照渡しについて尋ねられることがあります。今回は、Javaの引数の渡し方について解説してみましょう。

スポンサーリンク
rectangle

引数の渡し方

 CやC++などでよく見られた、いわゆる「ポインタ」はJavaの世界においては存在しません。また、.NET系言語にみられる渡し方の指定もできませんから、必然的に、引数の渡し方は固定されていることになります。

値渡し

 値渡しとは、変数の値自体を渡すことです。Javaではプリミティブ型がこの渡し方になります。

public class Main {
    public static void main(String[] args) throws Exception {
        int num = 1;
        System.out.println(num);    // -> 1

        increment(num);
        System.out.println(num);    // -> 1
    }

    private static void increment(int num){
        num++;
    }
}

 関数内部で変数値を直接インクリメントしても、元の変数は不動のままです。特に解説は不要ですね。

Stringは値渡しと聞いた

 初心者向けにわかりやすく説明しようとしたのでしょうか、Stringを値渡しと説明する人がいるようですが、Stringはimmutable(不変)オブジェクトであり、あたかも値渡しのように振る舞っているだけに過ぎません。

 Stringの実態はchar型の配列を内包したオブジェクトです。Javaでは、オブジェクトは例外なく、後述する参照渡しとして渡されます。

参照渡し

 参照渡しとは、変数の参照を渡すことです。オブジェクト型(つまり、プリミティブ以外のすべての型)はすべてこの形になります。

import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        List<String> list = new ArrayList<>();
        addHello(list);
        System.out.println(list.toString());    // -> [hello]
    }

    private static void addHello(List<String> list){
        list.add("hello");
    }
}

 ただし、厳密な「参照渡し」ではなく「参照の値」を渡していることに注意してください。たとえば、C#では以下が実現できますが…。

public class Hello{
    public static void Main(){
        string str = "Hello C#!!";
        ChangeString(ref str);              // ref修飾子によって参照渡しを明示する
        System.Console.WriteLine(str);      // -> Hello World!!
    }

    private static void ChangeString(ref string str){   // ref修飾子によって参照渡しを明示する
        str = "Hello World!!";
    }
}

 Javaにはできません。

public class Main{
    public static void main(String[] args){
        String str = "Hello Java!!";
        changeString(str);
        System.out.println(str);    // -> Hello Java!!
    }

    private static void changeString(String str){
        str = "Hello World!!";
    }
}

 つまりJavaでは、オブジェクト型として定義された変数は「ここの番地を見てね!」と、メモリアドレスのメモを保持しているに過ぎないわけです。

 メモを上書きしても当然その場所のデータは変更されませんから、このコードの動作にも納得できますね。

 では、先ほどのリストの例は何が違うのか?というと、ドット演算子によって渡されたメモのメモリアドレスを参照しています。メモリアドレスにはListオブジェクトが格納されており、それを直接触るわけですから、呼び出し元のオブジェクトも当然変更されます。

Javaはすべて値渡し(参照の値渡し)なのに、「参照渡し」と言うことに違和感がある

 詳しい人にとってはまさにその通りなのですが、動作の本質を分かっていればどちらでもよいと思います。初心者に説明するときには少し注意が必要ですが、普段の会話等々でわざわざ「参照の値渡し」などと言うのも冗長です。「参照渡し」でも十分伝わるので、特に気にすることもないでしょう。

 そもそも、Javaの言語仕様を理解している人同士での会話で、値渡し、参照渡しなどといった言葉が出てくること自体稀ですが…。

引数のオブジェクトはむやみに変更すべきでない

 先述したように、Javaでは渡されたオブジェクトの中身にそのままアクセスすることができます。即ち、オブジェクトを自由に変更できる可能性があるということです。

 しかし、呼び出し先のメソッド内部で渡されたオブジェクトの中身をむやみに変更すべきではありません。これは、コードの一貫性を破壊し、可読性を著しく損なうことがあります。以下に例示してみましょう。

public class Main{
    public static void main(String[] args){
        User user = new User();
        user.name = "michael";
        System.out.println(user.name);    // -> michael
        changeName(user);
        System.out.println(user.name);    // -> john

    }

    private static void changeName(User user){
        user.name = "john";
    }
}

public class User{
    public String name;
    public int age;
}

 一番の問題点は、引数として渡したオブジェクトの内容が、「いつの間にか」変わってしまう点にあります。これはデバッグ困難な問題を容易に引き起こします。

 絶対に使ってはいけない、ということはありませんが、使う場合は、コメントやメソッド名などで「引数として渡されたオブジェクトが変更される可能性がある」ことを必ず明示しておきましょう。

JavaでC#におけるint.TryParseと同じことをしたい

 ところで、C#ではParseできるかどうかを判定する、int.TryParse(string, out int)メソッドがあります。このメソッドの戻り値はパース可能かどうかを示すboolean型で、パース可能であった場合には、参照として渡された第二引数である「out int」にパースされた値が格納されます。分かりやすいようにコードにしてみましょう。

public class Hello{
    public static void Main(){
        int output;
        string input = "100";
        bool isParsed = int.TryParse(input, out output);
        if(isParsed){
            System.Console.WriteLine(output);
        } else {
            System.Console.WriteLine("unparsable value");
        }
    }
}

 try~catchを使わなくていい分、簡潔な構文になりました。しかし、JavaはC#のような参照渡しがサポートされていないので、resultの他に値を出力することができません。それでも無理矢理実装するとすれば、以下のようになるでしょう。

public class Main {
    public static void main(String[] args) throws Exception {
        String input = "100";
        ParseResult res = tryParse(input);
        if(res.isParsed){
            System.out.println(res.value);
        } else {
            System.out.println("unparsable value");
        }
    }

    private static ParseResult tryParse(String str){
        ParseResult res = new ParseResult();
        try{
            res.value = Integer.parseInt(str);
            res.isParsed = true;
        } catch(Exception e) {
            res.isParsed = false;
        }
        return res;
    }
}

public class ParseResult{
    public boolean isParsed;
    public int value;
}

 しかし、パースできたかどうか判断するだけならこんな複雑なことをしなくても、

public class Main {
    public static void main(String[] args) throws Exception {
        String input = "100";
        Integer res = parse(input);
        if(res != null){
            System.out.println(res);
        } else {
            System.out.println("unparsable value");
        }
    }

    private static Integer parse(String str){
        try{
            return Integer.parseInt(str);
        } catch(Exception e) {
            return null;
        }
    }
}

 これで十分な気はします。

まとめ

 参照渡しは便利ですが、仕様をしっかり理解していないと、とんでもないバグに遭遇することがあります。本稿の例に限らず、プログラミング言語は正しく理解して使うようにしましょう。