面向对象范例中的方法可以被继承类中具有相同签名的方法覆盖。然而,变量不能。为什么?

时间:2011-12-28 11:07:37

标签: java oop polymorphism

维基百科将virtual methods定义为:

  

在面向对象的编程中,虚函数或虚方法是一种函数或方法,其行为可以通过具有相同签名的函数在继承类中重写[以提供多态行为]。

根据定义,Java中的每个非静态方法默认为虚拟,除了 final和private方法。对于多态行为不能继承的方法是虚拟方法。

Java中的静态方法永远不能被覆盖;因此,在Java中将静态方法声明为final是没有意义的,因为静态方法本身就像最终方法一样。它们可以通过具有相同签名的方法简单地在子类中隐藏。显然是这样,因为静态方法永远不会有多态行为:被覆盖的方法必须实现多态,而静态方法则不然。

从前一段开始,可以推动一个重要的结论。默认情况下,C ++中的所有方法都是静态的,因为除非在超类中显式声明为虚拟,否则C ++中的任何方法都不能以多态方式运行。相比之下,除了final,static和private方法之外,Java中的所有方法都是虚拟的,因为它们默认具有多态行为(不需要在Java中将方法显式声明为虚拟,因此,Java没有像“virtual”这样的关键字)。

现在,让我们演示实例变量(静态)也不能通过Java中的以下简单示例进行多态化。

class Super
{
    public int a=5;
    public int show()
    {
        System.out.print("Super method called a = ");
        return a;
    }
}

final class Child extends Super
{
    public int a=6;

    @Override
    public int show()
    {
        System.out.print("Child method called a = ");
        return a;
    }
}

final public class Main
{
    public static void main(String...args)
    {
        Super s = new Child();
        Child c = new Child();

        System.out.println("s.a = "+s.a);
        System.out.println("c.a = "+c.a);        

        System.out.println(s.show());
        System.out.println(c.show());
    }
}

上述代码段产生的输出如下:

s.a = 5
c.a = 6
Child method called a = 6
Child method called a = 6

在此示例中,调用s.show()c.show()分别通过show()类型和Super类型的变量对Child方法进行调用,调用show()类中的Child方法。这意味着show()类中的Child方法会覆盖show()类中的Super方法,因为它们都具有相同的签名。

但是,这不能应用于两个类中声明的实例变量a。在这种情况下,s.a会引用a类中的Super并显示5c.a会引用a Child 1}} class和display 6表示a类中的Child只隐藏(并且不会覆盖非静态方法)a { {1}}上课。

经过长时间的讨论,只有一个问题。为什么实例变量(以及其余变量)没有被覆盖?实施这种机制的特殊原因是什么?如果它们被覆盖了会有任何优点或缺点吗?

4 个答案:

答案 0 :(得分:3)

我认为覆盖的目的是在不改变签名的情况下改变功能。这仅适用于方法:方法可能具有相同的签名但具有不同的行为。字段只有“签名”,也仅限于类型和名称。他们没有行为,所以没有任何东西可以被覆盖。

无法“覆盖”字段的其他原因是字段通常是私有的(或应该是私有的),因此它们实际上是类实现的血腥细节。然而,方法表示类的外部接口。此外部接口应支持多态,以便更改模块的功能,而无需更改模块之间的关系。

答案 1 :(得分:2)

当引用名称解析时,术语“静态”还有另外一种用法:静态解析是指使用声明的变量类型来解析名称时的动态,当使用变量中存储的数据类型时动态。静态分辨率可以在编译时完成,而动态分辨率必须在运行时完成。解析虚拟成员(这是一种动态解析的形式)需要额外的间接级别,即表查找。允许“虚拟”字段会产生运行时成本。

一旦完成所有成员的静态分辨率,就没有必要使用静态类型变量。此时,您可以使用动态类型语言,例如Python和众多Lisp变体。当您丢弃静态类型时,尝试说出不覆盖(或覆盖)父字段的子字段就没有意义了,因为不再有静态变量类型来暗示其他情况。

class Super(object):
    a=5
    def show(self):
        print("Super method called a = ", end='')
        return self.a

class Child(Super):
    a=6
    def show(self):
        print("Child method called a = ", end='')
        return self.a

# there's no indication that 's' is supposed to be a Super...
s = Child();
c = Child();

# so it's no surprise that s.a is Child.a
print("s.a =", s.a)
# result: "s.a = 6"
print("c.a =", c.a)
# result: "c.a = 6"
print(s.show())
# result: "Child method called a = 6"
print(c.show())
# result: "Child method called a = 6"

应该注意,常见的lisps具有类型声明和动态分辨率。

(defgeneric show (o))

(defclass super () 
  ((a :accessor super-a
     :initform 5
     :initarg :a))
  )

(defmethod show ((o super))
  (list "Super method called a = " 
        (slot-value o 'a))
  )

(defclass child (super)
  ((a :accessor child-a
     :initform 6
     :initarg :a))
  )

(defmethod show ((o child))
  (list "Child method called a = "
        (slot-value o 'a))
  )

(defun test (s c)
  (declare (type super s))
  (declare (type child c))
  (list (list "s.a =" (slot-value s 'a))
        (list "c.a =" (slot-value c 'a))
        (show s)
        (show c)
        )
  )

(test (make-instance 'child) (make-instance 'child))
;; result: '(("s.a =" 6) ("c.a =" 6) ("Child method called a = " 6) ("Child method called a = " 6))

在纯OOP中(根据某些内容),对象不应该有公共字段,只有公共成员,因此静态解析字段不是问题。

  

C ++中的所有方法(也在C语言中)默认为static [...]

不,因为static在Java中意味着“不能被覆盖”,并不意味着在C ++中完全没有意义。在两者中,静态方法的基本属性是它们是类方法,通过类而不是实例访问(尽管Java允许它们被访问)。

答案 2 :(得分:2)

看看C#世界。严格禁止使用公共实例变量 - 请参阅StyleCop。唯一推荐的方法是使用可以覆盖的属性。

我认为这是正确的做法。只是不要使用公共实例变量并使用类比属性。

关于为什么在Java中这样,我认为某种类似C ++的风格在Java被设计为一种简化过渡的方式时被采用。

答案 3 :(得分:1)

在实例字段中几乎没有覆盖。您只需要用相同的东西替换它:相同的名称,相同的类型。您可以使用不同长度的数组替换数组,但是您需要仔细查看所有超类,以确保它们不会对长度进行假设。

正如其他人所指出的那样,非私人领域通常需要避免。有时一个类更像是一个C结构,有公共字段而没有方法。这是非常好的做法,但是压倒任何东西仍然没有意义。

覆盖实际上只是关于方法,而不考虑语言。 Java没有像C#这样的属性,它们本身就是(有限的)类,需要更改方法。字段可以看起来和行为类似属性,但它们根本不是一回事。