为什么当final类变量未初始化时,我的程序没有显示编译时错误?

30

以下是代码:

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一致,文中表示:

如果未通过类的静态初始化程序(§8.7)定义空白终态(§4.12.4)类变量,则在声明它的类中,它是编译时错误不明确分配(§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未初始化的编译错误?
编辑: 我使用命令提示符(没有使用任何IDE)使用javac 1.6.0_25对其进行编译。

1
我遇到了一个错误。 - Sotirios Delimanolis
1
@SotiriosDelimanolis:我在询问第二段代码...而不是第一段。 - Vishal K
2
@SotiriosDelimanolis 第二段代码可以编译。http://ideone.com/p4RS5e - Matt Ball
@RohitJain:我这边肯定可以编译,而且我肯定使用了类变量..完全相同的代码..尝试使用JDK javac进行编译。 - Vishal K
1
@RohitJain 我已经证明了第二个例子可以编译。http://ideone.com/p4RS5e - Matt Ball
显示剩余22条评论
4个回答

3
有趣的是,无论字段是否标记为static,代码都将编译 - 在IntelliJ中,它会在使用静态字段时抱怨(但编译),而在使用非静态字段时则不会说一句话。
你是正确的,JLS §8.1.3.2 对[静态]最终字段有特定的规定。然而,在这里扮演重要角色的还有其他一些关于最终字段的规则,来自于Java语言规范§4.12.4 - 它们指定了final字段的编译语义。
但在我们深入探讨这个问题之前,我们需要确定当我们看到throws时会发生什么 - 这是由§14.18给出的,重点在我身上:
一个throw语句会导致异常(§11)被抛出。结果是立即转移控制(§11.3),直到找到捕获抛出值的try语句(§14.20),可能退出多个语句和多个构造函数、实例初始化程序、静态初始化程序和字段初始化程序的评估以及方法调用。如果没有找到这样的try语句,则执行执行throw的线程(§17)在属于该线程的线程组的uncaughtException方法调用之后终止(§11.3)。
通俗地说,在运行时,如果我们遇到throws语句,它可以中断构造函数的执行(正式地说,“突然完成”),导致对象未被构造或以不完整的状态构造。这可能是一个安全漏洞,取决于平台和构造函数的部分完整性。
JVM期望的是:根据§4.5,一个被设置了ACC_FINAL的字段在对象构造之后就不再被赋值。因此,我们在运行时期望这种行为,但在编译时并非如此。如果该字段带有static,IntelliJ会引发轻微的麻烦,否则不会。首先,回到throws - 只有当以下三个条件中的一个未满足时,才会出现编译时错误。
  • 被抛出的表达式未经检查或为空,
  • 您尝试使用正确类型捕获异常,或者
  • 被抛出的表达式实际上是可以被抛出的,根据§8.4.6和§8.8.5。

因此,编译带有throws的构造函数是合法的。恰好在运行时,它总是会突然终止。

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

现在,到了那个空白的final字段。它们有一个奇怪的特点 - 它们的赋值只在构造函数结束后才有意义,强调他们。

空白的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的规则表明引用被弹出,如果有异常处理程序存在,那个会包含处理异常指令的地址。否则,它将从堆栈中移除。

我们从未显式地 return,因此暗示我们从未完成对象的构造。因此,可以认为该对象处于一个奇怪的半初始化状态,同时遵守编译时规则——也就是说,所有语句都是可达的

对于静态字段而言,由于它不被视为实例变量,而是类变量,这种调用似乎是错误的。可能值得提交一个错误报告。


回想一下,从上下文来看这是有道理的,因为在Java中以下声明是合法的,而方法体与构造函数体是相似的:
public boolean trueOrDie(int val) {
    if(val > 0) {
        return true;
    } else {
        throw new IllegalStateException("Non-natural number!?");
    }
}

1

我理解在这里我们都是开发人员,因此我相信我们不会在我们当中找到真正的答案...这件事与编译器内部有关...我认为这是一个错误,或者至少是一种不希望的行为。

除了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"); 
    }
}

这比您的示例更奇怪,因为未经检查的异常在方法签名中未声明,并且只有在运行时才会被发现(至少在阅读本帖子之前是这样认为的)。
观察行为,我可以说(但根据规范是错误的),对于静态final变量,编译器尝试查看它们是否明确初始化或在静态初始化块中初始化,但由于某种奇怪的原因,它也在构造函数中寻找一些内容:
- 如果它们在构造函数中初始化,则编译器将生成一个错误(您不能在那里为最终静态变量分配值) - 如果构造函数为空,则编译器将生成一个错误(如果编译第一个示例,即具有显式零参数构造函数的示例,则编译器会中断,指示构造函数的关闭括号为错误行)。 - 如果类无法实例化,因为构造函数不完整并抛出异常(例如,如果您写System.exit(1)而不是抛出异常...它将无法编译!),则默认值将分配给静态变量!

0

在添加了一个main方法以使代码打印i后,代码打印出值0。这意味着Java编译器会自动将i初始化为0。我用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虚拟机完成的。我最近做出的更改足以证明这一点。

必须说你很棒,能够发现这个问题。

相关问题: 在这里


Java编译器为什么允许这个? - Vishal K
在这种特定情况下,发生了以下情况,正如我所说,这是一个错误。当您指定变量为final时,它必须被初始化,如果没有初始化,则会导致编译错误。事实上,这种情况并未发生,这是编译器的错误。为确保这一点,您可以将.class文件反编译为Java代码。 - AmirHd
它证明了Java编译器正在初始化i,而应该让你自己来完成。它还回答了你的问题“为什么Java编译器允许这样做?”:因为它有一个错误。这个错误是它代表你初始化了一个静态final值,而它不应该这样做。如果情况是这样的话,你很棒能够发现这个问题,兄弟。 - AmirHd
我刚刚使用了跨平台的jad,而dj-java则是基于Windows的。但奇怪的是,代码似乎与我创建的初始Java代码完全相同。这可能意味着初始化是由虚拟机完成的。真的很感谢你发现了这个问题。 - AmirHd
让我们在聊天中继续这个讨论。点击此处进入聊天室 - AmirHd
显示剩余6条评论

-2

我认为这是因为当你添加 Throws 关键字时,你实际上在处理错误,所以编译器会说:「哦,嗯,他可能知道自己在做什么」。毕竟,它还是会出现运行时错误。


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