请参阅下面的代码,其中方法print
被覆盖但变量a
未被覆盖。为什么允许在子类中声明重复变量?
class B {
int a = 10;
public void print() {
System.out.println("inside B superclass");
}
}
class C extends B {
int a = 20;
public void print() {
System.out.println("inside C subclass");
}
}
public class A {
public static void main(String[] args) {
B b = new C();
b.print(); // prints: inside C subclass
System.out.println(b.a); // prints superclass variable value 10
}
}
答案 0 :(得分:38)
为什么超类的实例变量没有在子类方法中重写,请参阅下面的代码...
因为实例变量不能在Java中重写。在Java中,只能覆盖方法。
当您声明一个与超类中现有字段同名的字段时,新字段会隐藏现有字段。超类中的现有字段仍然存在于子类中,甚至可以使用...遵循普通的Java访问规则。
因为实例变量不能在Java中重写,但为什么呢?为什么在Java中以这种方式完成?是什么原因?
他们为什么这样设计?
因为重写变量会从根本上破坏超类中的代码。例如,如果覆盖更改了变量的类型,则可能会更改使用原始变量的父类中声明的方法的行为。最糟糕的是,它使它们无法编译。
例如:
public class Sup {
private int foo;
public int getFoo() {
return foo;
}
}
public class Sub extends Sup {
private int[] foo;
...
}
如果Sub.foo
覆盖(即替换)Sup.foo
,getFoo()
怎么办?在子类上下文中,它会尝试返回错误类型字段的值!
如果被覆盖的字段不是私有的,那就更糟了。这将以一种非常基本的方式打破Liskov Substitutability Principle(LSP)。这消除了多态性的基础。
另一方面,覆盖字段无法实现任何无法以其他方式完成的更好。例如,一个好的设计将所有实例变量声明为私有,并根据需要为它们提供getter / setter。可以重写getter / setter ,并且父类可以通过直接使用私有字段或声明getter / setter final
来“保护”自己免受不良覆盖。
参考文献:
答案 1 :(得分:4)
您可以参考Java language specification中有关该主题的以下部分/示例。
我的帖子的其余部分是那些有兴趣在这个主题上抓住jvm内部表面的人的附加信息。我们可以从使用javap检查为A类生成的字节代码开始。下面将字节代码反汇编成基于人类可读文本的指令(助记符)。
javap -c A.class
不要迷失整个拆卸的许多细节,我们可以专注于与b.print和b.a相对应的行
9: invokevirtual #4 // Method B.print:()V
...
...
16: getfield #6 // Field B.a:I
我们可以立即推断用于访问方法的操作码和变量是不同的。如果您来自C ++学校,您可以感觉到默认情况下所有方法调用都是虚拟的。
现在让我们编写另一个与A相同的A1类,但只有一个用于在C中访问变量'a'的转换。
公共班A1 {
public static void main(String [] args){
B b = new C();
b.print(); //这里的转换是无关紧要的,因为方法无论如何都要在运行时绑定 System.out.println(((C)b).a); //铸造允许我们访问C中的值a }
}
编译文件并反汇编类。
javap -c A1.class
您会注意到,拆卸现在指向C.a而不是B.a
19:getfield#6 // Field C.a:我
如果你想深入研究这个,这里有更多的信息:
- invokevirtual对应于操作码0xb6
- getfield对应于操作码0xb4
您可以在 - http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html找到一个全面解释这些操作码的JVM规范 在amazon.com上查看“Java虚拟机”书籍,这些书籍可以让生活更容易解码规范。
答案 2 :(得分:3)
我修改了您的代码以便于解释,而不是变量' a' ,假设C类包含变量' c'。 出于同样的原因,为什么C类在没有Typecasting的情况下无法访问Class c的实例变量。 示例如下所示
class B
{
int a=10;
public void print()
{
System.out.println("inside B super class");
}
}
class C extends B
{
int x=20;
public void print()
{
System.out.println("inside C sub class");
}
}
public class A {
public static void main(String[] args) {
B b=new C();
System.out.println(b.x);//will throw compile error unless b is type casted to Class C
}
}
因此,在java中,编译器通过引用而不是实例。 要克服此编译器使用运行时多态,但它适用于方法,而不是实例变量。 因此,如果没有类型转换,就无法访问变量 和 除非被覆盖(运行时多态),否则无法在没有类型转换的情况下访问这些方法。
所以,在我们的例子中。对于带有子类实例的Superclass的参考,在超类中查看是显而易见的。
答案 3 :(得分:2)
因为Java中的变量不遵循多态性,所以重写仅适用于方法,而不适用于变量。
在Java中,当子类和父类都具有相同名称的变量时,即使子类的类型不同,子类的变量也会隐藏父类的变量。这个概念称为可变隐藏。
在方法覆盖的情况下,覆盖方法完全替换了继承的方法,但是在变量隐藏子类中,它隐藏了继承的变量而不是替换,这基本上意味着Child类的对象包含两个变量,而Child的变量则隐藏了Parent的变量。因此,当我们尝试访问Child类中的变量时,将从子类中访问该变量。
如果我们尝试访问Parent和Child类之外的变量,则从引用类型中选择实例变量。
如How Does JVM Handle Method Overloading and Overriding Internally中所述,在编译时,覆盖方法调用仅从引用类处理,但是所有覆盖的方法在运行时使用vtable替换为覆盖方法,这种现象称为运行时多态性。
类似地,在编译时,变量访问也从引用类型处理,但是正如我们所讨论的,变量不遵循重写或运行时多态性,因此它们在运行时不会被子类变量替代,仍然引用引用类型。
因为如果我们在子类中更改变量的类型,则变量覆盖可能会破坏从父类继承的方法。
我们知道每个子类都从其父类继承变量和方法(状态和行为)。想象一下,如果Java允许变量覆盖,并且我们在子类中将变量的类型从int
更改为Object
。它将破坏使用该变量的任何方法,并且由于子级已从父级继承了这些方法,因此编译器将在子级中给出错误。
并且如上所述,如果Java允许变量覆盖,则Child的变量不能替代Parent的变量,这将破坏Liskov替代性原则(LSP)。
您可以阅读我的文章What is Variable Shadowing and Hiding in Java,Why Instance Variable Of Super Class Is Not Overridden In Sub Class
的更多内容答案 4 :(得分:0)
由于实例变量在java中没有被覆盖,因此没有与它们相关联的运行时多态,因此在编译时它只能通过引用来决定。
在您的代码中
B b = new C();
b.print();
As b is of type Class B which is Parent to C and hence as there is no
run time polymorphism it is decided at compile time to call instance
variable of Class B.
答案 5 :(得分:0)
这是我在设计/概念层面上关于为什么不覆盖实例变量的观点。为了简单起见,如果我们考虑抽象类,它们会定义抽象方法并期望它们被覆盖。 从来没有像抽象变量那样的东西。如果有,那么我们可以期望语言通过覆盖来支持它。因此,当设计抽象类时,设计者为子类型定义一些常见的具体状态和常见行为(包括抽象方法)。几乎总是如果要继承状态(受保护的访问),那么它将被简单地继承,并且我相信在极少数情况下,其中一些可以重新定义,但很少重新声明。因此,状态自然被期望被简单地继承,而行为被期望被继承和覆盖。
答案 6 :(得分:0)
就像其他人提到的那样,您不能覆盖超类的实例变量,但是您可以使用构造函数为对象分配适当的值。例如,您可以使用构造函数为...赋值C类中的“ a”等于“ 20”。
这是您的原始代码,使用构造函数对其进行了扩展,以在C类中将“ a”的值设置为等于“ 20”。
长话短说,我们正在使用对象实例和构造函数的参数将值传递给超类。
public class B {
private int a; //initialize int a
public int getA() { //create a getter for a
return a;
}
public B(int size) { //constructor that takes an int
a = size; //sets a to the value in the parameters
}
public void print() {
System.out.println("inside B superclass");
}
}
public class C extends B{
public C(int a) { //C constructor takes an int
super(a); //it send the name up to its superclass (B)
}
public void print() {
System.out.println("inside C subclass");
}
}
public class A {
public static void main(String[] args) {
B b = new C(20); //Creates a new object 'b' of type C
b.print(); // prints: inside C subclass
System.out.println(b.getA()); // prints the value '20'
}
}