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

2022-08-31 15:14:12

请考虑以下示例代码

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


答案 1

原因在 Java 语言规范中第 15.11 节的一个示例中进行了解释,引用如下:

...

最后一行显示,实际上,访问的字段不依赖于被引用对象的运行时类;即使 s 包含对类 T 的对象的引用,表达式 s.x 也引用类 Sx 字段,因为表达式 s 的类型是 S。类 T 的对象包含两个名为 x 的字段,一个用于类 T,另一个用于其超类 S

这种缺乏对现场访问的动态查找使得程序能够通过简单的实现高效运行。后期绑定和重写的强大功能可用,但仅当使用实例方法时...

所以是的,性能是一个原因。如何计算字段访问表达式的规范说明如下:

  • 如果该字段不是 :static

    ...

    • 如果该字段是非空白的,则结果是命名成员字段的值,该字段的类型位于由 Primary 的值所引用的对象中。finalT

其中,在您的案例中,“主要”是指类型为 的变量。derivedMyClass

另一个原因,正如@Clashsoft建议的那样,在子类中,字段没有被覆盖,而是被隐藏。因此,允许根据声明的类型或使用强制转换访问哪些字段是有意义的。对于静态方法也是如此。这就是根据声明的类型确定字段的原因。与实例方法重写不同,实例方法依赖于实际类型。上面的JLS引用确实隐含地提到了这个原因:

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


答案 2

虽然您在性能方面可能是正确的,但字段未动态调度还有另一个原因:如果您有实例,则根本无法访问该字段。MyClass.varMyDerivedClass

通常,我不知道任何静态类型语言实际上具有动态变量分辨率。但是,如果您确实需要它,则可以创建 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;
    }
}

推荐