java final 定义不明确吗?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/49365930/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 10:20:57  来源:igfitidea点击:

Is final ill-defined?

javafinalclass-variablesstatic-initialization

提问by Little Helper

First, a puzzle: What does the following code print?

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

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;
    }
}

Answer:

回答:

0

0

Spoilers below.

以下剧透。



If you print Xin scale(long) and redefine X = scale(10) + 3, the prints will be X = 0then X = 3. This means that Xis temporarily set to 0and later set to 3. This is a violation of final!

如果您打印X的规模(长),并重新定义X = scale(10) + 3,印刷品会X = 0那么X = 3。这意味着X暂时设置为0,稍后设置为3。这是违反了final

The static modifier, in combination with the final modifier, is also used to define constants. The final modifier indicates that the value of this field cannot change.

static 修饰符与 final 修饰符一起用于定义常量。最后一个修饰符表示这个字段的值不能改变

Source: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html[emphasis added]

来源:https: //docs.oracle.com/javase/tutorial/java/javaOO/classvars.html[重点添加]



My question: Is this a bug? Is finalill-defined?

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



Here is the code that I am interested in. Xis assigned two different values: 0and 3. I believe this to be a violation of 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;
    }
}


This question has been flagged as a possible duplicate of Java static final field initialization order. I believe that this question is nota duplicate since the other question addresses the order of initialization while my question addresses a cyclic initialization combined with the finaltag. From the other question alone, I would not be able to understand why the code in my question does not make an error.

此问题已被标记为可能与Java 静态最终字段初始化顺序重复。我相信这个问题不是重复的,因为另一个问题解决了初始化的顺序,而我的问题解决了与final标签相结合的循环初始化。仅从另一个问题,我将无法理解为什么我的问题中的代码不会出错。

This is especially clear by looking at the output that ernesto gets: when ais tagged with final, he gets the following output:

通过查看 ernesto 得到的输出,这一点尤其明显:当a被标记为 时final,他得到以下输出:

a=5
a=5

which does not involve the main part of my question: How does a finalvariable change its variable?

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

回答by Zabuzard

A very interesting find. To understand it we need to dig into the Java Language Specification (JLS).

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

The reason is that finalonly allows one assignment. The default value, however, is no assignment. In fact, every such variable(class variable, instance variable, array component) points to its default valuefrom the beginning, before assignments. The first assignment then changes the reference.

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



Class variables and default value

类变量和默认值

Take a look at the following example:

看看下面的例子:

private static Object x;

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

We did not explicitly assign a value to x, though it points to null, it's default value. Compare that to §4.12.5:

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

Initial Values of Variables

Each class variable, instance variable, or array component is initialized with a default valuewhen it is created(§15.9, §15.10.2)

变量的初始值

每个类变量、实例变量或数组组件在创建时都使用默认值进行初始化(第15.9 节、第15.10.2 节

Note that this only holds for those kind of variables, like in our example. It does not hold for local variables, see the following example:

请注意,这只适用于那些类型的变量,就像我们的例子一样。它不适用于局部变量,请参见以下示例:

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

From the same JLS paragraph:

来自同一个 JLS 段落:

A local variable(§14.4, §14.14) must be explicitly given a valuebefore it is used, by either initialization (§14.4) or assignment (§15.26), in a way that can be verified using the rules for definite assignment (§16 (Definite Assignment)).

一个局部变量§14.4§14.14)必须明确给出的数值在使用之前,由要么初始化(§14.4)或分配(§15.26),在可以使用明确赋值规则(验证的方式§ 16(明确分配))。



Final variables

最终变量

Now we take a look at final, from §4.12.4:

现在我们来看看final,来自§4.12.4

finalVariables

A variable can be declared final. A finalvariable may only be assigned to once. It is a compile-time error if a finalvariable is assigned to unless it is definitely unassigned immediately prior to the assignment(§16 (Definite Assignment)).

最终变量

变量可以声明为final。甲最终变量可以仅分配给一次。如果最终变量被赋值,则是编译时错误,除非它在赋值之前明确未赋值§16 (Definite Assignment))。



Explanation

解释

Now coming back to the your example, slightly modified:

现在回到你的例子,稍微修改一下:

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;
}

It outputs

它输出

Before: 0
After: 1

Recall what we have learned. Inside the method assignthe variable Xwas not assigneda value to yet. Therefore, it points to its default value since it is an class variableand according to the JLS those variables always immediately point to their default values (in contrast to local variables). After the assignmethod the variable Xis assigned the value 1and because of finalwe can't change it anymore. So the following would not work due to final:

回忆一下我们学到了什么。在方法内部,assign变量X没有赋值。因此,它指向它的默认值,因为它是一个类变量,并且根据 JLS,这些变量总是立即指向它们的默认值(与局部变量相反)。在assign方法之后,变量X被赋值1,因为final我们不能再改变它了。因此,以下将不起作用final

private static long assign() {
    // Assign X
    X = 1;

    // Second assign after method will crash
    return X + 1;
}


Example in the JLS

JLS 中的示例

Thanks to @Andrew I found a JLS paragraph that covers exactly this scenario, it also demonstrates it.

感谢@Andrew,我找到了一个完全涵盖这种情况的 JLS 段落,它还演示了它。

But first let's take a look at

但首先让我们来看看

private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer

Why is this not allowed, whereas the access from the method is? Take a look at §8.3.3which talks about when accesses to fields are restricted if the field was not initialized yet.

为什么这是不允许的,而方法的访问是?看看第8.3.3 节,它讨论了如果字段尚未初始化,则何时限制对字段的访问。

It lists some rules relevant for class variables:

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

For a reference by simple name to a class variable fdeclared in class or interface C, it is a compile-time error if:

  • The reference appears either in a class variable initializer of Cor in a static initializer of C(§8.7); and

  • The reference appears either in the initializer of f's own declarator or at a point to the left of f's declarator; and

  • The reference is not on the left hand side of an assignment expression (§15.26); and

  • The innermost class or interface enclosing the reference is C.

对于通过简单名称引用f在 class 或 interface 中声明的类变量C如果

  • 引用出现在( §8.7)的类变量初始值设定项C或静态初始值设定项中;和C

  • 引用出现在f自己的声明符的初始值设定项中,或者出现在 的声明符左边的一点处f;和

  • 引用不在赋值表达式的左侧(第15.26 节);和

  • 包含引用的最里面的类或接口是C.

It's simple, the X = X + 1is caught by those rules, the method access not. They even list this scenario and give an example:

很简单,X = X + 1被那些规则捕获,方法访问没有。他们甚至列出了这个场景并举了一个例子:

Accesses by methods are not checked in this way, so:

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);
    }
}

produces the output:

0

because the variable initializer for iuses the class method peek to access the value of the variable jbefore jhas been initialized by its variable initializer, at which point it still has its default value(§4.12.5).

方法访问不以这种方式检查,因此:

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

因为变量初始值设定项 fori使用类方法 peek 访问变量的值j之前j已经被其变量初始值设定项初始化,此时它仍然具有其默认值(第4.12.5 节)。

回答by Suresh Atta

Nothing to do with final here.

这里与final无关。

Since it is at instance or class level, it holds the default value if nothing gets assigned yet. That is the reason you seeing 0when you accessing it without assigning.

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

If you access Xwithout completely assigning, it holds the default values of long which is 0, hence the results.

如果您在X没有完全分配的情况下访问,它会保存 long 的默认值,即0,因此结果。

回答by OldCurmudgeon

Not a bug.

不是错误。

When the first call to scaleis called from

当第一次呼叫scale

private static final long X = scale(10);

It tries to evaluate return X * value. Xhas not been assigned a value yet and therefore the default value for a longis used (which is 0).

它试图评估return X * value. X尚未分配值,因此long使用a 的默认值(即0)。

So that line of code evaluates to X * 10i.e. 0 * 10which is 0.

所以这行代码的计算结果是X * 10ie 0 * 10which is 0

回答by Eugene

It's not a bug at all, simply put it is not an illegal formof forward references, nothing more.

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

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;
}

It's simply allowed by the Specification.

这是规范所允许的。

To take your example, this is exactly where this matches:

以您的示例为例,这正是匹配的地方:

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

You are doing a forward referenceto scalethat is not illegal in any way as said before, but allows you to get the default value of X. again, this is allowed by the Spec (to be more exact it is not prohibited), so it works just fine

你是做一个向前引用scale像以前一样说,是不以任何方式违法,但可以让你获得的默认值X。再一次,这是规范允许的(更确切地说,它不被禁止),所以它工作得很好

回答by psaxton

Class level members can be initialized in code within the class definition. The compiled bytecode cannot initialize the class members inline. (Instance members are handled similarly, but this is not relevant for the question provided.)

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

When one writes something like the following:

当一个人写如下内容时:

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

The bytecode generated would be similar to the following:

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

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

The initialization code is placed within a static initializer which is run when the class loader first loads the class. With this knowledge, your original sample would be similar to the following:

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

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. The JVM loads the RecursiveStatic as the jar's entry point.
  2. The class loader runs the static initializer when the class definition is loaded.
  3. The initializer calls the function scale(10)to assign the static finalfield X.
  4. The scale(long)function runs while the class is partially initialized reading the uninitialized value of Xwhich is the default of long or 0.
  5. The value of 0 * 10is assigned to Xand the class loader completes.
  6. The JVM runs the public static void main method calling scale(5)which multiplies 5 by the now initialized Xvalue of 0 returning 0.
  1. JVM 加载 RecursiveStatic 作为 jar 的入口点。
  2. 类加载器在加载类定义时运行静态初始值设定项。
  3. 初始化程序调用函数scale(10)来分配static final字段X
  4. scale(long)函数在类部分初始化时运行,读取未初始化的值为Xlong 或 0 的默认值。
  5. 的值0 * 10被赋值给X类加载器完成。
  6. JVM 运行 public static void main 方法调用scale(5),它将 5 乘以现在的初始化X值 0,返回 0。

The static final field Xis only assigned once, preserving the guarantee held by the finalkeyword. For the subsequent query of adding 3 in the assignment, step 5 above becomes the evaluation of 0 * 10 + 3which is the value 3and the main method will print the result of 3 * 5which is the value 15.

静态 final 字段X只分配一次,保留final关键字持有的保证。对于后面的赋值加3的查询,上面的第5步就变成了对0 * 10 + 3哪个值的求值3,main方法会打印出3 * 5哪个是值的结果15

回答by Kafein

Reading an uninitialized field of an object ought to result in a compilation error. Unfortunately for Java, it does not.

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

I think the fundamental reason why this is the case is "hidden" deep within the definition of how objects are instantiated and constructed, though I don't know the details of the standard.

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

In a sense, final is ill-defined because it doesn't even accomplish what its stated purpose is due to this problem. However, if all your classes are properly written, you don't have this problem. Meaning all fields are always set in all constructors and no object is ever created without calling one of its constructors. That seems natural until you have to use a serialization library.

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