首先,一个谜题:下面的代码打印了什么?
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以 scale(long) 打印并重新定义,则X = scale(10) + 3打印件将是。这意味着暂时设置为并稍后设置为。这是违反!X = 0``X = 3``X``0``3``final
X
X = scale(10) + 3
X = 0``X = 3``X``0``3``final
static 修饰符与 final 修饰符一起用于定义常量。final修饰符表示 该字段的值不能改变 。
来源:https ://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [强调添加]
我的问题:这是一个错误吗?final定义不明确吗?
final
这是我感兴趣的代码。 X分配了两个不同的值:0和3. 我认为这是违反final.
3
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; } }
通过查看 ernesto 获得的输出,这一点尤其明显:当a被标记时final,他获得以下输出:
a
a=5 a=5
这不涉及我问题的主要部分:final变量如何改变其变量?
一个非常有趣的发现。要理解它,我们需要深入研究 Java 语言规范 ( JLS )。
原因是final只允许一个 赋值 。但是,默认值是 no assignment 。事实上,每个 这样的变量 (类变量、实例变量、数组组件)从一开始就指向它的 默认值,在 assignments 之前。然后第一个分配会更改引用。
看看下面的例子:
private static Object x; public static void main(String[] args) { System.out.println(x); // Prints 'null' }
我们没有明确地为 赋值x,尽管它指向null,但它是默认值。比较一下搂4.12.5:
x
null
变量的初始值 每个 类变量* 、实例变量或数组组件在 创建 时都会使用 默认值 进行初始化(拥抱15.9,拥抱15.10.2) *
变量的初始值
每个 类变量* 、实例变量或数组组件在 创建 时都会使用 默认值 进行初始化(拥抱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)在使用前必须通过初始化(搂14.4)或赋值(搂15.26)以一种可以使用明确赋值规则进行验证的方式 显式赋予一个值 (搂14.14) 16(明确的任务))。
现在我们来看看final,从搂4.12.4开始:
最终 变量 变量可以声明为 final 。 最终 变量只能 分配给 一次。 如果最终 变量被分配给它是编译时错误,除非它在 分配之前立即被明确地取消分配 (拥抱16(定义分配))。
最终 变量
变量可以声明为 final 。 最终 变量只能 分配给 一次。 如果最终 变量被分配给它是编译时错误,除非它在 分配之前立即被明确地取消分配 (拥抱16(定义分配))。
现在回到您的示例,稍作修改:
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; }
它输出
Before: 0 After: 1
回想一下我们学到的东西。在方法内部assign,变量X还 没有被赋值 。因此,它指向它的默认值,因为它是一个 类变量 ,并且根据 JLS,这些变量总是立即指向它们的默认值(与局部变量相反)。在assign方法之后,变量X被赋值1,因为final我们不能再改变它了。因此,由于以下原因,以下内容将不起作用final:
assign
1
private static long assign() { // Assign X X = 1; // Second assign after method will crash return X + 1; }
感谢@Andrew,我找到了一个完全涵盖这种情况的 JLS 段落,它也演示了它。
但首先让我们看一下
private static final long X = X + 1; // Compile-time error: // self-reference in initializer
为什么不允许这样做,而从方法访问是?看看拥抱8.3.3,它讨论了如果字段尚未初始化,则何时限制对字段的访问。
它列出了一些与类变量相关的规则:
f对于在 class 或 interface 中声明的类变量的简单名称引用,如果出现以下情况C,则为 编译时错误 : * 引用出现在(拥抱8.7C )的类变量初始化器或静态初始化器中;和C f引用要么出现在’ 自己的声明符的初始化程序中,要么出现在’ 声明符的左侧f;和 * 引用不在赋值表达式的左侧(搂15.26);和 包含引用的最里面的类或接口是C.
f对于在 class 或 interface 中声明的类变量的简单名称引用,如果出现以下情况C,则为 编译时错误 :
f
C
* 引用出现在(拥抱8.7C )的类变量初始化器或静态初始化器中;和C
* 引用不在赋值表达式的左侧(搂15.26);和
很简单,X = X + 1被那些规则捕获,方法访问没有。他们甚至列出了这种情况并举了一个例子:
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 因为变量初始化器fori使用类方法peek来访问变量的值j之前j已经被它的变量初始化器初始化了,此时它 仍然有它的默认值 (搂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); } }
产生输出:
因为变量初始化器fori使用类方法peek来访问变量的值j之前j已经被它的变量初始化器初始化了,此时它 仍然有它的默认值 (搂4.12.5)。
i
j