最终定义不明确吗?

时间:2018-03-19 14:56:50

标签: java final class-variables static-initialization

首先,一个谜题: 以下代码打印了什么?

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10);

    private static long scale(long value) {
        return X * value;
    }
}

答案:

  

0

下面的剧透。

如果您按比例(长)打印X并重新定义X = scale(10) + 3, 打印件为X = 0,然后为X = 3。 这意味着X暂时设置为0,之后设置为3。 这违反了final

  

static修饰符与final修饰符结合使用,也用于定义常量。   最终修饰符表示此字段的值不能更改

来源:https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [强调补充]

我的问题: 这是一个错误吗? final定义不明确吗?

这是我感兴趣的代码。 X分配了两个不同的值:03。 我认为这违反了final

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10) + 3;

    private static long scale(long value) {
        System.out.println("X = " + X);
        return X * value;
    }
}

此问题已被标记为Java static final field initialization order的可能副本。 我认为这个问题是重复,因为 另一个问题解决了初始化的顺序 我的问题涉及与final标签结合的循环初始化。 仅从另一个问题来看,我就无法理解为什么我的问题中的代码不会出错。

通过查看ernesto获得的输出,这一点尤为明显: 当afinal标记时,他会得到以下输出:

a=5
a=5

这不涉及我的问题的主要部分:final变量如何改变其变量?

6 个答案:

答案 0 :(得分:213)

一个非常有趣的发现。为了理解它,我们需要深入研究Java语言规范(JLS)。

原因是final只允许一个分配。但是,默认值为赋值。实际上,每个这样的变量(类变量,实例变量,数组组件)从赋值开始之前指向默认值。然后第一个分配更改引用。

类变量和默认值

看看下面的例子:

private static Object x;

public static void main(String[] args) {
    System.out.println(x); // Prints 'null'
}

我们没有明确地为x分配值,但它指向null,它是默认值。将其与§4.12.5进行比较:

  

变量的初始值

     

每个类变量,实例变量或数组组件在创建时初始化默认值§15.9§15.10.2

请注意,这仅适用于那些变量,例如我们的示例。它不适用于局部变量,请参见以下示例:

public static void main(String[] args) {
    Object x;
    System.out.println(x);
    // Compile-time error:
    // variable x might not have been initialized
}

来自同一个JLS段落:

  

本地变量§14.4§14.14)在使用之前必须显式赋予值,通过初始化({{ 3}})或赋值(§14.4),可以使用明确赋值规则(§15.26)进行验证。

最终变量

现在我们来看看final中的public static void main(String[] args) { System.out.println("After: " + X); } private static final long X = assign(); private static long assign() { // Access the value before first assignment System.out.println("Before: " + X); return X + 1; }

  

最终变量

     

变量可以声明为 final final 变量只能分配一次。如果 final 变量被分配给它是一个编译时错误,除非在分配之前明确地未分配§16 (Definite Assignment))。

说明

现在回到你的例子,稍加修改:

Before: 0
After: 1

输出

assign

回想一下我们学到的东西。在方法X内,变量assign 尚未分配值。因此,它指向其默认值,因为它是类变量,并且根据JLS,这些变量总是立即指向其默认值(与局部变量相反)。在X方法之后,变量1被赋值为final,由于final,我们无法再对其进行更改。因此,由于private static long assign() { // Assign X X = 1; // Second assign after method will crash return X + 1; }

,以下内容无效
private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer

JLS中的示例

感谢@Andrew我找到了一个JLS段落,它完全涵盖了这个场景,它也展示了它。

但首先让我们来看看

f

为什么不允许这样做,而方法的访问权限是?看看§4.12.4,其中讨论了如果字段尚未初始化,则何时限制访问字段。

它列出了一些与类变量相关的规则:

  

对于通过简单名称引用类或接口C中声明的类变量C,如果,则编译时错误:

     
      
  • 引用显示在C的类变量初始值设定项中或f§16 (Definite Assignment))的静态初始值设定项中;以及

  •   
  • 引用出现在f自己的声明者的初始值设定项中,或者出现在C的声明者左侧的点;以及

  •   
  • 引用不在赋值表达式的左侧(§8.3.3);以及

  •   
  • 封闭引用的最里面的类或接口是X = X + 1

  •   

很简单,class Z { static int peek() { return j; } static int i = peek(); static int j = 1; } class Test { public static void main(String[] args) { System.out.println(Z.i); } } 被这些规则捕获,方法不能访问。他们甚至列出了这个场景并举了一个例子:

  

不会以这种方式检查方法的访问,所以:

0
     

产生输出:

i
     

因为j的变量初始值设定项使用类方法peek在j由其变量初始化程序初始化之前访问变量p的值,此时它仍然有默认值§8.7)。

答案 1 :(得分:22)

与最终决赛没什么关系。

由于它是实例级或类级别,如果尚未分配任何内容,它将保留默认值。这就是你在没有分配时访问它时看到0的原因。

如果您在未完全分配的情况下访问X,则会保留默认值long 0,从而得到结果。

答案 2 :(得分:20)

不是错误。

调用对scale的第一次调用时
private static final long X = scale(10);

它会尝试评估return X * value。尚未为X分配值,因此使用long的默认值(0)。

因此该代码行的评估结果为X * 10,即0 * 10 0

答案 3 :(得分:14)

它根本不是一个错误,只是说它不是前向引用的非法形式,仅此而已。

String x = y;
String y = "a"; // this will not compile 


String x = getIt(); // this will compile, but will be null
String y = "a";

public String getIt(){
    return y;
}

规范允许它。

举个例子,这正是匹配的地方:

private static final long X = scale(10) + 3;

您正在对scale执行转发参考,这不是前面所述的任何非法行为,但允许您获取默认值X。再次,这是规范允许的(更确切地说,它是不被禁止的),所以它可以正常工作

答案 4 :(得分:4)

类级别成员可以在类定义中的代码中初始化。编译后的字节码无法内联初始化类成员。 (实例成员的处理方式类似,但这与提供的问题无关。)

当人们写下如下内容时:

public class Demo1 {
    private static final long DemoLong1 = 1000;
}

生成的字节码类似于以下内容:

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

初始化代码放在静态初始化程序中,该程序在类加载器首次加载类时运行。有了这些知识,您的原始样本将类似于以下内容:

public class RecursiveStatic {
    private static final long X;

    private static long scale(long value) {
        return X * value;
    }

    static {
        X = scale(10);
    }

    public static void main(String[] args) {
        System.out.println(scale(5));
    }
}
  1. JVM加载RecursiveStatic作为jar的入口点。
  2. 类加载器在加载类定义时运行静态初始化程序。
  3. 初始值设定项会调用函数scale(10)来指定static final字段X
  4. scale(long)函数在类部分初始化时运行,读取未初始化的X值,默认值为long或0。
  5. 0 * 10的值已分配给X,类加载器完成。
  6. JVM运行调用scale(5)的public static void main方法,该方法将5乘以现在初始化的X值0,返回0。
  7. 静态最终字段X仅分配一次,保留final关键字所保留的保证。对于在分配中添加3的后续查询,上面的步骤5将成为0 * 10 + 3的值3的评估,主方法将打印3 * 5的结果,即值15 {{1}}。

答案 5 :(得分:3)

读取对象的未初始化字段应该导致编译错误。不幸的是,它没有。

我认为这种情况的根本原因是“隐藏”在对象实例化和构造的定义中,尽管我不知道标准的细节。

从某种意义上说,最终定义不明确,因为它甚至没有达到其声明的目的是由于这个问题。但是,如果所有类都已正确编写,则不会出现此问题。意味着所有字段始终在所有构造函数中设置,并且不会在不调用其构造函数的情况下创建任何对象。在您必须使用序列化库之前,这似乎很自然。