Java: 类继承自身

8

我知道这样做没有意义:我只是觉得有趣,想更多地了解当您创建一个继承自自身的类时会发生什么机制,导致堆栈溢出崩溃。令人惊奇的是,Java允许您首先创建这样的构造。

我只是猜测,JVM是否将自身置于无限循环中,尝试在实例化之前解析该类,还是实际上不断实例化多个类的副本?

我应该更具体地说明一下;我正在使用内部类从封闭类派生。

 public class Outside {
    private int outsideValue;

    public class Inside extends Outside {
        private int insideValue;
        public Inside(int val) {
            insideValue = val;
        }
    }

    public Outside() {
        Inside o = new Inside(0);
    }
}

public class Main {
    public static void main(String args[]) {
        Outside o = new Outside();
    }
}

什么意思?我无法得到任何循环继承,你使用的是哪个版本?Inherit.java:1:涉及First的循环继承。 - sharpner
尝试继承正在声明的类会导致编译错误(在Windows/Cygwin上使用jdk 1.6)。您使用的Java版本是什么? - MAK
使用内部类从外部类继承。 - TurtleToes
1
javac 不是创建类文件的唯一方法。您也可以编辑一个来获取虚假类。 - vbence
一个思考问题给你;如果所有的类都隐式地扩展了Object,那么Object扩展了什么? ;) - Peter Lawrey
显示剩余6条评论
8个回答

8

请记住,由于Inside扩展了Outside,因此它具有对super()的隐式调用,该调用是Outside的构造函数(其又调用Inside的构造函数),因此循环进行。

您发布的代码在概念上与以下程序没有区别

class A {
    B b = new B();
}

class B extends A {
}

public class Test {
    public static void main(String[] args) {
        new A(); // Create an A...
                 //   ... which creates a B
                 //   ... which extends A thus implicitly creates an A
                 //   ... which creates a B
                 //   ...
    }
}

运行我发布的代码,你会得到一个堆栈溢出错误。 - TurtleToes
创建另一个类并尝试实例化 Outside 类。 - TurtleToes

2
在最终形式中,这个问题与循环继承和内部类无关。它只是由于未绑定的递归构造函数调用引起的无限递归。以下是一个简单的示例,可以展示相同的效果:
public class A {
    public A() {
        new A();
    }
}

请注意,此代码是完全有效的,因为Java不对递归调用施加任何限制。
在您的情况下,由于继承关系略微复杂,但如果您记得子类的构造函数隐式调用超类的构造函数,那么应该清楚这些调用形成无限递归。

你的例子没有反映出一个类继承另一个类的事实。请看我的答案,其中包含一个更接近原始问题的例子。 - aioobe
我已经超过48小时没有睡觉了,而且我已经喝了第五罐巨型红牛...一开始对我来说似乎很棒。 - TurtleToes

1
你发布的示例如果再稍微改动一下就可能会出现问题:
public class Outside {

    public class Inside extends Outside {

            public Inside(int val) {
        }

    }

    private Inside i;

    public Outside() {
        i = new Inside();
    }
}

但这与 InsideOutside 的内部类并不相关,如果是两个独立的顶级类也可能发生相同的情况。


这里有一个现实生活中的例子,其中有人用这个问题构建了一些东西。 - Paŭlo Ebermann
我要说,这样的例子是由糟糕的设计引起的。我无法想象出一个有效的面向对象模型会考虑到循环继承。由于循环继承甚至在真实系统中都不存在,而我们的任务是对这些真实系统进行建模和构建软件表示,因此可以得出结论,循环继承没有位置(或者不应该有)。 - luis.espinal
有一些面向对象的语言允许循环继承,但这意味着循环中的所有类都是同义词关系。(如果我没记错的话,Cecil就是一个例子。)然而,这仍然与混合使用内部类的继承无关。 - Paŭlo Ebermann

1

在像eclipse这样的IDE中尝试,它不允许您这样做。即会显示类似以下错误:

检测到循环:Test 类型无法扩展/实现自身或其成员类型之一


但这是IDE比编译器更聪明(一件好事)。我惊讶的是Java编译器居然允许这样的结构存在。 - luis.espinal

1

扩展自身会产生循环继承错误(Java不允许)。您的代码示例可以编译并且有效。


由于 Vladimir Ivanov的坚持,我将修复我的编辑。

你的代码抛出了StackOverflowError,原因如下。

Inside o = new Inside(0);

由于Inside扩展了OutsideInside首先隐式地调用super()方法(因为你自己没有调用它)。Outside()构造函数初始化Inside o,然后循环再次运行,直到栈满并溢出(在堆栈中有太多的InsideOutside)。

希望这对Vladimir Ivanov尤其有帮助。


这与循环继承无关(-1)。 - Vladimir Ivanov
@Vladimir Ivanov,由于OP后来添加了代码,因此我添加了第二句话。 - Buhake Sindi
是的。这不是我的意愿。顺便说一下,SO不允许您在回答被编辑之前更改投票。 - Vladimir Ivanov

1
Java编译器在尝试进入循环继承链时不会陷入无限循环。毕竟,每个继承链都是最终有限的图形(从计算上讲,节点和边的数量非常少)。更准确地说,从子类A到(最终)超类Z的继承图必须是一条线(反过来不行),编译器可以轻松确定它是否是一条线。
程序不需要太多就能确定这样一个小图是否是循环的或者是否是一条线,这就是编译器所做的。因此,编译器不会进入无限循环,JVM也永远不会耗尽堆栈空间,因为1)编译器不在JVM上运行,2)JVM不会执行(因为没有任何东西被编译,编译器在这种情况下从未调用JVM)。
我不知道有哪些语言允许这样的循环继承图(但我已经做了11年的Java,所以我对除Java以外的任何东西的记忆都模糊了)。此外,我也看不出这样的结构在建模或现实生活中有什么用处。虽然可能在理论上有趣。

好的,我运行了你的代码,确实导致了堆栈溢出。你是对的。我需要坐下来认真研究一下,才能理解为什么编译器允许这样的结构。

发现得很好!!!


谢谢...我刚刚坐在这里试图破坏东西,结果它就坏了。 - TurtleToes
1
@luis.espinal,这个程序没有什么特别的地方。堆栈溢出是由于两个相互递归的构造函数引起的。请看我的答案。 - aioobe
我想通了...一开始让我措手不及...但是,我仍然无法相信它会让你制作如此明显的循环引用。 - TurtleToes
@aioobe - 不,不是的,我知道这两个构造函数是相互递归的(顺便说一句,你的回答很好)。我需要考虑的是为什么编译器会允许这样的结构存在。在一个有效的模型上,继承链不应该是循环的,内部成员也不应该扩展它们所包含的类型。这真的不是很好的建模,语义值得怀疑。我很惊讶编译器居然允许这样做。不过还是谢谢你的回复。 - luis.espinal
语义很清楚:这是一种无限递归,如果在拥有无限内存的计算机上运行,它将永远运行下去。实际上,如果编译器或JVM够聪明,它会将其编译成具有等价语义的 while (true) {}。正如您可能意识到的那样,禁止这个Java程序与禁止一个 while(true) {} 程序一样没有多少理由。 - aioobe
我非常怀疑编译器会采取将相互递归函数转换为带有空体的无限循环的步骤。与后者(其检测是可判定的)不同,相互递归函数可能具有停止条件,也可能没有(除了最简单的情况外,检测这些条件是不可判定的)。然而,值得注意的是,为什么编译器不禁止这样的结构。话虽如此,智能IDE确实会标记循环继承条件。 - luis.espinal

0

你可以通过以下方式获得答案:

Class.forName("MyClass");

这样它就被解决了,但没有实例化。因此,您可以检查解析本身是否会导致崩溃。

我猜这取决于您使用的JVM。


0

当我尝试编译时:

class A extends A {
}

我得到:

$ javac A.java
A.java:1: cyclic inheritance involving A
class A extends A {
^
1 error

所以Java不允许你做这种事情。有关信息,java version "1.6.0_24"


1
这与环绕继承无关(-1),而是关于内部类继承其封闭类。 - Vladimir Ivanov
@Vladimir 我的回答是在问题添加代码之前写的。没有代码,我们无法知道 OP 是否在谈论内部类(“你创建一个继承自身的类”这正是我所做的)。 - krtek
@Krtel,如果答案再次被编辑,我会撤回我的踩。 - Vladimir Ivanov

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