为什么在子类中不覆盖Super Class的实例变量


当我们在父类和子类中使用相同的名称创建变量,并尝试使用包含子类对象的父类引用访问它时,我们会得到什么?

为了理解这一点,让我们考虑下面的示例,在该示例中,我们xParentChild 类中都声明了一个具有相同名称 的变量 。

class Parent {
    // Declaring instance variable by name `x`
    String x = "Parent`s Instance Variable";

    public void print() {
        System.out.println(x);
    }
}

class Child extends Parent {

    // Hiding Parent class's variable `x` by defining a variable in child class with same name.
    String x = "Child`s Instance Variable";

​
    @Override
    public void print() {
        System.out.print(x);
​
        // If we still want to access variable from super class, we do that by using `super.x`
        System.out.print(", " + super.x + "\n");
    }
}
​

现在,如果我们尝试x 使用下面的代码进行访问 , System.out.println(parent.x)将输出:

Parent parent = new Child();
System.out.println(parent.x) // Output -- Parent`s Instance Variable

一般而言,我们说 Child 该类将覆盖该类中声明的变量 Parent , parent.x 并将为我们提供Child对象所持有的任何东西 。因为这是同一件事,所以在我们对方法执行相同类型的操作时会发生这种情况。

但是,实际上不是, parent.x 它将为我们提供在Parent 类中声明的Parent的Instance变量的值 -但是为什么呢?

这是因为Java中的变量不遵循多态性,并且重写仅适用于方法,而不适用于变量。并且,当类中的实例变量与child 类中的实例变量 具有相同的名称时, parent 则从引用类型中选择该实例变量。

在Java中,当我们 Child 使用已经在Parent 类中定义变量的名称在类中定义变量时 ,Child类的变量将隐藏父类的变量,即使它们的类型不同。这个概念被称为变量隐藏。

换句话说,当child和parent 类都具有相同名称Child 的变量时,该类的变量将隐藏Parent 该类的变量。

变量隐藏与方法覆盖不同 尽管变量隐藏看起来像是重写,但变量并非都那么相似。覆盖仅适用于方法,而隐藏适用于变量。

在方法覆盖的情况下,覆盖方法完全替换了继承的方法,因此当我们尝试通过持有子对象从父对象的引用访问该方法时,将调用子类中的方法。在方法重载与方法重载以及 为什么要遵循方法重载规则的所有内容中,您可以阅读有关重载以及被重载的方法如何完全替代继承的方法的更多信息。

但是在变量隐藏中,子类将隐藏继承的变量而不是替换它们,这基本上意味着Child 该类的对象包含两个变量,而子代 的变量隐藏了父代的变量。因此,当我们尝试从Child 类内部访问变量时,将从子类中访问该变量 。

如果我简化了示例8.3.1.1-3。实例变量的隐藏的Java语言规范:

当我们在Child类中声明一个具有相同名称x的变量时,例如,作为类中的实例变量Parent,则:

  1. 本 Child 类的对象包含两个变量(从继承的一个 Parent类,另一个宣布Child本身),但在子类变量皮父类的变量。
  2. 因为xin class的声明Child隐藏了in class的定义,所以x在classParent的声明内Child,简单名称x始终引用class内声明的字段Child。而如果在该方法的代码Child类要参考变量x的的 Parent类,那么这是可以做到的super.x。
  3. 如果我们试图访问ParentandChild类之外的 变量,则从引用类型中选择实例变量。因此,parent2.x以下代码中的表达式给出了属于Parent 该类的变量值 ,即使它持有的对象Child。但是,((Child) parent2).x从Child类中访问值,因为我们对进行了相同的引用Child。 为什么子类中的超级影子实例变量不会由于变量阴影而被覆盖 variable-shadowing.jpeg

为什么用这种方式设计可变隐藏 因此,我们知道实例变量是从引用类型而不是实例类型中选择的,并且多态性不适用于变量,但是真正的问题是为什么?为什么将变量设计为隐藏而不是覆盖?

这是因为Parent 如果我们在Child 类中更改其类型,则 变量覆盖可能会破坏从继承的方法 。

我们知道每个 Child 类都从其Parent 类继承变量和方法(状态和行为) 。试想一下,如果Java允许变量覆盖,我们改变一个变量的类型从 int到 Object 的 Child 类。它将破坏使用该变量的任何方法,并且由于子级已从继承了这些方法 Parent,因此编译器将在Child 类中给出错误 。

例如:

class Parent {
    int x;
    public int increment() {
        return ++x;
    }
    public int getX() {
        return x;
    }
}
​
class Child extends Parent {
    Object x;
    // Child is inherting increment(), getX() from Parent and both methods returns an int 
    // But in child class type of x is Object, so increment(), getX() will fail to compile. 
}

如果Child.x覆盖 Parent.x,怎么能 increment() 和 getX() 工作?在子类中,这些方法将尝试返回错误类型的字段的值!

如前所述,如果Java允许变量覆盖,那么 Child'的变量不能替代 Parent'的变量,这将破坏Liskov可替换性原理(LSP)。

为什么从引用类型而不是实例中选择实例变量? 如JVM内部如何处理方法重载和覆盖中所述,在编译时,覆盖方法调用仅从引用类处理,但是所有覆盖的方法在运行时都使用vtable被覆盖的方法代替。这种现象称为运行时多态。

同样,在编译时,也从引用类型处理变量访问,但是正如我们所讨论的,变量不遵循重写或运行时多态性,因此它们在运行时不会被子类变量替代,仍然引用引用类型。

一般来说,没有人会建议隐藏字段,因为这会使代码难以阅读并造成混乱。这种混淆将不会始终遵循一般准则来创建POJO并通过将它们声明为私有并封装我们的字段并按要求提供getter / setter来封装我们的字段,因此变量在该类之外不可见,并且子类无法访问它们。


原文链接:http://codingdict.com