为什么Java在编译时绑定变量?

65

请考虑以下示例代码

class MyClass {
    public String var = "base";

    public void printVar() {
        System.out.println(var);
    }
}

class MyDerivedClass extends MyClass {
    public String var = "derived";

    public void printVar() {
        System.out.println(var);
    }
}

public class Binding {
    public static void main(String[] args) {
        MyClass base = new MyClass();
        MyClass derived = new MyDerivedClass();

        System.out.println(base.var);
        System.out.println(derived.var);
        base.printVar();
        derived.printVar();
    }
}

它会产生以下输出

base
base
base
derived

在运行时解析方法调用并调用正确的重写方法,这是预期的。
然而,我后来学到变量访问是在编译时解析的。
我原本期望的输出为

base
derived
base
derived

因为在派生类中重新定义的 var 遮蔽了基类中的变量。


为什么变量的绑定发生在编译时而不是运行时?这只是出于性能原因吗?


3
针对类似的语言,需要注意的是,在 C# 中,除非你明确使用 virtualoverride,否则所有情况下都将具有编译时绑定。而且你无法用它们来操作变量。 - edc65
相关链接:https://dev59.com/42vXa4cB1Zd3GeqPMcOB - user11153
4个回答

46
在Java语言规范中,有一个例子在第15.11节中解释了原因,如下所引用:
“...最后一行表明,实际上,访问的字段不依赖于所引用对象的运行时类;即使s持有对T类对象的引用,表达式s.x也是指向S类的x字段,因为表达式s的类型是S。T类的对象包含两个名为x的字段,一个是T类的,另一个是它的超类S的。”
“这种对字段访问的动态查找缺失允许使用简单的实现高效地运行程序。晚期绑定和覆盖的能力是可用的,但仅当使用实例方法时...”
因此,性能是一个原因。字段访问表达式的求值规范如下所述:
如果字段不是 static

...

  • 如果字段是非空的 final,则结果是在对象中找到类型为 T 的命名成员字段的值,该对象由 Primary 的值引用。

这里的 Primary 指的是您的情况下变量 derived 的类型为 MyClass

另一个原因,如 @Clashsoft 建议的那样,在子类中,字段不会被覆盖,它们是 hidden。因此,根据声明的类型或使用转换允许访问哪些字段是有意义的。静态方法也是如此。这就是为什么基于声明的类型确定字段。与实例方法的重写取决于实际类型不同。JLS 上述引语确实隐含地提到了这个原因:

晚期绑定和重写的功能可用,但仅在使用实例方法时可用。


25

虽然你关于性能的看法可能是正确的,但有另一个原因导致字段不会被动态分派:如果你有一个MyDerivedClass实例,你将无法访问 MyClass.var 字段。

一般来说,我不知道有任何静态类型语言实际上具有动态变量解析。但是,如果你真的需要它,你可以创建getter或访问器方法(在大多数情况下应该这样做,以避免使用 public 字段):

class MyClass
{
    private String var = "base";

    public String getVar() // or simply 'var()'
    {
        return this.var;
    }
}

class MyDerivedClass extends MyClass {
    private String var = "derived";

    @Override
    public String getVar() {
        return this.var;
    }
}

4
比我的回答更好的是:因为语言是这样设计的。 - Andreas
2
如果您有一个MyDerivedClass实例,您将无法访问MyClass.var字段。对于方法也是如此,因此这根本不是一个原因。 - Jens Schauder
1
@JensSchauder 是的,但对于方法,您总是知道您重写了另一个方法,这是面向对象编程中最著名的关键特性。 - Clashsoft
除非我误解了你对“动态变量解析”的定义,否则我可以想到许多具有该特性的编程语言。像Python或JavaScript这样的动态语言通常在运行时解析变量,即使Scala(静态语言)也会多态地查找变量。 - James_pic
我已编辑以仅包括静态类型语言。Scala依赖于JVM,并通过为字段生成访问器方法来“作弊”。我不知道这种覆盖行为是否是编译器开发人员的意图。 - Clashsoft
MyDerivedClass.var 也可能有不同的类型。为了使动态访问成为可能,语言设计者需要确保类型安全,因此应该有一种“变量覆盖”或类似于虚方法的东西。 - Vlad

5
Java语言的多态行为是通过方法而不是成员变量实现的:设计该语言是为了在编译时绑定成员变量。

2
在Java中,这是故意设计的。因为动态解析字段的设置会使事情运行得更慢。实际上,没有任何理由这样做。因此,您可以将类中的字段设为私有,并使用动态解析的方法访问它们。因此,字段在编译时更好地解析 :)

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