为什么静态初始化程序中声明的顺序很重要?

6
我有这段代码。
private static Set<String> myField;

static {
    myField = new HashSet<String>();
    myField.add("test");
}

当我按照原来的顺序编写代码时,它可以正常工作。但是当我颠倒顺序后,就会出现非法向前引用错误。

static {
    myField = new HashSet<String>();
    myField.add("test"); // illegal forward reference
}

private static Set<String> myField;

我有些震惊,没想到Java会出现这样的情况。 :)

这里发生了什么?为什么声明的顺序很重要?为什么赋值可以正常工作,但方法调用却不能?

6个回答

10

首先,让我们讨论什么是"forward reference"以及为什么它很糟糕。"Forward reference"是指对尚未初始化的变量的引用,而且不仅限于静态初始化器。如果允许这样的引用,它们会给我们带来意想不到的结果,因此它们是不好的。看一下下面这段代码:

public class ForwardRef {
    int a = b; // <--- Illegal forward reference
    int b = 10;
}

当初始化这个类时,j应该是什么? 当一个类被初始化时,初始化会按照第一次遇到的顺序执行。因此,你应该期望这行代码

a = b; 

在执行之前:

b = 10; 

为了避免这种问题,Java设计者完全禁止了对前向引用的这种使用。 编辑 这种行为在《Java语言规范》的第8.3.2.3节中有明确定义:
声明成员需要出现在使用它之前,只有以下所有条件都满足时,才需要满足这个规定:
- 使用出现在类或接口C的实例变量初始化器或静态变量初始化器中,或出现在类或接口C的实例初始化器或静态初始化器中。 - 使用不在赋值操作的左边。 - C是包含使用的最内部的类或接口。
如果上述任何一个条件未满足,则会产生编译时错误。

好的,我明白了。但是在初始赋值之后,myField已经被初始化了。为什么我还是不能调用add方法呢? - Daniel Rikowski
如果这三个要求不存在,我可以使用初始化程序中的本地变量创建一个隐式前向引用,对吗?这些限制是出于这个原因吗? - Daniel Rikowski
JLS表示:“这些限制旨在在编译时捕获循环或其他格式不正确的初始化。” - user85421
2
@dfa: 你引用的是JLS 2.0版本。在JLS 3.0版本中,这一部分表达得更清晰:http://java.sun.com/docs/books/jls/third_edition/html/j3TOC.html。 - Stephen C

2
尝试这个:

class YourClass {
    static {
        myField = new HashSet<String>();
        YourClass.myField.add("test");
    }

    private static Set<String> myField;
}

根据JLS,应该能够无错误编译...


我还没有测试过这个,但它仍然违反了JLS中的“左手规则”。请查看下面的答案以了解原因。 - rtperson
1
@DR:这是编译器的一个特性。反射可以用来规避许多编译器检查,例如从类外部调用私有方法。 - Robert Munteanu
哇,给Carlos点赞加一,并向他致以敬意,因为他把这个问题解决得很好。 - rtperson
@rtperson:这也违反了第三条规则:“使用是通过简单名称进行的”。在我看来,JLS 很难读:“如果成员的声明需要在使用之前以文本形式出现,只有当...并且满足以下所有条件...”,但 JLS 还给出了这个例子:“int z = UseBeforeDeclaration.x * 2; //ok-不是通过简单名称访问” - user85421
1
@Carlos:JLS很难,但现在我理解了。第6章的介绍区分了简单名称(即变量和方法的名称--myField.add())和限定名称(即YourClass.myField.add())。只有当8.2.3.2中的所有规则都被违反时,才会抛出前向引用错误,因此将名称从简单更改为限定可以使其正常工作。他们在JLS的第三版中添加了简单名称规则,因此这里的机制是通过反射实现的。非常有趣。 - rtperson

1

关于DFA的答案,我想你被JLS 8.2.3.2中第二个小点里“左侧规则”绊住了。在你的初始化中,myField在左侧。在你调用add时,它在右侧。这里的代码是隐含的:

boolean result = myField.add('test')  

你没有评估结果,但编译器仍然会像它存在一样处理。这就是为什么你的初始化通过了,而调用add()失败的原因。

至于为什么会这样,我不知道。可能是出于JVM开发人员的方便考虑,我也不确定。


好的,不,原因完全相同。返回 void 的调用仍然在右侧,因为你没有对它进行赋值。你只是告诉编译器该函数没有输出,但这并不改变它是函数的事实。 - rtperson

1
在Java中,所有的初始化器(无论是静态的还是非静态的)都按照它们在类定义中出现的顺序进行评估。

2
但我认为问题是“为什么”,不是吗? - Brian Agnew
为什么这会禁止调用 add 方法但不禁止赋值? - Daniel Rikowski

1

请参阅JLS中有关前向引用的规则。如果出现以下情况,则无法使用前向引用:

  • 在C的实例(或静态)变量初始化程序中使用,或在C的实例(或静态)初始化程序中使用。
  • 使用不在赋值语句的左侧。
  • 使用通过简单名称进行。
  • C是封闭使用的最内部类或接口。

由于您的示例满足所有这些条件,因此前向引用是非法的。


0

我认为方法调用有问题,因为编译器无法确定在没有myField的引用类型的情况下使用哪个add()方法。

在运行时,使用的方法将由对象类型确定,但编译器只知道引用类型。


我认为那不是问题,因为使用虚方法时,编译器几乎无法在编译时绑定方法。这就是VMT的用途:http://en.wikipedia.org/wiki/Virtual_method_table - Daniel Rikowski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接