在类本身中创建类的实例绝对没有问题。在编译程序和运行时,以不同的方式解决明显的先有鸡还是先有蛋的问题。
编译时
当编译创建自身实例的类时,编译器会发现该类对自身具有循环依赖关系。这种依赖关系很容易解决:编译器知道该类已经在编译中,因此它不会尝试再次编译它。相反,它假装该类已存在,并相应地生成代码。
运行时
一个类创建自己的对象的最大先有鸡还是先有蛋的问题是当类甚至还不存在时;也就是说,在装入类时。通过将类加载分为两个步骤来解决此问题:首先定义类,然后对其进行初始化。
定义意味着向运行时系统(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 加载类。这意味着首先定义了类,以便JVM知道Test
- 一个名为 exist 的类,并且它具有一个方法和一个构造函数,并且
Test
main
- 该类有两个静态变量,一个调用,另一个调用 ,和
Test
x
instance
- 类的对象布局是什么。换句话说:一个物体是什么样子的;它有什么属性。在这种情况下,没有任何实例属性。
Test
Test
现在,类已定义,它已初始化。首先,将默认值 or 分配给每个静态属性。这将设置为 。然后,JVM 按源代码顺序执行静态字段初始值设定项。有两个:0
null
x
0
- 创建该类的实例并将其分配给 。创建实例有两个步骤:
Test
instance
- 为对象分配第一个内存。JVM 可以做到这一点,因为它已经从类定义阶段就知道了对象布局。
- 调用构造函数来初始化对象。JVM 可以执行此操作,因为它已经具有类定义阶段的构造函数代码。构造函数打印出 的当前值 ,即 。
Test()
x
0
- 将静态变量设置为 。
x
1
直到现在,类才完成加载。请注意,JVM 创建了该类的一个实例,即使它尚未完全加载。您有此事实的证明,因为 构造函数打印出了 的初始默认值。0
x
现在 JVM 已经加载了此类,它将调用该方法来运行程序。该方法创建另一个类对象 - 程序执行中的第二个对象。构造函数再次打印出 的当前值,即现在的 。该程序的完整输出是:main
main
Test
x
1
x=0
x=1
正如你所看到的,没有先有鸡还是先有蛋的问题:将类加载分离到定义和初始化阶段完全避免了这个问题。
当对象的实例想要创建另一个实例时,该怎么办,如下面的代码所示?
class Test {
Test buggy = new Test();
}
创建此类的对象时,同样不存在固有问题。JVM 知道对象在内存中应如何布局,以便它可以为其分配内存。它将所有属性设置为其默认值,因此设置为 。然后,JVM 开始初始化对象。为此,它必须创建类的另一个对象。像以前一样,JVM已经知道如何做到这一点:它分配内存,将属性设置为,然后开始初始化新对象...这意味着它必须创建同一类的第三个对象,然后创建第四个,第五个,依此类推,直到它耗尽堆栈空间或堆内存。buggy
null
Test
null
这里没有概念问题:这只是一个写得不好的程序中无限递归的常见情况。递归可以控制,例如使用计数器;此类的构造函数使用递归来创建对象链:
class Chain {
Chain link = null;
public Chain(int length) {
if (length > 1) link = new Chain(length-1);
}
}