为什么我们无法通过未初始化的本地变量访问静态内容?

73

看一下下面的代码:

class Foo{
    public static int x = 1;
}

class Bar{    
    public static void main(String[] args) {
        Foo foo;
        System.out.println(foo.x); // Error: Variable 'foo' might not have been initialized
    }
}

当试图通过一个未初始化的本地变量Foo foo;访问静态字段x时,foo.x会产生编译错误:Variable 'foo' might not have been initialized.

这个错误似乎是有道理的,但只有当我们意识到JVM在访问static成员时实际上并没有使用变量的值,而是只用它的类型时,才能理解。

例如,我可以用null值初始化foo,这将使我们可以无任何问题地访问x

Foo foo = null;
System.out.println(foo.x); //compiles and at runtime prints 1!!! 

这种情况可行是因为编译器认识到 x 是静态的,并将 foo.x 视为像 Foo.x 那样书写(至少在我之前是这么想的)。

那么为什么编译器突然坚持要求 foo 有一个值,而实际上它将完全不会使用呢?


声明:这不是真正应用中会使用的代码,而是我找不到在Stack Overflow上的答案的有趣现象,所以我决定询问。


7
鉴于代码已经引发了警告,我认为编译器中存在的限制并不值得修复。 - M A
1
@manouti 这也是我的猜测,但我仍然对编译器为什么会这样行为感兴趣。规范的哪个部分强制执行它? - Pshemo
9
@portfoliobuilder 这里没有NPE的风险,因为正如问题中提到的,如果访问static成员变量,编译器不使用变量的而是使用其类型。 我们甚至可以编写 ((Foo)null).x,这将编译并工作,因为编译器将识别出x是静态的(除非我误解了你的评论)。 - Pshemo
29
如果在Java刚刚创建的时候,在非静态上下文中访问静态变量(比如 foo.x)应该被编译器报错。但可惜的是,这艘船已经在25年前启航,并且如果现在改变它,将会导致破坏性的变化。 - Powerlord
2
@portfoliobuilder:“..绝对存在风险”,您考虑的是哪些风险?还有一个小问题:两种方式在技术上都是正确的(不幸的是),但Foo.x首选(这就是为什么我们通常在尝试使用变体foo.x时会得到编译警告)。 - Pshemo
显示剩余4条评论
4个回答

73

§15.11. Field Access Expressions:

If the field is static:

The Primary expression is evaluated, and the result is discarded. If evaluation of the Primary expression completes abruptly, the field access expression completes abruptly for the same reason.

此处提到的字段访问是通过 Primary.Identifier 进行标识的。这表明,即使似乎未使用 Primary 表达式,它仍然会被求值并且结果被丢弃,这也就是为什么需要初始化的原因。正如引用中所述,当评估停止访问时,这可能会产生差异。

编辑:

以下是一个简短的示例,以直观地演示即使结果被丢弃,Primary 表达式仍将被求值:

class Foo {
    public static int x = 1;
    
    public static Foo dummyFoo() throws InterruptedException {
        Thread.sleep(5000);
        return null;
    }
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println(dummyFoo().x);
        System.out.println(Foo.x);
    }
}
在这里,您可以看到dummyFoo()仍然会被评估,因为print由5秒的Thread.sleep()延迟,即使它始终返回一个被丢弃的null值。

如果表达式未被评估,则print将立即出现,当直接使用类Foo访问x时,可以看到这一点,即Foo.x

注意:方法调用也被认为是Primary,如§15.8 Primary Expressions中所示。


4
有趣的是,javac 字面上执行此操作,生成一个 load 和 pop 指令,而 ecj 强制执行正式规则,即不允许通过未初始化的变量进行访问,但不会为无副作用的操作生成代码。 - Holger

21

第16章。明确赋值

每个局部变量 (§14.4) 和每个空白的 final 域 (§4.12.4, §8.3.1.2) 在任何访问其值之前必须具有明确定义的值。

无论你试图通过局部变量访问什么,规则是在此之前它必须被明确定义。

为了评估 foo.x字段访问表达式,需要先评估它的primary部分 (foo)。这意味着将访问 foo,会导致编译时错误。

对于每个局部变量或空白的 final 域 x 的访问,在访问之前必须明确定义 x,否则会发生编译时错误。


14

保持规则尽可能简单是有价值的,“不要使用可能未初始化的变量”就是最简单的规则。

更重要的是,存在一种已经建立的调用静态方法的方式 - 总是使用类名,而不是变量。

System.out.println(Foo.x);
变量“foo”是不必要的开销,应该将其移除,并且编译器的错误和警告可以被视为帮助推向这一点。

3
其他答案已经很好地解释了正在发生的事情背后的机制。也许你也想知道Java规范背后的原理。作为一个非Java专家,我无法告诉你最初的原因,但是让我指出一下:
每一段代码都有意义或者会触发编译错误。
对于静态变量来说,因为不需要实例,Foo.x是自然的。
现在,我们该如何处理通过实例变量进行访问的foo.x呢?
它可能会导致编译错误,就像C#一样;
它也有一定含义。因为Foo.x已经表示"简单访问x",所以表达式foo.x具有不同的含义。也就是说,表达式的每个部分都是有效的并且可以访问x
希望有资深的人能告诉真正的原因。 :-)

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