Javaの参照渡しとは
Javaのメソッドで扱う引数の値は、基本型では値渡し、参照型では参照渡し、という話です。しかし、この話が出るとStringの挙動がわからないという疑問がちらほら出てきます。
まぁ、そのあたりを説明できたら良いなぁと思います。
基本型の場合
基本型の場合は、値渡しです。なので、値がコピーされて渡される。
値そのものを渡すわけではない。
つまり、メソッドにわたす前とわたす後で、渡した変数の値が変わることはないということです。
// 例1
public class PrimitiveType {
public static void main(String[] args) {
PrimitiveSample ps = new PrimitiveSample();
int i = 5;
System.out.println("mainメソッドの変更前の値 : " + i);
ps.changeInt(i);
System.out.println("mainメソッドの変更後の値 : " + i);
}
}
class PrimitiveSample {
public int changeInt(int i) {
System.out.println("changeIntで変更前の値 : " + i);
i = i + 10;
System.out.println("changeIntで変更後の値 : " + i);
return i;
}
}
mainメソッドの変更前の値 : 5
changeIntで変更前の値 : 5
changeIntで変更後の値 : 15
mainメソッドの変更後の値 : 5
参照渡し
参照型の場合は、「参照型が持つ、参照しているアドレスをコピーして渡す」なので実際は『値渡し』です。
つまり、「東京都渋谷区桜丘町1の住所」が書かれたメモをコピーして渡して、メモのコピーを使って「東京都渋谷区桜丘町1の住所に手紙を送る」動作をした場合、「東京都渋谷区桜丘町1の住所」の人は手紙を受け取ります。
しかし、メモのコピーをビリビリに破いたとしても、「東京都渋谷区桜丘町1の住所」には影響がありません。
// 例2
public class PointerType {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("あいうえお");
System.out.println("mainメソッドの変更前の値 : " + sb.toString());
PointerSample ps = new PointerSample();
ps.appendData(sb);
System.out.println("mainメソッドの変更前の値 : " + sb.toString());
ps.deleteObject(sb);
System.out.println("mainメソッドのオブジェクトは消えない : " + sb.toString());
}
}
class PointerSample {
public void appendData(StringBuffer sb) {
System.out.println("appendDataで変更前の値 : " + sb.toString());
sb.append("かきくけこ");
System.out.println("appendDataで変更後の値 : " + sb.toString());
}
public void deleteObject(StringBuffer sb) {
sb = null;
}
}
mainメソッドの変更前の値 : あいうえお
appendDataで変更前の値 : あいうえお
appendDataで変更後の値 : あいうえおかきくけこ
mainメソッドの変更前の値 : あいうえおかきくけこ
mainメソッドのオブジェクトは消えない : あいうえおかきくけこ
ここまでは、今まで話しに上がってきたものだと思います。そもそも、参照渡しという言葉も良くなくて「参照先の値渡し」が正しいですし、そう唱えている人も他にも居ます。
Javaはあまり知らないが、C言語とC++は得意という人に聞いたら「これは値渡しだ」と断言していました。面白いですね。
StringとかIntegerとか…
そこで、StringとかIntegerとか出てくると、「参照型は参照渡しなのに、Stringの値は変わらない。これは値渡しなのでは」という謎の議論が出るのですが、そもそも上記の話を考えれば「参照型は参照先の値渡し」で全部終わっちゃうんですけどね。
もう少し踏み込んで解説すると、
String型には、そのオブジェクト自身を変更する関数がない、定数として扱うものです。
Integer型とかも同じ。オブジェクト自身を変更する関数がない。ListとかStringBufferとかみたいに「自分のオブジェクトに値を追加し、オブジェクトの中身が変わる」ような関数がないのです。
// スッキリしない例3
public class StringType {
public static void main(String[] args) {
StringSample ss = new StringSample();
String str = "あいうえお";
System.out.println("mainメソッドの変更前の値 : " + str);
ss.changeString(str);
System.out.println("mainメソッドの変更後の値 : " + str);
}
}
class StringSample {
public void changeString(String str) {
System.out.println("changeStringで変更前の値 : " + str);
str = str + "かきくけこ";
System.out.println("changeStringで変更後の値 : " + str);
}
}
mainメソッドの変更前の値 : あいうえお
changeStringで変更前の値 : あいうえお
changeStringで変更後の値 : あいうえおかきくけこ
mainメソッドの変更後の値 : あいうえお
例3だけ見ると例1と同じように見えて「値渡し」と言ってしまうのでしょう。
実際、str = str + "かきくけこ";
の中身をきちんと確認しなければなりません。
System.identityHashCode(obj)は万能ではないのですが、この程度の調査であれば活用できます。業務に組み込もうとすると「重複も有り得る」ことを視野にいれなければいけないのですが、値が変わっている場合はオブジェクト自体が変わっていると見て良いものです。
public class StringType {
public static void main(String[] args) {
StringSample ss = new StringSample();
String str = "あいうえお";
System.out.println("mainメソッドの変更前の値 : " + str);
System.out.println("mainの中のハッシュ1:" + System.identityHashCode(str));
ss.changeString(str);
System.out.println("mainメソッドの変更後の値 : " + str);
System.out.println("mainの中のハッシュ2:" + System.identityHashCode(str));
}
}
class StringSample {
public void changeString(String str) {
System.out.println("-------");
System.out.println("changeStringで変更前の値 : " + str);
System.out.println("changeStringの中のハッシュ1:" + System.identityHashCode(str));
System.out.println("-------");
str = str + "かきくけこ";
System.out.println("changeStringで変更後の値 : " + str);
System.out.println("changeStringの中のハッシュ2:" + System.identityHashCode(str));
System.out.println("-------");
}
}
mainメソッドの変更前の値 : あいうえお
mainの中のハッシュ1:2018699554
-------
changeStringで変更前の値 : あいうえお
changeStringの中のハッシュ1:2018699554
-------
changeStringで変更後の値 : あいうえおかきくけこ
changeStringの中のハッシュ2:1311053135
-------
mainメソッドの変更後の値 : あいうえお
mainの中のハッシュ2:2018699554
main内のハッシュは「2018699554」ですが、changeString内では「2018699554」と「1311053135」があります。
引数であるstrに変更がない場合は、main内のハッシュ値と一致しているのですが、strの値を変更した時にオブジェクトの変更が発生しています。
つまり、元の「2018699554」に対して変更したのではなく、新しいオブジェクト「1311053135」を作って、そこで「あいうえおかきくけこ」を参照しているということになっているわけです。
これは例2の「メモをコピーしたものを破いた」ものと同じです。str的には「nullを入れた」か「新しいオブジェクトを入れた」かの違いでしか無いわけです。
もしも、String型に「自身のオブジェクトの値を変更する」ようなメソッドがあったら、こんなややこしい論争にはならなかったかもしれないです。
逆に言えば、String型は、一度設定したオブジェクトは他のオブジェクトが代入されるまで不変である、ということも成り立つということです。
配列…お前…
配列なんかはint型の配列だから値渡しに違いない、と言って泣きを見るようなことがあるのですが、配列は実質オブジェクトなので他の参照型と同じ扱いで考えると良いですね。基本型も中に入れられるオブジェクトの箱と思えばそんなに難しくはないと思います。
終わりに
参照渡しって言うと、オブジェクト自体の参照値を渡しているようなイメージになっちゃうので良くないですよね。って話でした。