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

时间:2013-06-28 18:43:31

标签: java static compilation final class-variables

以下代码:

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

4 个答案:

答案 0 :(得分:3)

有趣的是,代码将编译该字段是否标记为static - 并且在IntelliJ中,它将使用静态字段抱怨(但编译),而不是用非静态字段说出一个字

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

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

  

throw语句导致抛出异常(§11)。结果是立即转移控制(第11.3节),可以退出多个语句和多个构造函数,实例初始化器,静态初始化器和字段初始化器评估,以及方法调用,直到try语句(§14.20)为止发现抓住了抛出的价值。如果没有找到这样的try语句,则在调用该线程所属的线程组的uncaughtException方法之后,终止执行执行throw的线程(第17节)(第11节)。

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

§4.5给出的JVM期望的是a field with ACC_FINAL set never has its value set after construction of the object

  

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

所以,我们有点蠢 - 我们期待运行时期间的行为,但不是编译时期间的行为。当我在那个领域static时,为什么IntelliJ会引起一阵轻微的烦恼,但是当我没有这个时却不是这样呢?

首先,回到throws - 如果one of these three pieces aren't satisfied,则该语句只有编译时错误:

  • 抛出的表达式未选中或为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
}

观察我们在成功调用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
}

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

我们从未明确返回,因此暗示我们永远不会完成对象的构建。因此,可以认为该对象处于一个不稳定的半初始化状态all the while obeying compile-time rules - 也就是说,所有语句都可到达

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


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

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

答案 1 :(得分:1)

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

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

这比你的例子更奇怪,因为未经检查的异常未在方法签名中声明,并且仅在运行时发现(至少这是我在阅读本文之前所想到的)。

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

对于静态最终变量,编译器会尝试查看它们是在显式初始化,还是在静态的intializer块中初始化,但是,由于某些奇怪的原因,它也会在构造函数中查找某些内容:

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

答案 2 :(得分:0)

添加main方法后,打印代码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虚拟机完成的。我所做的最后一次改变是一个足够好的证据表明情况就是如此。

要发现这一点,必须说得好。

相关问题: Here

答案 3 :(得分:-2)

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