44

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

7 回答 7

51

为什么在子类方法中不覆盖超类的实例变量,请参阅下面的代码...

因为实例变量不能在 Java 中被覆盖。在 Java 中,只有方法可以被覆盖。

当您在超类中声明与现有字段同名的字段时,新字段会隐藏现有字段。超类中的现有字段仍然存在于子类中,甚至可以使用……符合正常的 Java 访问规则。

(在您的示例中, 的实例C有两个不同的字段,称为a,包含不同的值。)


因为实例变量在 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.foo,如何getFoo()工作?在子类上下文中,它会尝试返回错误类型的字段的值!

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

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


参考:

于 2012-08-23T07:36:48.957 回答
4

您可以参考Java 语言规范中解释该主题的以下部分/示例。

  1. 示例 8.3.1.1-3。隐藏实例变量
  2. 第 8.4.8 节。继承、覆盖和隐藏以及相关示例

我的其余帖子是为那些有兴趣在此主题上了解 jvm 内部结构的人提供的附加信息。我们可以首先检查使用 javap 为类 A 生成的字节码。以下将字节码反汇编成人类可读的基于文本的指令(助记符)。

javap -c A.class 

在不迷失整个反汇编的很多细节的情况下,我们可以专注于 b.print 和 ba 对应的行

9: invokevirtual #4                  // Method B.print:()V
...
...
16: getfield      #6                  // Field B.a:I

我们可以立即推断出用于访问方法和变量的操作码是不同的。如果您来自 C++ 学校,您可能会感觉到所有方法调用在 java 中默认情况下都是虚拟的。

现在让我们编写另一个与 A 相同的类 A1,但只有一个用于访问 C 中的变量“a”的转换。

公共类 A1 {
  public static void main(String[] args) {
    B b=new C();
    b.打印();// 转换在这里是无关紧要的,因为无论如何方法都是在运行时绑定 System.out.println(((C)b).a);// 转换允许我们访问 C 中 a 的值
  }
}

编译文件并反汇编类。

javap -c A1.class

你会注意到反汇编现在指向 Ca 而不是 Ba

19: getfield #6 // 字段 Ca:I

如果您想深入研究,这里有更多信息:
- invokevirtual 对应于操作码 0xb6
- getfield 对应于操作码 0xb4

您可以在http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html找到全面解释这些操作码的 JVM 规范
在 amazon.com 中查看“Java 虚拟机” ” 可以使解码规范的生活变得更加轻松的书籍。

于 2013-08-12T00:44:37.323 回答
3

我已经修改了您的代码以便于解释,而不是变量“a”,可以说 C 类包含变量“c”。这与 C 类在没有类型转换的情况下无法访问 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 中,编译器通过引用而不是实例。为了克服这个编译器使用运行时多态性,但它是用于方法,而不是实例变量。所以变量不能在没有类型转换的情况下被访问,并且方法除非被覆盖(运行时多态性)不能在没有类型转换的情况下被访问。

所以,在我们的例子中,携带子类实例的超类的引用很明显要在超类中查看。

于 2016-09-18T10:52:54.167 回答
2

因为Java中的变量不遵循多态性,覆盖只适用于方法而不适用于变量。

在Java中,当子类和父类都有同名的变量时,子类的变量会隐藏父类的变量,即使它们的类型不同。这个概念被称为变量隐藏

在方法覆盖的情况下,覆盖方法完全替换了继承的方法,但在变量隐藏中,子类隐藏了继承的变量而不是替换,这基本上意味着子类的对象包含两个变量,但子类的变量隐藏了父类的变量。因此,当我们尝试访问 Child 类中的变量时,它将从子类中访问。

如果我们试图访问 Parent 和 Child 类之外的变量,则从引用类型中选择实例变量。

为什么从引用类型而不是实例中选择实例变量

正如JVM 如何在内部处理方法重载和覆盖中所解释的,在编译时,仅从引用类处理覆盖方法调用,但所有被覆盖的方法在运行时使用 vtable 被覆盖方法替换,这种现象称为运行时多态

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

为什么变量不遵循覆盖或为什么它们遵循隐藏

因为如果我们在子类中更改变量的类型,变量覆盖可能会破坏从父类继承的方法。

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

如前所述,如果 Java 允许变量覆盖,则 Child 的变量不能替代 Parent 的变量,这将违反 Liskov 可替代性原则 (LSP)。

你可以阅读我的文章什么是Java中的变量阴影和隐藏为什么超类的实例变量在子类中没有被覆盖

于 2018-11-09T16:13:12.267 回答
0

这是我在设计/概念层面上关于为什么不覆盖实例变量的观点。为简单起见,如果我们考虑抽象类,它们会定义抽象方法并期望它们被覆盖。从来没有像抽象变量这样的东西。如果有,那么我们可以期望语言通过覆盖来支持它。所以,当一个抽象类被设计时,设计者定义了一些常见的具体状态子类型的常见行为(包括抽象方法)。几乎总是如果要继承状态(受保护的访问),那么它将被简单地继承,我相信在极少数情况下可以重新定义其中的一些,但很少重新声明。因此,状态自然被期望被简单地继承,而行为被期望被继承和覆盖。

于 2017-08-04T11:09:39.803 回答
0

就像其他人提到的那样,您不能覆盖超类的实例变量,但您可以使用构造函数为您的对象分配正确的值。例如,您可以使用构造函数使类 C 中的 'a' 的值等于 '20'。

这是您的原始代码,使用构造函数将“a”的值设置为 C 类中的“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'
    }
}
于 2019-05-26T05:10:29.323 回答
-1

由于实例变量不会在 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.
于 2017-02-11T20:27:39.187 回答