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

对于以下代码:

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 ,它说:

如果空白的最终类变量 (§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.
    }
}

在这里,构造函数永远不会完成,因为被扔在构造函数的中间。而且这段代码编译得很好!!
这是为什么呢?为什么此代码未显示有关变量未初始化的编译时错误?InstantiationExceptionfinali


编辑
我正在使用命令提示符编译它 (不使用任何 IDEjavac 1.6.0_25 )


答案 1

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

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

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

throw 语句会导致引发异常 (§11)。结果是控制权的立即转移 (§11.3),该转移可能会退出多个语句和多个构造函数、实例初始值设定项、静态初始值设定项和字段初始值设定项计算以及方法调用,直到找到捕获引发值的 try 语句 (§14.20)。如果未找到此类 try 语句,则在调用线程所属线程组的 uncaughtException 方法后,将终止执行 throw 的线程 (§17) 的执行 (§11.3)。

通俗地说 - 在运行时,如果我们遇到一个语句,它可能会中断构造函数的执行(正式地说,“突然完成”),导致对象无法构造,或者在不完整状态下构造。这可能是一个安全漏洞,具体取决于平台和构造函数的部分完整性。throws

JVM 期望的(由 §4.5 给出)是,构造对象后具有ACC_FINAL集的字段永远不会设置其值

宣布最终确定;从不直接分配给对象构造之后(JLS §17.5)。

所以,我们有点腌制 - 我们期望在运行时这样做,但在编译时不行。为什么IntelliJ在我在那个领域时会引起轻微的大惊小怪,而当我不这样做时却没有?static

首先,回到 - 如果这三个部分之一不满足,则该语句只有编译时错误:throws

  • 被抛出的表达式未选中或为 null,
  • 你例外,你用正确的类型来对待它,或者trycatchcatch
  • 根据 §8.4.6 和 §8.8.5,抛出的表达式实际上是可以抛出的。

因此,使用 a 编译构造函数是合法的。碰巧的是,在运行时,它总是会突然完成。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
}

观察到,在成功调用我们的 .这是有道理的,而且是完全合法的。RETURN<init>

第二个程序:抛出构造函数和空白字段,反编译: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!?");
    }
}

答案 2

正如我所理解的,我们都是开发人员,所以我相信我们不会在我们中间找到真正的回应......这件事与编译器内部有关...我认为这是一个错误,或者至少是一个不需要的行为。

排除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) 而不是引发异常,则情况并非如此...它不会编译!),然后默认值将被分配给静态变量(!)

推荐