当我们在父类和子类中创建一个具有相同名称的变量,并尝试使用持有子类对象的父类引用访问它时,我们会得到什么?
为了理解这一点,让我们考虑下面的示例,其中在Parent
和Child
类中声明一个具有相同名称的变量x
。
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";@Overridepublic 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's
对象所持有的东西。 因为在方法上进行相同类型的操作时发生的是同一件事。
但是实际上并非如此, parent.x
将为我们提供在Parent
类中声明的Parent
实例变量的值,但是为什么呢?
因为Java中的变量不遵循多态性,所以重写仅适用于方法,而不适用于变量。 并且,当子类中的实例变量与父类中的实例变量具有相同的名称时,则从引用类型中选择该实例变量。
在Java中,当我们在Child类中使用已经用于在Parent类中定义变量的名称定义变量时,Child类的变量将隐藏父类的变量,即使它们的类型不同。 这种概念称为可变隐藏。
换句话说,当子类和父类都具有相同名称的变量时,子类的变量将隐藏父类的变量。 您可以在文章什么是Java中的变量阴影和隐藏中阅读有关变量隐藏的更多信息。
变量隐藏与方法覆盖不同
尽管变量隐藏看起来像是覆盖变量,类似于方法覆盖,但事实并非如此,但覆盖仅适用于方法,而隐藏适用于变量。
在方法覆盖的情况下,覆盖方法完全替代了继承的方法,因此当我们尝试通过持有子对象来从父对象的引用访问该方法时,将调用子类中的方法。 您可以在“方法重载与方法重载”一书中了解有关重载以及被重载的方法如何完全替代继承的方法的知识,以及为什么要遵循方法 重载 规则 。
但是在变量隐藏中,子类将隐藏继承的变量而不是替换它们,这基本上意味着子类的对象包含两个变量,而子变量则隐藏了父变量。 因此,当我们尝试从Child类中访问变量时,将从子类中访问该变量。
如果我简化了示例8.3.1.1-3。 隐藏 Java语言规范 的实例变量 :
当我们在Child
类中声明一个具有相同名称(例如x
作为Parent
类中的实例变量的变量时,
- 子类的对象包含两个变量(一个是从
Parent
类继承的,另一个是在Child
本身中声明的),但是子类变量隐藏了父类的变量。 - 由于声明
x
类Child
皮的定义x
类Parent
,类的声明中Child
,简单名称x
总是指到外地类中声明的Child
。 而且,如果Child
类方法中的代码想要引用Parent
类的变量x
,则可以将其作为super.x
来完成。 - 如果我们尝试访问
Parent
和Child
类之外的变量,则从引用类型中选择实例变量。 因此,以下代码中的表达式parent2.x
给出了属于父类的变量值,即使它持有Child
的对象,但((Child) parent2).x
可以从Child
类访问该值,因为我们进行了相同的转换参考Child
。
为什么以这种方式设计可变隐藏
因此,我们知道实例变量是从引用类型而不是实例类型中选择的,并且多态性不适用于变量,但是真正的问题是为什么? 为什么变量被设计为跟随隐藏而不是覆盖。
因为如果我们在子类中更改其类型,则变量覆盖可能会破坏从父级继承的方法。
我们知道每个子类都从其父类继承变量和方法(状态和行为)。 想象一下,如果Java允许变量覆盖,并且我们在子类中将变量的类型从int
更改为Object
。 它将破坏使用该变量的任何方法,并且由于子级已从父级继承了这些方法,因此编译器将在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,以便在该类之外看不到变量,并且子类无法访问它们。
您可以在此Github存储库中找到完整的代码,请随时提供宝贵的反馈。
翻译自: https://www.javacodegeeks.com/2018/11/instance-variable-class-overridden-class.html