在Java中使用大括号出现奇怪的行为

40

当我运行下面的代码时:

public class Test {

  Test(){
    System.out.println("1");
  }

  {
    System.out.println("2");
  }

  static {
    System.out.println("3");
  }

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

我希望按照这个顺序输出:

1
2
3

但我得到的顺序是相反的:

3
2
1

有人能解释一下为什么它会反向输出吗?

================

另外,当我创建多个Test实例时:

new Test();
new Test();
new Test();
new Test();

静态块只在第一次执行。


11
+1 还请参阅§8.7 静态初始化器 - trashgod
14
你为什么认为这种行为奇怪呢?为什么你期望这些块按照你描述的顺序运行? - Daniel Pryden
完整的解释请看下面我的回答。 - gprathour
9个回答

62
一切都取决于初始化语句的执行顺序。您的测试表明,此顺序为:
  1. 静态初始化块
  2. 实例初始化块
  3. 构造函数
编辑 感谢评论,现在我可以引用JVM规范中适当的部分了。 在这里,详细的初始化过程。

11
挑剔一下:这个测试并不能证明什么。它只是展示了你所描述的顺序。如果想要证明,你需要查看Java语言规范。 - ObscureRobot
6
建议您查看虚拟机规范而非语言规范,具体来说是第2.17节。(注意,某些初始化程序按照文本顺序执行)。"double nitty pick"的意思可能是过于琐碎或繁琐的事情。 - ccoakley
3
无论你称之为“证明”或“展示”,它都是同一件事:一个经验事实。规格说明了应该发生什么,但这可能与实际发生的情况不符。考虑到原帖作者期望得到其他结果,任何一个词都是适当的(尽管我承认“展示”更好一些)。 - jmoreno
3
不,证明和演示是非常不同的。一般理论和个别经验证据也是如此。仅仅因为你今天在你的桌面上观察到某种行为并不意味着它明天还会存在。可能只是观察到当前实现的怪癖,下次更新您的JDK或JRE时就会消失。这种差异微妙但很重要。在软件开发中,“证明”几乎没有任何用处...除非您正在与老板讨论新硬件的预算 :) - ObscureRobot

30

3 - 是一个静态初始化器,它会在类被加载时运行一次,这是最先发生的。

2 - 是一个初始化块,Java编译器实际上会将其复制到每个构造函数中,所以如果需要在多个构造函数之间共享某些初始化内容,可以使用此方法。但很少使用。

1 - 在 (3) 和 (2) 之后,在构造对象时执行。

在此处获取更多信息


18

首先执行静态代码块。

然后执行实例化初始化块。

请参阅JLS中的实例初始化器

{

// SOP语句

}

与构造函数一样,在实例初始化块中不能有返回语句。


5
首先,类被加载到JVM中并进行类初始化。在此过程中,静态块被执行。"{...}"只是"static{...}"的语法等价物。由于代码中已经有一个"static{...}"块,因此"{...}"将附加到它上面。这就是为什么你会在2之前看到3的原因。
接下来,一旦类被加载,java.exe(我假设你从命令行执行)将找到并运行主方法。主静态方法初始化实例,其构造函数被调用,所以最后打印出"1"。

块打印2是一个实例初始化器:http://java.sun.com/docs/books/jls/third_edition/html/classes.html#8.6。它与静态初始化器不相等。 - Samuel Edwin Ward

5
Test(){System.out.println("1");}

    {System.out.println("2");}

    static{System.out.println("3");}

静态内容首先被执行,{System.out.println("2");}不是函数的一部分,因为它的作用域最先被调用,而Test(){System.out.println("1");}是最后被调用的,因为其他两个先被调用。


4

看起来没有人明确说明为什么只有3被明确打印了一次。因此我会补充说明这与它为什么首先被打印有关。

静态定义的代码被标记为与类的任何特定实例分开。通常,可以将静态定义的代码视为根本不是任何类(当然,考虑到作用域时,这个说法有些无效)。因此,该代码在加载类时运行一次,如上所述,并且不会在构造实例Test()时被调用,因此多次调用构造函数不会再运行静态代码。

包含2的括号代码添加到构造中,如上所述,因为它是所有类构造函数的一种前提条件。您不知道在Test的构造函数中会发生什么,但可以保证它们都从打印2开始。因此,这发生在任何特定构造函数中的任何内容之前,并且每次调用任何构造函数时都会调用它。


4

static{} 代码块在 JVM 中首次初始化类时运行(即甚至在调用 main() 之前),而实例 {} 在实例首次初始化之前被调用,然后构造函数在所有这些操作完成后被调用。


4
我用ASM得到了类似字节码的代码。
我认为这可以回答你的问题,解释在这种情况下创建对象时发生了什么。
public class Test {
static <clinit>() : void
GETSTATIC System.out : PrintStream
LDC "3"
INVOKEVIRTUAL PrintStream.println(String) : void
RETURN


<init>() : void
ALOAD 0: this
INVOKESPECIAL Object.<init>() : void
GETSTATIC System.out : PrintStream
LDC "2"
INVOKEVIRTUAL PrintStream.println(String) : void
GETSTATIC System.out : PrintStream
LDC "1"
INVOKEVIRTUAL PrintStream.println(String) : void
RETURN

public static main(String[]) : void
NEW Test
INVOKESPECIAL Test.<init>() : void
RETURN
}

我们可以看到 LDC "3" 在 "clinit" 中,这是一个类初始化器。
一个对象的生命周期通常是:加载类 -> 链接类 -> 类初始化 -> 对象实例化 -> 使用 -> 垃圾回收。这就是为什么 3 最先出现的原因。由于这是在类级别而不是对象级别,因此它将作为类类型只加载一次而出现一次。有关详细信息,请参阅Java2虚拟机内部:类型的生命周期LDC "2"`LDC "1" 在 "init" 中,即构造函数中。
之所以按照这个顺序是因为构造函数会首先执行一些隐式指令,例如超级构造函数和类的 {} 中的代码,然后执行显式的构造函数中的代码。
这就是编译器对Java文件所做的事情。

4

完整解释

执行顺序如下:

  1. 静态块
  2. 实例化块
  3. 构造函数

解释

静态块 总是在类被任何方式访问时 仅调用一次,在您的情况下,即运行程序时。 (这就是静态块的用途)。它不依赖于实例,因此在创建新实例时不会再次调用。

然后,实例初始化块 将为每个创建的实例调用,然后为每个创建的实例调用构造函数。因为两者都可以用来实例化实例。

实例初始化块是否真的在构造函数之前调用?

编译后,代码将变成:

public class Test {

  Test(){
    super();
    System.out.println("2");
    System.out.println("1");
  }


  static {
    System.out.println("3");
  }

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

因此,您可以看到,在实例块中编写的语句成为构造函数的一部分。因此,在构造函数中已经编写的语句之前执行它。

从这份文档中

Java编译器将初始化程序块复制到每个构造函数中。因此,可以使用此方法在多个构造函数之间共享代码块。


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