为什么我的程序在最终类变量未初始化时不显示编译时错误?



对于以下代码:

public class StaticFinal
{
    private final static int i ;
    public StaticFinal()
    {}
}

我得到编译时错误:

StaticFinal.java:7: variable i might not have been initialized
        {}
         ^
1 error

符合JLS8.3.1.2,其中规定:

如果一个空的final(§4.12.4)类变量没有被声明它的类的静态初始值设定项(§8.7)明确地赋值(§16.8),这是一个编译时错误。

因此,我们完全理解了上述错误
但现在考虑以下内容:

public class StaticFinal
{
    private final static int i ;
    public StaticFinal()throws InstantiationException
    {
        throw new InstantiationException("Can't instantiate"); // Don't let the constructor to complete.
    }
}

这里,构造函数永远不会完成,因为InstantiationException是在构造函数的中间抛出的这个代码编译得很好
为什么?为什么此代码没有显示关于final变量i未初始化的编译时错误?


编辑
我在命令提示符下使用javac 1.6.0_25编译它(不使用任何IDE

有趣的是,无论字段是否标记为static-,代码都会编译,在IntelliJ中,它会用静态字段抱怨(但编译),而不用非静态字段说一句话。

你说得对,JLS§8.1.3.2有一些关于[静态]最终字段的规则。然而,关于最终字段,还有一些其他规则在这里发挥了重要作用,这些规则来自Java语言规范§4.12.4,它们指定了final字段的编译语义。

但在我们进入蜡球之前,我们需要确定当我们看到throws时会发生什么——这是§14.18给我们的,重点是我的:

throw语句会引发异常(§11)。结果是控制权的立即转移(§11.3),它可能退出多个语句和多个构造函数、实例初始化器、静态初始化器和字段初始化器评估以及方法调用,直到找到捕获抛出值的try语句(§14.20)。如果没有找到这样的try语句,那么在调用该线程所属线程组的uncaughtException方法后,执行抛出的线程(§17)的执行将终止(§11.3)。

用外行的话来说,在运行时,如果我们遇到throws语句,它可能会中断构造函数的执行(形式上是"突然完成"),导致对象无法构造,或构造为不完整状态。这个可能是一个安全漏洞,这取决于平台和构造函数的部分完整性。

JVM所期望的,如§4.5所示,是ACC_FINAL集的字段在构造对象:后,其值永远不会设置为

宣布为最终;从未直接分配给后对象构造(JLS§17.5)。

因此,我们有点棘手——我们预计在运行时期间会出现这种行为,但在编译时。为什么IntelliJ在我有static的时候会引起轻微的骚动,而在我没有的时候却不会?

首先,回到throws——如果这三个部分中的一个不满足,那么该语句只会出现编译时错误:

  • 正在抛出的表达式未选中或为null
  • trycatch是异常,而catch使用正确的类型,或者
  • 根据§8.4.6和§8.8.5,被抛出的表达式实际上是可以抛出的

因此,使用throws编译构造函数是合法的。碰巧的是,在运行时,它总是会突然完成。

如果一个throw语句包含在构造函数声明中,但它的值没有被包含它的某个try语句捕获,那么调用构造函数的类实例创建表达式将因为throw而突然完成(§15.9.4)

现在,进入空白的final字段。他们有一个奇怪的地方——他们的赋值只在构造函数结束后的才重要,重点是他们的。

一个空白的最终实例变量必须在声明它的类的每个构造函数(§8.8)的末尾明确赋值(§16.9);否则将发生编译时错误

如果我们从未到达构造函数的末尾,该怎么办?


第一个程序:static final字段的正常实例化,反编译:

// class version 51.0 (51)
// access flags 0x21
public class com/stackoverflow/sandbox/DecompileThis {
    // compiled from: DecompileThis.java
    // access flags 0x1A
    private final static I i = 10
    // access flags 0x1
    public <init>()V
            L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
            L1
    LINENUMBER 9 L1
            RETURN // <- Pay close attention here.
    L2
    LOCALVARIABLE this Lcom/stackoverflow/sandbox/DecompileThis; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

注意,在成功调用<init>之后,我们实际上调用了RETURN指令。有道理,而且完全合法。

第二个程序:抛出构造函数和空白static final字段,反编译:

// class version 51.0 (51)
// access flags 0x21
public class com/stackoverflow/sandbox/DecompileThis {
  // compiled from: DecompileThis.java
  // access flags 0x1A
  private final static I i
  // access flags 0x1
  public <init>()V throws java/lang/InstantiationException 
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 8 L1
    NEW java/lang/InstantiationException
    DUP
    LDC "Nothin' doin'."
    INVOKESPECIAL java/lang/InstantiationException.<init> (Ljava/lang/String;)V
    ATHROW // <-- Eeek, where'd my RETURN instruction go?!
   L2
    LOCALVARIABLE this Lcom/stackoverflow/sandbox/DecompileThis; L0 L2 0
    MAXSTACK = 3
    MAXLOCALS = 1
}

ATHROW的规则表示引用被弹出,如果有异常处理程序,则将包含处理异常的指令的地址。否则,它将从堆栈中删除。

我们从未显式地返回,因此意味着我们从未完成对象的构造。因此,对象可以被视为处于不稳定的半初始化状态,同时始终遵守编译时规则——也就是说,所有语句都是可访问的

在静态字段的情况下,由于它不被视为实例变量,而是类变量,因此允许这种调用似乎是错误的。这可能值得提出一个错误。


回想起来,它确实在上下文中有一定的意义,因为Java中的以下声明是合法的,并且方法体与构造函数体一致:

public boolean trueOrDie(int val) {
    if(val > 0) {
        return true;
    } else {
        throw new IllegalStateException("Non-natural number!?");
    }
}

据我所知,我们都是开发人员,所以我相信我们之间不会找到真正的反应……这件事与编译器内部有关。。。我认为这是一个错误,或者至少是一种不想要的行为。

除了Eclipse,它有某种增量编译器(因此能够立即检测到问题),命令行javac执行一次性编译。现在,第一个片段

public class StaticFinal {
    private final static int i ;
}

这基本上与有一个空构造函数(如第一个示例)相同,它抛出编译时错误,这很好,因为它尊重规范。

在第二个片段中,我认为编译器中有一个错误;编译器似乎会根据构造函数所做的事情做出一些决定。如果你试图编译这个,这一点会更明显

public class StaticFinal
{
    private final static int i ;
    public StaticFinal() 
    {
        throw new RuntimeException("Can't instantiate"); 
    }
}

这比您的示例更奇怪,因为未检查的异常没有在方法签名中声明,并且只有在运行时才会被发现(至少这是我在阅读本文之前的想法)。

观察我的行为,我可以说(但根据规格是错误的)。

对于静态最终变量,编译器试图查看它们是否被显式初始化,或者是否在静态初始化器块中初始化,但是,出于某种奇怪的原因,它也在构造函数中寻找一些东西:

  • 如果它们在构造函数中初始化,编译器将产生错误(不能为最终静态变量赋值)
  • 如果构造函数为空,编译器将产生一个错误(如果编译第一个示例,即带有显式零参数构造函数的示例,编译器会中断,将构造函数的右括号指示为错误行)
  • 如果由于构造函数未完成而导致类无法实例化,则是因为抛出了异常(这不是真的,例如,如果您编写System.exit(1)而不是抛出异常。。。它不会编译!),则默认值将分配给静态变量(!)

添加主方法后,使代码打印i。代码打印值0。这意味着java编译器会自动初始化值为0的i。我在IntelliJ中编写了它,并且必须禁用代码检查才能构建代码。否则,它不会让我在抛出异常之前给我同样的错误。

JAVA代码:未初始化

public class StaticFinal {
    private final static int i;
    public StaticFinal(){
        throw new InstantiationError("Can't instantiate!");
    }
    public static void main(String args[]) {
        System.out.print(i);
    }
}

已分解

相同的

JAVA代码:已初始化

public class StaticFinal {
    private final static int i = 0;
    public StaticFinal(){
        throw new InstantiationError("Can't instantiate!");
    }
    public static void main(String args[]) {
        System.out.print(StaticFinal.i);
    }
}

已分解

public class StaticFinal
{
    public StaticFinal()
    {
        throw new InstantiationError("Can't instantiate!");
    }
    public static void main(String args[])
    {
        System.out.print(0);
    }
    private static final int i = 0;
}

在对代码进行反编译后,事实证明并非如此。因为反编译的代码和原始代码是相同的。唯一的其他可能性是初始化是通过Java虚拟机完成的。我所做的最后一次修改充分证明了这一点。

看到这个不得不说再见。

相关问题:这里

我想说这只是因为当你添加Throws时,你基本上是在处理错误,所以编译器会说"哦,好吧,他可能知道他当时在做什么"。毕竟,它仍然会产生运行时错误。

最新更新