为什么可以在其定义中实例化类?

一位同事(对Java来说很陌生)今天停下来问了一个看似简单的问题。 不幸的是,我做了一个绝对可怕的工作,试图向他解释。 他有一本书的代码看起来像这样:

class XCopy { public static void main(String[] args) { XCopy x = new XCopy(); // 1 x.doIt(); } public void doIt() { // Some code... } } 

他在第1行感到困惑。他想知道的是为什么可以在XCopy类的定义中创建一个新的XCopy实例。 他认为这会产生某种前向引用错误。 毕竟,我们还没有宣布XCopy类是什么,所以我们怎么能创建一个呢?

我当然知道这是有效的代码,但是当我试图向他解释时,我发现自己磕磕绊绊的答案,我担心他比他开始时更加困惑。 我想听听其他解释为什么会有效。

有什么想法吗? 为什么你可以在类的定义中实例化一个类的实例呢?

您正在编译时定义类,其所有字段和方法等。 直到运行时才创建实例。 所以没有矛盾,课程完全由你到达第1行的时间来定义。

正如其他人所指出的那样,因为main方法是static ,所以你会在没有实例化对象的情况下到达第1行,但是你可以毫无问题地这样做。 我一直用这种模式进行一类实验。

因为代码是先编译的,然后再执行。 所有编译器都需要知道validation该行是否存在名为XCopy的类,并且它具有无参数构造函数。 它不需要知道关于class级的一切。

它不是C / C ++意义上的前向引用。 您的主要方法是将类作为其自身上下文中的类型引用。 你没有“领先”任何东西。

main是静态的事实并不是密切相关的,因为即使对于非静态方法,它仍然可以工作:

 public class Foo { private String x; public Foo(String x) { this.x = x; } public Foo(Foo f) { this.x = fx; } // copy constructor; still compiles fine, even without static } 

一个区别是编译和链接。 C / C ++有单独的编译和链接步骤。 Java有一个类加载器。 我认为使用类加载器编译为字节代码并在运行时根据需要加载是Java和C / C ++之间的细微差别,这解释了为什么不需要前向引用的想法,但我不确定。

Class只是一个蓝图, 描述了类的每个实例的样子和行为。 根据类及其构造函数的可见性,同一个类,同一个包或完全陌生人中的代码可能会创建实例。

例如,在构造函数不应该是公共的类中提供工厂方法是常见的:

 public class Foo { // only I get to create new instances private Foo() { } // but you can get instances through this factory method public static Foo createFoo() { return new Foo(); } } 

你可以在第42行调用一个直到第78行才定义的方法的原因相同吗? Java不是一种脚本语言,因此在使用之前不必声明事物(实际上,某些脚本语言也是如此)。 类定义在编译时被视为一个整体。

您甚至可以在自己的构造函数中实例化类的对象:

 public class Test { Test a; Test() { a = new Test(); } public static void main(String[] args) { System.out.println(new Test()); } } 

这会产生……等待它……一个java.lang.StackOverflowError

如果您的同事来自C或pascal编程背景,那么这个问题绝对符合逻辑。 在C程序中,必须在首次使用它们的行之上声明方法。 由于按此顺序对函数进行排序并不总是切实可行的,因此有前向声明只提供方法名称,返回类型和参数,而不定义函数体:

 // forward declaration void doSomething(void); void doSomethingElse(void) { doSomething(); } // function definition void doSomething(void) { ... } 

这样做是为了简化解析器的创建并允许更快的解析,因为需要更少的源传递。 但是,在Java中,允许在定义点之前使用标识符。 因此,解析必须分几个阶段进行。 在构建对应于源代码的语法树之后,遍历该树以确定类或方法的所有定义。 方法体在稍后阶段处理,此时有关范围内名称的所有信息都是已知的。

因此,只要处理main方法的方法体,编译器就会知道类的默认构造函数及其doIt方法,并且可以生成正确的字节码来调用此方法。

因为主要方法是静态的。 而通过静态,这意味着该方法不属于该类的任何特定实例。 也就是说,可以在不创建实例的情况下访问它。

因此,为了调用非静态的doIt方法,必须创建一个保存它的类的实例。

只要第一次“提到”,jvm就会加载类。 然后没有什么可以阻止实例化 – 它已经被加载了

类通过类加载器加载到内存中,并在发生以下任何情况时进行初始化。

1)使用new()关键字或使用class.forName()的reflection创建类的实例,这可能会在Java中抛出ClassNotFoundException。

2)调用Class的静态方法。

3)分配类的静态字段。

4)使用类的静态字段,它不是常量变量。

5)如果Class是顶级类,则执行词法中嵌套在类中的断言语句。

因此,通过第1行,class既被加载又被初始化,因此在实例化类本身实例时没有问题。

但如果您的代码是这样的,

 class Test { Test test2 = new Test(); public static void main(String[] args) { Test test1 = new Test(); } } the above code will result in stackoverflow exception. class Test { public static void main(String[] args) { Test test = new Test(); } } In the above code creating an instance won't call main again. Remember, main is a static method, not tied to any particular instance. class Test { static Test test2 = new Test(); public static void main(String[] args) { Test test1 = new Test(); } } This code will also run fine. 

阅读更多内容来自https://javarevisited.blogspot.in/2012/07/when-class-loading-initialization-java-example.html