4.2 フィールド
4.2.1 オブジェクトが持つ変数
オブジェクトに結び付けられた変数を宣言することで、オブジェクトは個別に保存領域を確保することができます。この変数は、メソッド内で宣言されたローカル変数とは異なり、オブジェクトが存在する限り破棄されることはありません。オブジェクトに関連付けられた変数をフィールドと呼びます。フィールドを導入するには、フィールド宣言をクラスの内部で行わなければなりません。
class クラス名 { 型 変数名 = 式文 , ... ... }
フィールド宣言はローカル変数宣言とほとんど同じだと思ってかまいません。変数初期化子を指定することも可能で、初期化した場合は、クラスのインスタンスが生成される時点で式が評価されます。このように、クラスに関連付けられた要素を、そのクラスのメンバと呼びます。Java 言語では、フィールドとメソッドがクラスのメンバとなることができます。
ただし、フィールドはクラスに関連付けられているため、ローカル変数のように独立した存在ではありません。フィールドにアクセスするには、そのフィールドを保有するオブジェクトを指定しなければなりません。これをフィールド・アクセス式と呼びます。
オブジェクト . フィールド識別子
フィールドはインスタンスごとに独立しているため、最初に対象のオブジェクトを指定しなければなりません。その後、ドット記号 . に続いて目的のフィールド名を指定します。これで、従来の変数のようにフィールドの値を取得したり、新しい値を代入することができます。
多くの情報は、関係する複数のデータが集まって構造的に管理されます。例えば、グラフを書くときに使われる方眼紙を想像してください。方眼紙の特定の点は X 座標と Y 座標から構成される情報で表すことができます。これをオブジェクト指向で表現する場合、X 座標と Y 座標の値を格納する 2 つの int 型フィールドを持つ Point クラスを宣言すれば、構造的に情報を管理することができるようになるのです。
class Point { int x = 0 , y = 0; } class Test { public static void main(String args[]) { Point pt1 = new Point(); Point pt2 = new Point(); pt2.x = 100; pt2.y = 50; String str = "pt1.x = " + pt1.x + " : pt1.y = " + pt1.y + '\n'; str += "pt2.x = " + pt2.x + " : pt2.y = " + pt2.y + '\n'; System.out.println(str); } }
>java Test pt1.x = 0 : pt1.y = 0 pt2.x = 100 : pt2.y = 50
コード1は、2次元座標の点を表すための Point クラスを作成しています。Point クラスには X 座標を表す x フィールドと、Y 座標を表す y フィールドが宣言されています。これらの値は、0 で初期化されているためインスタンスが生成された時点では 0 となります(数値型のフィールドは、明示的に初期化しなくても、初期値は 0 です)。
Test クラスでは、Point クラスのインスタンスを作成し、それぞれクラス型変数 pt1 と pt2 に代入しています。pt1 オブジェクトのフィールドは初期値のまま、pt2 オブジェクトのフィールドは新しい値を代入しています。それぞれの値を表示することで、クラスのメンバはインスタンスごとに独立して与えられていることを確認することができるでしょう。
実行結果は、pt1 のフィールドは双方共に初期値の 0 であり、pt2 は x フィールドが 100、y フィールドが 50 であることを示しています。この結果は、クラスで宣言されたフィールドがオブジェクトごとに独立して存在(すなわち、固有のメモリ領域を保有)していることを表しています。
4.2.2 再帰的な初期化
フィールドの型はローカル変数宣言と同様に自由に指定することができます。クラス型を指定することもできるため、フィールドとして何らかのオブジェクトを保有することもできます。また、自分自身のオブジェクトを保有することもできます。例えば、次のようなクラス宣言は有効です。
class Point { Point origin; int x , y; }
この Point クラスは、座標の原点となる座標を指定する origin フィールドを宣言していると仮定します。何らかの座標に対して相対的に座標を決定する仕組みを導入する場合、このような設計が好ましくなるでしょう。
ただし、注意しなければならないことがあります。origin フィールドを初期化するためにクラス・インスタンス生成式を使うことはできません。もし、origin の宣言と同時にインスタンスを生成する場合、Point クラスのインスタンスを生成するタイミングで Point クラスのインスタンスを生成する再帰的な初期化となります。これは、構文上の問題はないためコンパイルすることができますが、実行すると永遠と Point クラスの初期化を繰り返してしまいます。
class Point { Point origin = new Point(); int x , y; } class Test { public static void main(String args[]) { new Point(); } }
>java Test java.lang.StackOverflowError at Point.<init>(Test.java:2) at Point.<init>(Test.java:2) at Point.<init>(Test.java:2) at Point.<init>(Test.java:2) at Point.<init>(Test.java:2) ...
コード2はコンパイルすることは可能ですが、実行すると実行時エラーとなります。Point クラスの origin フィールドは Point 型となっています。これ自体は問題ありませんが、フィールド宣言と同時にインスタンスを生成していることが問題です。フィールド宣言時の初期化子は、クラスのインスタンスを生成する時に実行されます。Point クラスのインスタンスを生成しようとすると、さらに Point クラスのインスタンスを生成しようとするため、抜け出せなくなってしまうというわけです。
また、同一クラス内の他のフィールドをフィールドの初期化に参照することもできます。
class Point { int x = 0 , y = x; }
この宣言では、y フィールドの初期値に x フィールドを指定していますが問題はありません。y フィールドが初期化される時点では x フィールドは正しく初期化されています。幸い、次のような宣言はコンパイル時にエラーとして検出されます。
class Point { int x = y; int y = x; }
この場合、y = x は正しく処理されますが、x = y が不正であるとしてコンパイル・エラーとなります。フィールド宣言の初期化では、循環した初期化が行われないように制限が加えられています。フィールドの初期化に用いる式文では、それよりも前に宣言したフィールドしか使うことができません。フィールドの初期化子以外では、メンバの宣言順序を気にする必要はありません。