在类本身中创建类的实例绝对没有问题。在编译程序和运行程序时,可以通过不同方式解决明显的“鸡或蛋”问题。
编译时间
在编译创建自身实例的类时,编译器会发现该类对其自身具有循环依赖关系。这种依赖性很容易解决:编译器知道该类已经在编译,因此它不会尝试再次编译它。相反,它假装该类已经存在会相应地生成代码。
运行
类创建自身对象的最大问题是,该类甚至不存在。也就是说,当类被加载时。通过将类加载分为两个步骤来解决此问题:首先 定义 类,然后将其 初始化 。
定义意味着将类注册到运行时系统(JVM或CLR),以便它知道类对象具有的结构,以及在调用其构造函数和方法时应运行什么代码。
一旦定义了类,就将其初始化。这是通过初始化静态成员并运行静态初始化程序块以及用特定语言定义的其他操作来完成的。回想一下,此时已经定义了该类,因此运行时知道该类的对象是什么样,应该运行哪些代码来创建它们。这意味着初始化类时创建类的对象没有任何问题。
以下示例说明了Java中类初始化和实例化如何交互:
class Test { static Test instance = new Test(); static int x = 1; public Test() { System.out.printf("x=%dn", x); } public static void main(String[] args) { Test t = new Test(); }}让我们逐步介绍JVM如何运行该程序。首先,JVM加载
Test该类。这意味着首先 定义 了该类,以便JVM知道
- 一个叫做
Test
exist 的类,它具有一个main
方法和一个构造函数,并且 - 在
Test
类有两个静态变量,一个叫x
和另一个叫instance
,和 Test
类的对象布局是什么?换句话说:一个对象看起来像什么;它具有什么属性。在这种情况下Test
,没有任何实例属性。
现在定义了类,就将其 初始化了
。首先,将默认值
0或
null分配给每个静态属性。设置
x为
0。然后,JVM按源代码顺序执行静态字段初始化程序。那里有两个:
- 创建
Test
该类的实例并将其分配给instance
。创建实例有两个步骤:- 首先为对象分配了内存。JVM之所以可以这样做,是因为它已经从类定义阶段知道了对象的布局。
- 该
Test()
构造函数初始化对象。JVM之所以可以这样做,是因为它已经具有来自类定义阶段的构造函数代码。构造函数打印出的当前值x
,即0
。
- 将静态变量设置
x
为1
。
直到现在,该课程已完成加载。请注意,JVM创建了该类的实例,即使它尚未完全加载。你有这个事实证明,因为构造函数打印出初始默认值
0的
x。
现在,JVM已经加载了此类,它调用
main方法来运行程序。该
main方法创建另一个类对象
Test-程序执行中的第二个对象。构造函数再次输出当前的值
x,现在是
1。该程序的完整输出为:
x=0x=1
如您所见,这里没有鸡或蛋的问题:将类加载分为定义阶段和初始化阶段完全可以避免该问题。
当对象的一个实例想要创建另一个实例时(如下面的代码),该怎么办?
class Test { Test buggy = new Test();}当您创建此类的对象时,同样没有固有的问题。JVM知道如何将对象布置在内存中,以便可以为其分配内存。它将所有属性设置为其默认值,因此
buggy设置为
null。然后,JVM开始初始化对象。为此,它必须创建class的另一个对象
Test。像以前一样,JVM已经知道该怎么做:它分配内存,将属性设置为
null,并开始初始化新对象……这意味着它必须创建相同类的第三个对象,然后创建第四个对象。第五,依此类推,直到它耗尽堆栈空间或堆内存为止。
在这里,您不会有任何概念上的问题:这只是编写不良程序中无限递归的常见情况。可以使用例如计数器来控制递归;此类的构造函数使用递归来创建对象链:
class Chain { Chain link = null; public Chain(int length) { if (length > 1) link = new Chain(length-1); }}


