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

时间:2011-03-03 20:32:40

标签: java

一位同事(他是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类是什么,所以我们怎么能创建一个?

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

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

9 个答案:

答案 0 :(得分:33)

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

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

答案 1 :(得分:12)

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

答案 2 :(得分:5)

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

main是静态的事实并不是密切相关的,因为它甚至可以用于非静态方法:

public class Foo
{
   private String x;

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

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

答案 3 :(得分:4)

一个类只是一个蓝图,描述类的每个实例看起来像什么样子。根据类及其构造函数的可见性,同一个类,同一个包中的代码或完全陌生的代码可能会创建实例。

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

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();
    }
}

答案 4 :(得分:3)

你可以在第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

答案 5 :(得分:3)

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

// forward declaration
void doSomething(void);

void doSomethingElse(void) {
    doSomething();
}

// function definition
void doSomething(void) {
    ...
}

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

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

答案 6 :(得分:1)

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

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

答案 7 :(得分:1)

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

答案 8 :(得分:-1)

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

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

2)调用Class的静态方法。

3)分配了一个Class的静态字段。

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

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

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

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

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

了解详情