为什么超类的实例变量没有被子类覆盖?

时间:2012-08-23 06:57:07

标签: java

请参阅下面的代码,其中方法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
    }
}

7 个答案:

答案 0 :(得分:38)

  

为什么超类的实例变量没有在子类方法中重写,请参阅下面的代码...

因为实例变量不能在Java中重写。在Java中,只能覆盖方法。

当您声明一个与超类中现有字段同名的字段时,新字段会隐藏现有字段。超类中的现有字段仍然存在于子类中,甚至可以使用...遵循普通的Java访问规则。


  

因为实例变量不能在Java中重写,但为什么呢?为什么在Java中以这种方式完成?是什么原因?

他们为什么这样设计?

  1. 因为重写变量会从根本上破坏超类中的代码。例如,如果覆盖更改了变量的类型,则可能会更改使用原始变量的父类中声明的方法的行为。最糟糕的是,它使它们无法编译。

    例如:

       public class Sup {
           private int foo;
           public int getFoo() {
               return foo;
           }
       }
    
       public class Sub extends Sup {
           private int[] foo;
           ...
       }
    

    如果Sub.foo覆盖(即替换)Sup.foogetFoo()怎么办?在子类上下文中,它会尝试返回错误类型字段的值!

  2. 如果被覆盖的字段不是私有的,那就更糟了。这将以一种非常基本的方式打破Liskov Substitutability Principle(LSP)。这消除了多态性的基础。

  3. 另一方面,覆盖字段无法实现任何无法以其他方式完成的更好。例如,一个好的设计将所有实例变量声明为私有,并根据需要为它们提供getter / setter。可以重写getter / setter ,并且父类可以通过直接使用私有字段或声明getter / setter final来“保护”自己免受不良覆盖。


  4. 参考文献:

答案 1 :(得分:4)

您可以参考Java language specification中有关该主题的以下部分/示例。

  1. 例8.3.1.1-3。 Hiding of Instance Variables
  2. 第8.4.8节。 Inheritance, Overriding, and Hiding及相关示例
  3. 我的帖子的其余部分是那些有兴趣在这个主题上抓住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 JavaWhy 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'
    }
}