是什么使得在类本身内部创建类的实例成为可能?
public class My_Class
{
My_Class new_class= new My_Class();
}
我知道这是可能的并且已经自己完成但是我仍然不能让自己相信这不是“谁是第一个 - 鸡还是鸡蛋?”问题的类型。我很高兴收到一个答案,从编程角度以及从JVM /编译器的角度来澄清这一点。我认为理解这将有助于我清除OO编程的一些非常重要的瓶颈概念。
我收到了一些答案,但没有一个人能达到预期的程度。
答案 0 :(得分:36)
在类本身中创建类的实例绝对没有问题。在编译程序和运行程序时,明显的鸡或蛋问题以不同的方式解决。
<强>编译时强>
当编译创建自身实例的类时,编译器会发现该类本身具有circular dependency。这种依赖很容易解决:编译器知道该类已经被编译,所以它不会再尝试编译它。相反,它假装该类已经存在,相应地生成代码。
<强>运行时强>
创建自身对象的类中最大的鸡或蛋问题是当班级尚未存在时;也就是说,当加载类时。通过将类加载分为两个步骤来解决此问题:首先,类是定义的,然后是初始化。
定义意味着使用运行时系统(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知道
Test
的类存在,它有一个main
方法和一个构造函数,而且Test
类有两个静态变量,一个名为x
,另一个名为instance
,Test
类的对象布局是什么。换句话说:对象是什么样的;它有什么属性。在这种情况下,Test
没有任何实例属性。现在已经定义了类,它是初始化。首先,默认值0
或null
将分配给每个静态属性。这会将x
设置为0
。然后JVM以源代码顺序执行静态字段初始值设定项。有两个:
Test
类的实例并将其分配给instance
。实例创建有两个步骤:
Test()
构造函数来初始化对象。 JVM可以这样做,因为它已经从类定义阶段获得了构造函数的代码。构造函数打印出x
的当前值,即0
。x
设置为1
。现在只有课程已完成加载。请注意,JVM创建了该类的实例,即使它尚未完全加载。您有证据证明这一事实,因为构造函数打印出0
的初始默认值x
。
现在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);
}
}
答案 1 :(得分: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);
}
}
答案 2 :(得分: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
}
}
答案 3 :(得分:0)
用于保存自身实例的属性应为静态
public class MyClass {
private static MyClass instance;
static {
instance = new MyClass();
}
// some methods
}
答案 4 :(得分:-1)
在对象内部创建对象的实例可能会导致StackOverflowError,因为每次从这个“Test
”类创建实例时,您将创建另一个实例和另一个实例,依此类推。避免这种做法!
public class Test {
public Test() {
Test ob = new Test();
}
public static void main(String[] args) {
Test alpha = new Test();
}
}