在类内部创建类的实例是如何工作的?

40

在类内部创建一个类实例的可能性是什么?

public class My_Class
 {

      My_Class new_class= new My_Class();
 }

我知道这是可能的,我自己也做过,但我仍然无法相信这不是像“先有鸡还是先有蛋”的问题一样的问题。我很高兴能够从编程角度以及JVM /编译器角度得到一个能够澄清这个问题的答案。我认为理解这一点将有助于我消除面向对象编程中一些非常重要的瓶颈概念。

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


2
只要你不在MyClass的构造函数中创建一个MyClass(无限递归),那么这样做就没有任何问题。组合设计模式甚至是基于此的。我真的不明白问题在哪里。另外,public void class永远不会编译。 - Pierre-Luc Pineault
我知道这是可以做到的,但我的问题是:这不是就像你在没有创建函数的情况下使用函数吗?你如何向一个只懂函数式编程的人解释这个问题? - Jack_of_All_Trades
类加载和实例化过程的哪个部分仍然不清楚? - Joni
@Joni:当我尝试按照你说的去可视化答案时,仍然不太清楚。能否请您详细阐述一下,在编译器或JVM如何处理类实例化以及在类内声明同一类对象的情况下,它是如何不涉及到像我问的“鸡还是蛋”的问题的? 您的答案给了我一些好的线索,但从内心深处,我认为我仍然缺少一些东西来清楚地理解它。因为我的背景不是计算机科学,可能阻止我正确地表达我的问题。有时候,我知道我在Missing sth ,但无法解释清楚。 - Jack_of_All_Trades
我不知道这在Java中是否可行,但在C#中会因为无限递归而导致堆栈溢出异常。 - Habib
5个回答

53

在类本身创建实例没有任何问题。当程序被编译和运行时,明显的鸡生蛋问题有不同的解决方法。

编译时

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

运行时

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

定义意味着向运行时系统(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没有任何实例属性。
现在类已经定义好了,它被初始化了。首先,将默认值0或null分配给每个静态属性。这将x设置为0。然后JVM按源代码顺序执行静态字段初始化器。共有两个:
  1. 创建一个Test类的实例并将其分配给instance。实例创建有两个步骤:
    1. 首先为对象分配内存。JVM可以做到这一点,因为它已经从类定义阶段知道了对象的布局。
    2. 调用Test()构造函数来初始化对象。JVM可以做到这一点,因为它已经从类定义阶段获得了构造函数的代码。构造函数打印出x的当前值,即0
  2. 将静态变量x设置为1

只有现在类才完成加载。请注意,JVM创建了一个类的实例,即使它还没有完全加载。您可以证明这一事实,因为构造函数打印出x的初始默认值0

现在JVM已经加载了这个类,它调用main方法来运行程序。 main方法创建了另一个Test类的对象 - 程序执行中的第二个对象。再次,构造函数打印出x的当前值,现在是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);
    }
}

这是否意味着创建类实例的类定义内部表达式在其他表达式之前不会被执行/编译?您说,在这一点上,类已经被定义了,这是正确的,但是实例如何能够访问在此之前未定义的方法。您能否详细解释一下你的答案,以便向始终使用解释性语言(意味着代码按行执行)的人解释这个问题。 - Jack_of_All_Trades
所有的方法、构造函数和其他成员都是在类定义时定义的,在类中的任何代码执行之前。 - Joni
请帮我澄清一下,我的代码中,类的实例难道不也是该类的成员吗?我正在努力理清思路,Joni。 - Jack_of_All_Trades
成员是诸如变量和方法之类的东西。成员变量可以保存对象的实例。类的实例不是成员变量,但成员变量可以保存类的实例,就像int变量不是整数,但可以保存整数一样。清楚吗? - Joni

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
    }

}

2

我经常在类内部创建实例的主要原因是,在静态上下文中引用非静态项目时,例如当我为游戏创建框架或其他内容时,我使用主方法来设置框架。当构造函数中有想要设置的东西时(如在下面的示例中,我使我的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);
    }
}

0

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

public class Test  {

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

    public static void main(String[] args) {  
        Test alpha = new Test();  
    }  
}

1
只有在构造函数中无条件调用自身才会导致无限递归。如果在构造函数外(例如在Clone方法中)调用,则不适用此规则。如果是有条件地调用,则仍然是递归,但不会导致无限递归。 - Servy
@Servy,在声明时实例化相同类对象也会导致无限递归和堆栈溢出异常。这不仅仅是构造函数的问题。这段代码*(与问题相同)*将在运行时引发堆栈溢出异常public class My_Class{My_Class new_class = new My_Class();},而且我不确定Java是否有所不同,但在C#中,它将是一个异常。 - Habib
@Habib 好的,字段初始化器被移动到构造函数中,但是如果你想要明确,将一个类的实例作为实例字段初始化器(如果它不是有条件的)会导致无限递归。 - Servy

0

应该将保存self实例的属性设置为静态的

public class MyClass {

    private static MyClass instance;

    static {
        instance = new MyClass();
    }

    // some methods

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接