【问题标题】:Why my program doesn't show compile time error when final class variable is not initialized?为什么我的程序在最终类变量未初始化时不显示编译时错误?
【发布时间】:2013-06-26 15:07:42
【问题描述】:

以下代码:

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

【问题讨论】:

  • 我遇到了一个错误。
  • @SotiriosDelimanolis:我问的是第二个代码..不是第一个..
  • @SotiriosDelimanolis 第二段代码编译。 ideone.com/p4RS5e
  • 在我的情况下它没有编译。可能是您在谈论实例最终变量。
  • @RohitJain 我已经证明第二个示例可以编译。 ideone.com/p4RS5e

标签: java static compilation final class-variables


【解决方案1】:

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

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

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

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

通俗的说——在运行时,如果我们遇到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:则该语句只有编译时错误:

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

观察我们在成功调用&lt;init&gt; 之后实际上调用了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
}

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

我们从不明确地return,因此意味着我们永远不会完成对象的构造。因此,可以认为该对象处于不稳定的半初始化状态,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!?");
    }
}

【讨论】:

    【解决方案2】:

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

    不包括具有某种增量编译器的 Eclipse(因此能够立即检测到问题),命令行 javac 执行一次性编译。现在,第一个 sn -p

    public class StaticFinal {
        private final static int i ;
    }
    

    这与使用空构造函数(如第一个示例)基本相同,会引发编译时错误,这很好,因为符合规范。

    在第二个sn-p中,我认为编译器存在错误;似乎编译器会根据构造函数的操作做出一些决定。如果你尝试编译这个,这一点会更明显,

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

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

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

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

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        添加一个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 虚拟机完成的。我所做的最后更改足以证明情况确实如此。

        你发现这个必须说好。

        相关问题: Here

        【讨论】:

        • 如何解释:为什么java编译器允许这样做?
        • @vishal-k 它确实解释了为什么会发生这种情况或为什么 java 编译器允许它。那是因为它在生成字节码时正在初始化静态最终变量。显然,这是一个错误,因为 IntelliJ 不允许这样做。
        • 那是因为它在生成字节码时正在初始化静态最终变量.. 这样你是不是想说最终字段永远不会被初始化?而且由于静态最终字段i 被初始化为0 所以,这表明它是一个错误???
        • 在这种特定情况下,这就是发生的事情,就像我说的那样,这是一个错误。当您将变量指定为 final 时,它必须被初始化,如果没有,则会导致编译错误。这不会发生的事实是编译器错误。为确保是这种情况,您可以将 .class 文件反编译成 java 代码。
        • 你仍然没有回答我在评论中问你的问题的第一部分。以及类文件的反编译版本将如何提供帮助?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-03-30
        • 1970-01-01
        • 2020-12-05
        • 1970-01-01
        相关资源
        最近更新 更多