如何在类本身内部创建类的实例?

2022-09-01 04:33:52

是什么使得在类本身内部创建类的实例成为可能?

public class My_Class
 {

      My_Class new_class= new My_Class();
 }

我知道这是可能的,并且我自己也做过,但我仍然不能让自己相信这不是“谁是第一个 - 先有鸡还是先有蛋?”类型的问题。我很高兴收到一个答案,从编程角度以及JVM /编译器的角度来澄清这一点。我认为理解这一点将有助于我清除OO编程的一些非常重要的瓶颈概念。

我收到了一些答案,但没有一个像我预期的那样清楚。


答案 1

在类本身中创建类的实例绝对没有问题。在编译程序和运行时,以不同的方式解决明显的先有鸡还是先有蛋的问题。

编译时

当编译创建自身实例的类时,编译器会发现该类对自身具有循环依赖关系。这种依赖关系很容易解决:编译器知道该类已经在编译中,因此它不会尝试再次编译它。相反,它假装该类已存在,并相应地生成代码。

运行时

一个类创建自己的对象的最大先有鸡还是先有蛋的问题是当类甚至还不存在时;也就是说,在装入类时。通过将类加载分为两个步骤来解决此问题:首先定义类,然后对其进行初始化

定义意味着向运行时系统(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

  1. 一个名为 exist 的类,并且它具有一个方法和一个构造函数,并且Testmain
  2. 该类有两个静态变量,一个调用,另一个调用 ,和Testxinstance
  3. 类的对象布局是什么。换句话说:一个物体是什么样子的;它有什么属性。在这种情况下,没有任何实例属性。TestTest

现在,类已定义,它已初始化。首先,将默认值 or 分配给每个静态属性。这将设置为 。然后,JVM 按源代码顺序执行静态字段初始值设定项。有两个:0nullx0

  1. 创建该类的实例并将其分配给 。创建实例有两个步骤:Testinstance
    1. 为对象分配第一个内存。JVM 可以做到这一点,因为它已经从类定义阶段就知道了对象布局。
    2. 调用构造函数来初始化对象。JVM 可以执行此操作,因为它已经具有类定义阶段的构造函数代码。构造函数打印出 的当前值 ,即 。Test()x0
  2. 将静态变量设置为 。x1

直到现在,类才完成加载。请注意,JVM 创建了该类的一个实例,即使它尚未完全加载。您有此事实的证明,因为 构造函数打印出了 的初始默认值。0x

现在 JVM 已经加载了此类,它将调用该方法来运行程序。该方法创建另一个类对象 - 程序执行中的第二个对象。构造函数再次打印出 的当前值,即现在的 。该程序的完整输出是:mainmainTestx1

x=0
x=1

正如你所看到的,没有先有鸡还是先有蛋的问题:将类加载分离到定义和初始化阶段完全避免了这个问题。

当对象的实例想要创建另一个实例时,该怎么办,如下面的代码所示?

class Test {
    Test buggy = new Test();
}

创建此类的对象时,同样不存在固有问题。JVM 知道对象在内存中应如何布局,以便它可以为其分配内存。它将所有属性设置为其默认值,因此设置为 。然后,JVM 开始初始化对象。为此,它必须创建类的另一个对象。像以前一样,JVM已经知道如何做到这一点:它分配内存,将属性设置为,然后开始初始化新对象...这意味着它必须创建同一类的第三个对象,然后创建第四个,第五个,依此类推,直到它耗尽堆栈空间或堆内存。buggynullTestnull

这里没有概念问题:这只是一个写得不好的程序中无限递归的常见情况。递归可以控制,例如使用计数器;此类的构造函数使用递归来创建对象链:

class Chain {
    Chain link = null;
    public Chain(int length) {
        if (length > 1) link = new Chain(length-1);
    }
}

答案 2

其他答复大多涵盖了这个问题。如果它有助于将大脑包裹起来,那么举个例子怎么样?

鸡和蛋的问题得到解决,因为任何递归问题是:基例不会继续产生更多的工作/实例/任何东西。

假设您已经将一个类放在一起,以便在必要时自动处理跨线程事件调用。与线程化 WinForms 高度相关。然后,您希望该类公开一个事件,该事件在处理程序中注册或注销时发生,并且自然它也应该处理跨线程调用。

您可以编写两次处理它的代码,一次用于事件本身,一次用于状态事件,或者编写一次并重用。

该类的大部分内容已被修剪掉,因为它与讨论并不真正相关。

public sealed class AutoInvokingEvent
{
    private AutoInvokingEvent _statuschanged;

    public event EventHandler StatusChanged
    {
        add
        {
            _statuschanged.Register(value);
        }
        remove
        {
            _statuschanged.Unregister(value);
        }
    }

    private void OnStatusChanged()
    {
        if (_statuschanged == null) return;

        _statuschanged.OnEvent(this, EventArgs.Empty);
    }


    private AutoInvokingEvent()
    {
        //basis case what doesn't allocate the event
    }

    /// <summary>
    /// Creates a new instance of the AutoInvokingEvent.
    /// </summary>
    /// <param name="statusevent">If true, the AutoInvokingEvent will generate events which can be used to inform components of its status.</param>
    public AutoInvokingEvent(bool statusevent)
    {
        if (statusevent) _statuschanged = new AutoInvokingEvent();
    }


    public void Register(Delegate value)
    {
        //mess what registers event

        OnStatusChanged();
    }

    public void Unregister(Delegate value)
    {
        //mess what unregisters event

        OnStatusChanged();
    }

    public void OnEvent(params object[] args)
    {
        //mess what calls event handlers
    }

}