クラス自体でクラスのインスタンスを作成することにはまったく問題はありません。明らかなニワトリが先か卵が先かという問題は、プログラムのコンパイル中と実行中のさまざまな方法で解決されます。
コンパイル時
それ自体のインスタンスを作成するクラスがコンパイルされると、コンパイラは、そのクラスがそれ自体に対して循環依存関係を持っていることを検出します。この依存関係は簡単に解決できます。コンパイラは、クラスが既にコンパイルされていることを認識しているため、再度コンパイルを試みることはありません。代わりに、クラスが既に存在するふりをして、それに応じてコードを生成します。
ランタイム
それ自体のオブジェクトを作成するクラスに関する最大の鶏が先か卵が先かの問題は、クラスがまだ存在していない場合です。つまり、クラスがロードされているときです。この問題は、クラスのロードを 2 つのステップに分割することで解決されます。最初にクラスが定義され、次に初期化されます。
定義とは、クラスをランタイム システム (JVM または CLR) に登録することを意味します。これにより、クラスのオブジェクトが持つ構造と、そのコンストラクターとメソッドが呼び出されたときに実行する必要があるコードが認識されます。
クラスが定義されると、初期化されます。これは、静的メンバーを初期化し、静的初期化ブロックや特定の言語で定義されたその他のものを実行することによって行われます。クラスはこの時点ですでに定義されているため、ランタイムはクラスのオブジェクトがどのように見えるか、およびそれらを作成するためにどのコードを実行する必要があるかを認識していることを思い出してください。これは、クラスを初期化するときにクラスのオブジェクトを作成することにまったく問題がないことを意味します。
クラスの初期化とインスタンス化が Java でどのように相互作用するかを示す例を次に示します。
class Test {
static Test instance = new Test();
static int x = 1;
public Test() {
System.out.printf("x=%d\n", x);
}
public static void main(String[] args) {
Test t = new Test();
}
}
JVM がこのプログラムをどのように実行するかを見ていきましょう。まず、JVM がTest
クラスをロードします。これは、クラスが最初に定義されることを意味するため、JVM はそれを認識します。
- という名前のクラスが
Test
存在し、main
メソッドとコンストラクターがあること、および
Test
クラスには 2 つの静的変数があり、1 つは と呼ばれ、もうx
1つは と呼ばれます。instance
Test
クラスのオブジェクトレイアウトは何ですか。言い換えれば、オブジェクトがどのように見えるか。どんな属性を持っている。この場合Test
、インスタンス属性はありません。
クラスが定義されたので、初期化されます。まず、デフォルト値0
ornull
はすべての静的属性に割り当てられます。に設定x
し0
ます。次に、JVM は静的フィールド初期化子をソース コード順に実行します。二つあります:
- クラスのインスタンスを作成し、
Test
に割り当てinstance
ます。インスタンスの作成には、次の 2 つの手順があります。
- 最初のメモリがオブジェクトに割り当てられます。JVM は、クラス定義フェーズからオブジェクト レイアウトを既に認識しているため、これを行うことができます。
Test()
コンストラクターは、オブジェクトを初期化するために呼び出されます。JVM は、クラス定義フェーズからコンストラクターのコードを既に持っているため、これを行うことができます。コンストラクターは、 の現在の値を出力しx
ます0
。
- 静的変数
x
を に設定します1
。
クラスのロードが完了したのは今だけです。まだ完全にはロードされていませんが、JVM がクラスのインスタンスを作成したことに注意してください。0
コンストラクターが の初期デフォルト値を出力したため、この事実を証明できますx
。
JVM はこのクラスをロードしたので、main
メソッドを呼び出してプログラムを実行します。このmain
メソッドは、クラスの別のオブジェクトを作成しますTest
。これは、プログラムの実行の 2 番目です。再び、コンストラクターは の現在の値を出力します。x
これは現在1
です。プログラムの完全な出力は次のとおりです。
x=0
x=1
ご覧のとおり、ニワトリが先か卵が先かという問題はありません。クラスのロードを定義フェーズと初期化フェーズに分離することで、この問題を完全に回避できます。
以下のコードのように、オブジェクトのインスタンスが別のインスタンスを作成したい場合はどうでしょうか?
class Test {
Test buggy = new Test();
}
このクラスのオブジェクトを作成する場合も、固有の問題はありません。JVM は、オブジェクトをメモリに配置する方法を認識しているため、オブジェクトにメモリを割り当てることができます。すべての属性をデフォルト値に設定するため、buggy
に設定されnull
ます。次に、JVM がオブジェクトの初期化を開始します。これを行うには、 class の別のオブジェクトを作成する必要がありますTest
。以前と同様に、JVM はすでにその方法を知っています。メモリを割り当て、属性を に設定しnull
、新しいオブジェクトの初期化を開始します... つまり、同じクラスの 3 番目のオブジェクトを作成し、次に 4 番目のオブジェクトを作成する必要があります。 5 番目、というように、スタック スペースまたはヒープ メモリが不足するまで続きます。
ここに概念的な問題はありません。これは、不適切に作成されたプログラムでの無限再帰の一般的なケースにすぎません。再帰は、たとえばカウンターを使用して制御できます。このクラスのコンストラクターは、再帰を使用してオブジェクトのチェーンを作成します。
class Chain {
Chain link = null;
public Chain(int length) {
if (length > 1) link = new Chain(length-1);
}
}