36

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

public class My_Class
 {

      My_Class new_class= new My_Class();
 }

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

我收到了一些答案,但都没有达到我预期的程度。

4

5 回答 5

48

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

编译时

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

运行

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

定义意味着将类注册到运行时系统(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 知道

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

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

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

只是现在类已经完成加载。请注意,JVM 创建了该类的一个实例,即使它还没有完全加载。你有这个事实的证据,因为构造函数打印0x.

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

x=0
x=1

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

当对象的一个​​实例想要创建另一个实例时怎么办,就像下面的代码一样?

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

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

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

class Chain {
    Chain link = null;
    public Chain(int length) {
        if (length > 1) link = new Chain(length-1);
    }
}
于 2013-08-21T15:12:23.690 回答
2

我总是看到自己在类中创建一个实例的主要事情是,当我试图在静态上下文中引用非静态项目时,例如当我为游戏制作框架或其他什么时,我使用 main方法来实际设置框架。您还可以在构造函数中有要设置的内容时使用它(如下所示,我使 JFrame 不等于 null):

public class Main {
    private JFrame frame;

    public Main() {
        frame = new JFrame("Test");
    }

    public static void main(String[] args) {
        Main m = new Main();

        m.frame.setResizable(false);
        m.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        m.frame.setLocationRelativeTo(null);
        m.frame.setVisible(true);
    }
}
于 2013-09-19T23:02:43.077 回答
1

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

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

想象一下,您已经组合了一个类来在必要时自动处理跨线程事件调用。与线程化 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
    }

}
于 2013-09-18T20:41:53.017 回答
0

在对象内部创建一个对象的实例可能会导致 StackOverflowError ,因为每次从这个“ Test”类创建一个实例时,你都会创建另一个实例和另一个实例等等。尽量避免这种做法!

public class Test  {

    public Test() {  
        Test ob = new Test();       
    }

    public static void main(String[] args) {  
        Test alpha = new Test();  
    }  
}
于 2013-08-21T15:17:14.060 回答
0

保存 self 实例的属性应该是静态的

public class MyClass {

    private static MyClass instance;

    static {
        instance = new MyClass();
    }

    // some methods

}
于 2019-07-17T15:02:47.470 回答