WisdomSoft - for your serial experiences.

4.6 プリミティブ型と参照型

プリミティブ型と参照型の違いについて説明します。単純な値の代入は値の複製が行われますが、オブジェクトの代入はオブジェクトが複製されるのではなく、オブジェクトを指す参照が複製されます。

4.6.1 値とオブジェクト

Java では、byte、char、short、int、long、float、double そして boolean 型をプリミティブ型と呼んでいます。プリミティブ型は値型とも呼ばれオブジェクトのような構造的な情報を持たない単純な情報のことを表しています。これに対して、クラス型は多くの情報をフィールドとして抱合する構造的な情報です。プリミティブ型とは異なり、クラス型は情報量が多く、メモリを使用します。そこで、メソッドに引数として値を代入したり、他の変数に値を代入するという行為が問題になります。

プリミティブ型は、他の変数に代入したり、メソッドに引数として渡した場合は値が複製されます。しかし、インスタンスの複製はメモリを多く使用するうえ、コピーの過程でコンストラクタが呼び出されず、整合性に問題が発生してしまいます。これは、C++ 言語で指摘される重要な問題です。

そこで、Java では変数にインスタンスそのものを格納するのではなく、インスタンスのメモリ位置を表す数値を代入します。インスタンスも情報なので、当然メモリ上のどこかに保存されています。インスタンスが保存されている位置さえわかれば、仮想マシンはインスタンスにアクセスすることができるため、クラス型の変数には常に、インスタンスへの参照が保存されているのです。C 言語プログラマにはポインタ(情報が保存されているメモリ上の位置を表す参照値)といった方がわかりやすいでしょう。このような、インスタンスが保存されているメモリ上の位置を表す変数を、プリミティブ型に対して参照型と呼びます。クラス型の変数は参照型であると表現することができます。

コード1
class Test {
	String str;
	public static void main(String args[]) {
		Test obj1 = new Test();

		Test obj2 = obj1;
		obj2.str = "Kitty on your lap";

		System.out.println("obj1.str = " + obj1.str);
		System.out.println("obj2.str = " + obj2.str);
		System.out.println("(obj1 == obj2) = " + (obj1 == obj2));
	}
}
実行結果
>java Test
obj1.str = Kitty on your lap
obj2.str = Kitty on your lap
(obj1 == obj2) = true

コード1は、参照型の変数が個別にインスタンスを保有するのではなく、インスタンスの位置を指しているだけであることを証明するためのプログラムです。Test クラスは文字列型のフィールド str を持っています。main() メソッドで Test クラスのインスタンスを生成し、この参照を obj1 変数に代入しています。そして、同じ Test 型の変数 obj2 に obj1 を代入しています。しかし、obj1 はインスタンスが保存されているメモリ上の位置を保存しているにすぎないため、obj2 にも、メモリの位置を表す数値情報がコピーされるだけです。インスタンスは複製されません。

インスタンスが複製されていないという事実は、obj2 と obj1 変数が同一のフィールドを共有していることから確認できます。プログラムでは obj2 変数から str フィールドに文字列を代入しています。その後 println() メソッドで obj1 と obj2 それぞれの str フィールドを表示していますが、初期化していない obj1 の str フィールドも obj2 の str フィールドと同じ文字列を格納しています。

参照型の変数が同じインスタンスを指しているかどうかについては、関係演算子 == を用いて調べることができます。== 演算子のオペランドに参照型を与えた場合、双方が同じインスタンスを指している場合は true、そうでなければ false を返します。実行結果は obj1 と obj2 が同じインスタンスを共有していることを証明しています。

メソッドに引数として与えた場合も同様です。メソッドにはインスタンスの位置を表す参照のコピーが渡されるだけで、インスタンスは共有されます。もし、メソッド内で参照型の値を変更した場合は、呼び出し元にも何らかの影響を与えます。プログラマは、参照型の性質を十分に理解し、コード2のような危険なプログラムを作らないように注意しなければなりません。

コード2
class Point { 
	int x , y;
	Point(int x , int y) {
		this.x = x;
		this.y = y;
	}
	void addPoint(Point pt) {
		pt.x += x;
		pt.y += y;
	}
}

class Test {
	public static void main(String args[]) {
		Point pt1 = new Point(10 , 100);
		Point pt2 = new Point(50 , 30);
		pt1.addPoint(pt2);

		System.out.println("pt2.x = " + pt2.x + " : pt2.y = " + pt2.y);
	}
}
実行結果
>java Test
pt2.x = 60 : pt2.y = 130

コード2では、2次元座標を表す Point クラスを作成しています。この Point クラスは、指定した Point オブジェクトの座標を現在のオブジェクトから相対的な位置に変換する AddPoint() メソッドを宣言しています。AddPoint() メソッドでは、受け取った pt パラメータのフィールドに、自分自身の座標を加算しています。Point 型の変数は参照型なので、pt 変数のフィールドの値を変更するということは、main() メソッドの pt2 変数にも影響を与えるということです。

しかし、メソッドに参照型の引数を渡した開発者は、インスタンスに変更が加えられることを想定していないかもしれないのです。複雑化する現代プログラミングでは、クラスの設計者と利用者が同一人物とは限りません。あなたが作ったクラスが第三者に利用された時、開発者の意図しないところでインスタンスに変更が加えられるべきではありません。

4.6.2 参照型の寿命

プリミティブ型のローカル変数は、ブロックが終了すると共に破棄されました。これは、参照型も同様で、参照型の変数はブロックを抜けると破棄されます。しかし、参照型の変数が指していたインスタンスはどうなるのでしょうか。

インスタンスは、コード1の obj1 と obj2 変数の関係のように、複数の参照型変数に共有されている可能性があります。この場合、1 つの参照型変数が破棄されるタイミングでインスタンスまで破棄してしまえば、共有していた他の参照型変数が壊れてしまいます。インスタンスの削除のタイミングという問題は、安全性を確保する必要があり、これまで多くのプログラマを悩ます課題でした。

しかし、人間は簡単に過ちを犯す生物です。破棄してはならないタイミングでインスタンスを削除してしまったり、逆に不必要になったインスタンスをいつまでもメモリ残してしまう、いわゆるメモリリークと呼ばれるバグを発生させることもあります。Java はこうした問題をスマートに解決するために、インスタンスの管理責任を人間ではなくシステムに委譲しています。そのため、プログラマはインスタンスを明示的に破棄することはできず、インスタンスをいつ削除するかは Java 仮想マシンが判断し、不必要になった時点で自動的に削除してくれます。通常、インスタンスを参照する変数が存在しなくなった時点て不要と判断されるでしょう。

コード3
class Point {
	int x , y;
	Point(int x , int y) {
		this.x = x;
		this.y = y;
	}
	Point copy() {
		Point temp = new Point(x , y);
		return temp;
	}
}

class Test {
	public static void main(String args[]) {
		Point pt = new Point(10 , 100);
		Point copy = pt.copy();
		System.out.println("copy.x = " + copy.x + " : copy.y = " + copy.y);
	}
}
実行結果
>java Test
copy.x = 10 : copy.y = 100

コード3の Copy() メソッドは、自分自身のオブジェクトと同じ値を持つ Point オブジェクトを作成して戻り値として返します。Copy() メソッドでは、新しい Point インスタンスを作成し、その参照を temp 変数に格納しています。return 文で値を返し、ブロックが終了した時点でローカル変数は削除されます。このとき temp 変数は削除されてしまうのですが、temp 変数はインスタンスが保存されているメモリの位置情報を保存しているだけなので temp 変数が削除されても、インスタンスそのものが削除されるわけではありません。その証拠に、main() メソッドでは Copy() メソッドが返した Point オブジェクトを正常に表示できています。もし、インスタンスが削除されていれば、プログラムはクラッシュしてしまいます。

4.6.3 null 参照

フィールド変数は、ローカル変数とは異なり確実な代入状態であることを強制されません。初期化されていない数値型の変数は 0、boolean 型の変数は false が初期値となっています。そこで気になるのが、参照型の変数の初期値です。

インスタンスの参照が代入されていない参照型の変数は、その時点では何のインスタンスも指すことはできません。そこで、インスタンスへの参照を持たないことを明確にするための値として null を使います。null リテラルは概念上 null 型と呼ばれる独立した型に分類されていますが、事実上は参照型と同じように扱うことができます。そのため、null はあらゆる参照型の変数に代入可能です。

null を指す参照型の変数はオブジェクトが存在しないことを表しています。そのため、フィールドやメソッドにアクセスすることはできません。もし、null のフィールドやメソッドにアクセスを試みた場合、実行時にエラーが発生します。

コード4
class Point { int x , y; }

class Test {
	public static void main(String args[]) {
		Point pt = null;
		pt.x = 10;
		System.out.println("pt.x = " + pt.x);
	}
}
実行結果
>java Test
Exception in thread "main" java.lang.NullPointerException
        at Test.main(Test.java:6)

コード4は null を格納する参照型の変数から、フィールドにアクセスしています。参照型変数 pt の初期化で null を与え、その後 pt.x フィールドにアクセスしていますが、pt はインスタンスを指していないので、実行時エラーとなります。参照型変数がインスタンスを指しているかどうかを調べるには、関係演算子 == を用いて null と比較します。もし、変数が null を指しているならば null と比較して等しくなるでしょう。

このエラーは例外と呼び、例外はプログラム的に処理することができます。例外の捕捉については第8章で詳しく説明します。