代入演算子について、まず基本型から見てきます。これは、単に基本型の復習というだけです。
ソースコードは以下の通り。
H301/H301.java
/** * 基本型の代入演算子を確認します。 */ public class H301 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { int a = 10; int b; // aの値を bに代入する b = a; // aと bの値を表示する System.out.println("変更前:"); System.out.println("変数 aの値は " + a); System.out.println("変数 bの値は " + b); // a の値を変更する a = 20; // aと bの値を表示する System.out.println("変更後:"); System.out.println("変数 aの値は " + a); System.out.println("変数 bの値は " + b); } }
実行結果は以下の通り。
変更前: 変数 aの値は 10 変数 bの値は 10 変更後: 変数 aの値は 20 変数 bの値は 10
当たり前のことですよね。このイメージは以下のような感じです。(1)→(2)の順に処理が実行されます。
ただ、基本型変数と参照型変数を同じように考えていると、この後でさっそく混乱します。実例を見ていきます。
基本型で行ったようなことと、同じように見えることを参照型でも実行したいと思います。
ソースコードは以下の通り。
H302/H302.java
import java.awt.Point; /** * 参照型の代入演算子を確認します。 */ public class H302 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { Point p = new Point(100, 200); Point q; // pの参照を qに渡す q = p; // pと qの値を表示する System.out.println("変更前:"); System.out.println("変数 pの参照先インスタンスの内容は " + p); System.out.println("変数 qの参照先インスタンスの内容は " + q); // pの参照先インスタンスの値を変更する p.x = 150; p.y = -10; // pと qの値を表示する System.out.println("変更後:"); System.out.println("変数 pの参照先インスタンスの内容は " + p); System.out.println("変数 qの参照先インスタンスの内容は " + q); } }
実行結果は以下の通り。
変更前: 変数 pの参照先インスタンスの内容は java.awt.Point[x=100,y=200] 変数 qの参照先インスタンスの内容は java.awt.Point[x=100,y=200] 変更後: 変数 pの参照先インスタンスの内容は java.awt.Point[x=150,y=-10] 変数 qの参照先インスタンスの内容は java.awt.Point[x=150,y=-10]
変数 pの参照先のインスタンスを操作しただけなのに、変数 qの参照先インスタンスフィールドの内容が変わってしまっています。前の章からの説明をしっかり理解していれば当然の理屈として理解できるのですが、説明がつくでしょうか。自分で考えることは重要なことなので、もし納得いかなければじっくり考えてみてください。
さて、今回の(思わぬ?)動きを図にしてみようと思います。イメージとしては以下のような感じです。(1)→(2)の順番に処理が実行されます。
このように、参照型変数 pと qは同じインスタンスを示しています。そのため、pの参照先インスタンスのインスタンスフィールドを変更すれば、同時に変数 qの参照先インスタンスのインスタンスフィールドが変更されるのは当然です。以前、インスタンスそのものには名前が無いと説明しました。今回は、事実上、そのインスタンスに p , qという 2つの名前がつけられた状態、と言い換えることもできます。
「変数 pと qは別々の値を保持してもらわなければ困る」という場合の回避策のひとつ目を挙げてみます。
ソースコードは以下の通り。
H303/H303.java
import java.awt.Point; /** * 参照型のインスタンスの内容をコピーします。方法その 1。 */ public class H303 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { Point p = new Point(100, 200); Point q = new Point(); // pのインスタンスフィールドである基本型の値を qの基本型のインスタンスフィールドに渡す q.x = p.x; q.y = p.y; // pと qの値を表示する System.out.println("変更前:"); System.out.println("変数 pの参照先インスタンスの内容は " + p); System.out.println("変数 qの参照先インスタンスの内容は " + q); // pの値を変更する p.x = 150; p.y = -10; // pと qの値を表示する System.out.println("変更後:"); System.out.println("変数 pの参照先インスタンスの内容は " + p); System.out.println("変数 qの参照先インスタンスの内容は " + q); } }
実行結果は以下の通り。
変更前: 変数 pの参照先インスタンスの内容は java.awt.Point[x=100,y=200] 変数 qの参照先インスタンスの内容は java.awt.Point[x=100,y=200] 変更後: 変数 pの参照先インスタンスの内容は java.awt.Point[x=150,y=-10] 変数 qの参照先インスタンスの内容は java.awt.Point[x=100,y=200]
まず、予約語new
が 2回使用されていることに注目です。そもそもインスタンスが 1つでは、別々にすることは理論的に不可能です。そこで 13, 14行目でインスタンスを 2つ生成しています。続いて、26, 27行目では参照先インスタンスのインスタンスフィールドの値をインスタンスフィールドに代入しています。ここで x, yは基本型です。したがって、代入演算子は基本型の動きをします。そのため、p と qで異なったインスタンスの状態を保持できることになります。
イメージとしては以下のような感じです。(1)→(2)の順番に処理が実行されます。
理解できたでしょうか。
もうひとつ、変則的ではありますがこういう回避策もあるよ…ということで、挙げてみます。
ソースコードは以下の通り。
H304/H304.java
import java.awt.Point; /** * 参照型のインスタンスの内容をコピーします。その2。 */ public class H304 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { Point p = new Point(100, 200); Point q; // pの参照を qに渡す q = p; // pと qの値を表示する System.out.println("変更前:"); System.out.println("変数 pの参照先インスタンスの内容は " + p); System.out.println("変数 qの参照先インスタンスの内容は " + q); // pを新しいインスタンスに紐づける p = new Point(150, -10); // pと qの値を表示する System.out.println("変更後:"); System.out.println("変数 pの参照先インスタンスの内容は " + p); System.out.println("変数 qの参照先インスタンスの内容は " + q); } }
実行結果は以下の通り。
変更前: 変数 pの参照先インスタンスの内容は java.awt.Point[x=100,y=200] 変数 qの参照先インスタンスの内容は java.awt.Point[x=100,y=200] 変更後: 変数 pの参照先インスタンスの内容は java.awt.Point[x=150,y=-10] 変数 qの参照先インスタンスの内容は java.awt.Point[x=100,y=200]
やはり予約語new
を 2回使用していることには変わりありませんし、変数 pと qとではインスタンスの内容が異なっています。ただ、先ほどの例とは少し動きが異なります。
イメージとしては以下のような感じです。(1)→(2)→(3)の順番に処理が実行されます。
理解できたでしょうか。
Point型の場合、その他にももっとスマートな解決策があります。ただ、Point型に限定した話ではあまり今後の役には立たないと思い、省略します。余裕があったら、説明書(API)を読むなどしていろいろ実験してみてください。
インスタンスのコピーについて、今回は「変数 pの内容を変数 qにコピーしようとした」とみなすこともできます。基本型と違ってインスタンスの場合にはコピーの扱いが難しい。今回の例のように、参照だけコピーする例(H302.java)と、インスタンスをコピーする例(H303.java, H304.java)の 2つに分けられます。インスタンスの内容を変更しなければ両者の区別はつきません。ところが、インスタンスの内容を変更すると両者の動きは別物になってしまいます。このあたりは、両者の特性をわきまえて使い分ける必要があります。
メソッドを呼び出すときに引数を渡した際には、事実上、呼び出し先引数への代入が行われています。ということは、基本型と参照型で挙動が異なってくるということでもあります。ここまでの説明を踏まえれば理解することは難しくありません。さっそく見ていきたいと思います。
なお、C言語を学習済みの方にとっては、値渡しとポインタ渡しの違いと理解いただいて問題ありません。
引数が基本型の場合は値渡しです。
ソースコードは以下の通り。
H305/H305.java
/** * メソッドに値渡しをします。 */ public class H305 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { int a = 10; System.out.println("aの値は " + a); // メソッドを呼び出す H305.changeValue(a); System.out.println("aの値は " + a); } /** * 値を変更します。 * @param b 値 */ private static void changeValue(int b) { System.out.println("bの値は " + b); // 値を変更する b = 30; System.out.println("bの値は " + b); } }
実行結果は以下の通り。
aの値は 10 bの値は 10 bの値は 30 aの値は 10
見ての通り、変数 aの値は、メソッドを使用した後も変わりありません。
イメージとしては以下のような感じです。
続いて、引数に参照型を渡す場合を調べてみます。
ソースコードは以下の通り。
H306/H306.java
import java.awt.Point; /** * メソッドに参照渡しをします。 */ public class H306 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { Point p = new Point(100, 200); System.out.println("p の参照先インスタンスの内容は " + p); // メソッドを呼び出す H306.changeValue(p); System.out.println("p の参照先インスタンスの内容は " + p); } /** * 引数の参照先インスタンスの内容を変更します。 * @param q 対象 */ private static void changeValue(Point q) { System.out.println("q の参照先インスタンスの内容は " + q); q.x = 150; q.y = -10; System.out.println("q の参照先インスタンスの内容は " + q); } }
実行結果は以下の通り。
p の参照先インスタンスの内容は java.awt.Point[x=100,y=200] q の参照先インスタンスの内容は java.awt.Point[x=100,y=200] q の参照先インスタンスの内容は java.awt.Point[x=150,y=-10] p の参照先インスタンスの内容は java.awt.Point[x=150,y=-10]
引数に設定した変数 pの参照先インスタンスの内容が、メソッドを呼び出したことで変更されたことが分かるでしょうか。
イメージとしては以下のような感じです。(1)→(2)の順番に処理が実行されます。
理解できたでしょうか。
念のため、似て非なる例を示しておきますね。
ソースコードは以下の通り。
H307/H307.java
import java.awt.Point; /** * メソッドに参照渡しをします。ただし値は変わりません。 */ public class H307 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { Point p = new Point(100, 200); System.out.println("p の参照先インスタンスの内容は " + p); // メソッドを呼び出す H307.changeValue(p); System.out.println("p の参照先インスタンスの内容は " + p); } /** * 引数の参照先インスタンスの内容を変更します(失敗します)。 * @param q 対象 */ private static void changeValue(Point q) { System.out.println("q の参照先インスタンスの内容は " + q); q = new Point(150, -10); System.out.println("q の参照先インスタンスの内容は " + q); } }
実行結果は以下の通り。
p の参照先インスタンスの内容は java.awt.Point[x=100,y=200] q の参照先インスタンスの内容は java.awt.Point[x=100,y=200] q の参照先インスタンスの内容は java.awt.Point[x=150,y=-10] p の参照先インスタンスの内容は java.awt.Point[x=100,y=200]
この場合は、引数に設定した変数 pの参照先インスタンスの内容が、メソッドを呼び出したあとでも変更されておりません。もし、先ほどとの違いが分からなければ、じっくり考えてみてください。
イメージとしては以下のような感じです。(1)→(2)→(3)の順番に処理が実行されます。
理解できたでしょうか。
ここまでの幾つかのプログラムを実行する中で、基本型変数と参照型変数とでは、代入演算子の意味合いがまったく異なっていることが理解できたでしょうか。
ただ、実際のところは、代入演算子の存在によって基本型と参照型の違いが際立つというだけです。
基本型は、名前(変数名)とその箱が一体化している。参照型は、インスタンスそのものには名前が無く、参照型変数に参照されることで事実上の名前がつけられる…つまり分離している。この違いが、結果的に代入演算子の挙動の違いを生む、ということを理解できれば大丈夫です。