Javaの参照について
こんにちは、M.Oです。
今回はJavaと参照の関係をお題とした内容を紹介したいと思います。
「Javaは、基本参照渡しです」
これはずいぶん前に私自身が勉強会で発した言葉です。
微妙に語弊が生じているのがお分かりでしょうか?
これと似たようなことに、
Javaにはポインタがない
ということもよく聞きます。
結構このように理解されている皆様が多いのではないでしょうか?
本当にそうか?と思いこれに関して少し調べてみました。
まず最初に結論を出します。
Javaはすべて値渡しです。
これで全てなんですが、
これだけでは一瞬で記事が終わってしまうので、少しサンプルソースを交えながら
説明しておきたいと思います。
言葉の整理
まず少し言葉の整理をしておきます。
http://d.hatena.ne.jp/bleis-tift/20090603/1244031097
で紹介されている言語仕様によると
・Javaでは参照は、ポインタを表す。
・参照型は、参照(ポインタ)を保持する変数の型
となるそうです。
言葉の定義を念頭に次へ進みたいと思います。
検証
さて、ではちょっとした検証を行ってみましょう。
今まで信じていた通り、Javaが参照渡しなら以下のことが実現できるはずです。
・メソッドなどで引数に渡した変数自身に対しての変更が、呼び出し元の変数自身にも反映される。
この命題を検証するために以下のサンプルソースを試してみます。
仮引数への変更が、実引数に反映されているのかを検証するサンプルです。
もう少し手順を詳しく言うと、
メソッドを呼び出し、渡した引数をメソッド内で操作、
その後呼び出し側の引数と比較して変更が反映されているかを検証します。
サンプルソース
public class Main { public static void main(String[] args) { Target t1 = new Target("t1"); //変更前のインスタンスの表示 System.out.println(t1.toString()); //メソッド内で別の引数を生成する change(t1); //変更後のインスタンスの表示 System.out.println(t1.toString()); } public static void change(Target t) { t = new Target("t3"); } } class Target { private String name; public Target(final String name) { this.name = name; } public String toString() { return this.name; } }
実行結果
実行結果 t1 t1
解説
予想とは反した結果になっているのがお分かりでしょうか?
参照を渡しているのでオブジェクトに対する変更が反映されているはずです。
しかし、変更は反映されていません。
つまり、冒頭でイメージしていた参照渡しではないことがわかります。
値渡しで考えてみる
ここで値渡しであるという仮定のもと考えてみます。
値渡しをされた時点で、別の参照型変数に生成したオブジェクトのアドレスが格納(コピー)されます。
それが関数呼び出しにより、別のブロックに生成され渡されます。
この辺は「スタックと関数呼び出し」で検索するといいかもしれません。
関数を呼び出したときは、メモリの中でどのような動作が行われているのかを把握することができれば、
今回の記事はほとんど当たり前のように感じると思います。
実際に、私もあまり意識していなかったのでこのように調査をして記事を執筆しているのですが。
実際の動作のイメージは以下のようになります。
この時点で、main内の参照型変数t1とメソッドの仮引数で使われている変数tは別物ということになります。
ここで注意してほしいのは、互いの変数が指し示しているオブジェクトは同じということです。
なので、オブジェクトが指しているメンバ変数の変更は反映されるということです。
これは今までの通りのイメージですよね。t1のインスタンスのフィールドの変更は、呼び出し元でも反映されます。
結論
冒頭にあった結論を繰り返しますが、
「Javaはすべて値渡しです」
検証の例は、参照型を値渡ししている、というとわかりやすいでしょうか?
うーん、当たり前のような気がしますがじっくりと考えてみると結構ややこしいですね。
ちなみに、Javaにはきちんとポインタとなる概念が存在しますので、ポインタがないという
のは間違いで、ポインタを意識する必要がない、といった方がいいのかもしれませんね。
少し調査が足りないので、時間があればもう少し詳しく掘り下げることができると思います。
最後に色々なサイトを参考にしましたので、掲載しておきます。
今回の調査において、参考にさせていただいたサイトです。
「予定は未定Blog版」
http://d.hatena.ne.jp/bleis-tift/20090603/1244031097
「超てきとーですから」
http://macbookshiro.blog51.fc2.com/blog-entry-35.html
おまけ
Javaの引数は、全て値渡しであるためにprimitive型はswapができません。
そこでprimitive型をRapperするクラスを自作し、無理やりswapしてみました。
以下はイメージ図です。
ソースコード
public class Main { public static void main(String[] args) { RapperInteger rix = new RapperInteger(1111); RapperInteger riy = new RapperInteger(2222); System.out.println("before"); System.out.println(rix.toString()); System.out.println(riy.toString()); //swap swap(rix, riy); System.out.println("after"); System.out.println(rix.toString()); System.out.println(riy.toString()); } //swapメソッド public static void swap (final RapperInteger riX, final RapperInteger riY) { int tmp = riX.getValue(); riX.setValue(riY.getValue()); riY.setValue(tmp); } } //プリミティブ型をswapするためのラッパークラス class RapperInteger { private int value; public RapperInteger(final int value) { this.value = value; } public String toString() { return Integer.toString(this.value); } public int getValue() { return this.value; } public void setValue(final int value) { this.value = value; } }
出力結果
before 1111 2222 after 2222 1111
解説
primitive型をラップするクラスを自作して、そのクラスにprimitiveを保持する。
swapメソッドでは、ラッパークラスが保持しているprimitiveを入れ替える。
これでかなりの遠回りですが、primitiveのswapが実現できました。