多态和覆盖
多态是面向对象编程中最为重要的概念之一,而覆盖又是体现多态最重要的方面。对于像c#和java这样的面向对象编程的语言来说,实现了在编译时只检查接口是否具备,而不需关心最终的实现,即最终的实现方式是在运行时才会决定。这给强类型语言提供了强大的灵活性,请看下面的例子:
1 using System; 2 3 namespace study00 4 { 5 class Person 6 { 7 public string Name { set; get; } 8 public virtual void sayHello() 9 { 10 Console.WriteLine("Hello. My Name is " + Name); 11 } 12 } 13 14 class Student :Person 15 { 16 public override void sayHello() 17 { 18 Console.WriteLine("Hello, i am a student. My Name is " + Name); 19 } 20 } 21 22 class Teacher :Person 23 { 24 public override void sayHello() 25 { 26 Console.WriteLine("Hello, i am a teacher. My Name is " + Name); 27 } 28 } 29 30 class Program 31 { 32 static void Main(string[] args) 33 { 34 Teacher t = new Teacher(); 35 t.Name = "XiaoMing"; 36 doSomething(t); 37 38 Student s = new Student(); 39 s.Name = "XiaoMing"; 40 doSomething(s); 41 } 42 43 static void doSomething(Person p) 44 { 45 p.sayHello(); 46 } 47 } 48 49 /* Output: 50 * Hello, i am a teacher. My Name is XiaoMing 51 * Hello, i am a student. My Name is XiaoMing 52 */ 53 }
在上面这段代码中,doSomething方法需要一个Person类型的对象,但是即使给他传递的是Student类型的对象和Teacher类型的对象,她也能够欣然接受,并根据响应的类型作出正确的处理。我们都知道,这是由于Student和Teacher都继承了Person类,当调用doSomething传递参数时,Student类型的对象发生了向上转型,也称为里氏替换,即父类能够出现的地方,子类一定能够进行替换。所以将Student类型的对象传递给doSomething方法,编译不会出错,而且doSomething方法能够正确执行。这便是面向对象给我们提供的便利,即我们不需要判断某个对象属于什么类型,只要他能够满足给定的要求(这里是继承了Person类),就能够正确运行。
实际上,从更抽象的角度来看,由于每一个类都可以看做某个接口的具体实现,Person可以看做是包含了sayHello()方法的接口的实现。那么我们可以说,Person与它的子类Student和Teacher都是同一个接口的实现,那么我们也就可以理解,doSomething()方法可以正确的处理实现了sayHello()方法的类型。对于doSomething()方法,他不需要知道传递过来的参数是Person类的对象还是它的子类,只要该对象实现了sayHello()方法(继承机制使得子类自动继承了父类的所有方法),那么就可以正确执行。
在上面的代码中,Person中已经有了相应的sayHello()方法的实现,但是当我们运行时却发现,程序运行的却是它的子类的同名方法。这就是所谓的覆盖,即子类隐藏了父类中的同名方法,通过覆盖Person的sayHello()方法,Person的子类实现了自己想要实现的方法,同时又可以向上转型为父类去参与doSomething()方法的运行。也就是说,通过覆盖父类的同名方法,程序实现了运行时的多态。
virtual和override关键字
实现多态时,用到了两个关键字,virtual和override,分别用在父类和子类的签名相同的方法中。什么是签名相同呢?签名相同就是方法同名、参数相同(个数相同、类型相同)、返回值相同。virtual用在父类方法中,表示该方法可以被覆盖。override用在子类的同签名方法中,表示重写了父类的同签名方法。override关键字修饰的的方法必须是和父类的方法是签名相同,而且父类的签名相同的方法必须是被virtual、abstract或者override关键字修饰。
new关键字
我们都知道在c#或java中,创建一个新的对象可以使用new关键字。而在c#中,new还有另一种用法,那就是作为方法的修饰符。new作为方法的修饰符起到了隐藏父类签名相同的方法,在某些情况下起到了和override关键字相同的效果,请看下面的代码:
1 using System; 2 3 namespace study001 4 { 5 class Person 6 { 7 public string Name { set; get; } 8 public virtual void sayHello() 9 { 10 Console.WriteLine("Hello. My Name is " + Name); 11 } 12 public void sayBey() 13 { 14 Console.WriteLine("Bey"); 15 } 16 } 17 18 class Student : Person 19 { 20 public override void sayHello() 21 { 22 Console.WriteLine("Hello, i am a student. My Name is " + Name); 23 } 24 public new void sayBey() 25 { 26 Console.WriteLine("Bey, don't forget me!"); 27 } 28 } 29 30 class OverrideAndNew 31 { 32 static void Main(string[] args) 33 { 34 Console.WriteLine("********demo1*********"); 35 Student s1 = new Student(); 36 s1.Name = "XiaoMing"; 37 s1.sayHello(); 38 s1.sayBey(); 39 40 Console.WriteLine("********demo2*********"); 41 Person p = new Student(); 42 p.Name = "LiHua"; 43 p.sayHello(); 44 p.sayBey(); 45 46 Console.WriteLine("********demo3*********"); 47 Student s2 = new Student(); 48 Person p2 = s2; 49 p2.Name = "LaoWang"; 50 p2.sayHello(); 51 p2.sayBey(); 52 } 53 } 54 55 /* Output: 56 * ********demo1********* 57 * Hello, i am a student. My Name is XiaoMing 58 * Bey, don't forget me! 59 * ********demo2********* 60 * Hello, i am a student. My Name is LiHua 61 * Bey 62 * ********demo3********* 63 * Hello, i am a student. My Name is LaoWang 64 * Bey 65 */ 66 }
在上面代码中的第一个示例中,Student类继承了Person类,并且覆盖了Person类的sayHello()方法,而对于父类中的sayBey()方法,子类中有对应的签名相同的方法,但是没有采用override关键字修饰,而是采用了new关键词修饰,并且父类的对应方法没有采用任何的关键字修饰。在这种情况下,从输出结果中可以看出,子类的对象也成功隐藏了父类的sayBey()方法,采用了自己的实现。这样的情况下,只需要子类的同名方法用new关键字修饰即可,貌似比override更加方便。而且更加重要的是,这个new关键字是可以省略的,也就是说默认情况下,子类的同名方法会隐藏父类的方法。
而从上面的第二个和第三个实例中我们就可以看到new关键字和override关键字的不同之处了。在第二个示例中,采用了父类类型Person的变量引用了子类类型Student的对象。在这种情况下,虽然被覆盖的sayHello()方法依然隐藏了父类的实现,采用了子类的实现,但是对于用new关键字修饰的sayBey()方法,却执行了父类的sayBey()方法。实际上,new关键字的作用是隐藏父类的方法,而并不是覆盖父类的方法。两者的不同就在于覆盖是完全覆盖父类的方法,任何时候通过子类对象都无法获取父类的方法;而隐藏则是在某种情况下是可以通过子类的对象去执行父类的方法的,这种情况就是采用了父类的变量引用了子类的具体对象。同样的,第三个实例和第二个实例相似,都是父类的变量引用了子类的对象,所以执行的都是父类的方法。
java中的方法覆盖
在java中,方法的覆盖要简单的多。子类和父类的同名方法不需要添加任何多余的关键字修饰,只要子类的方法与父类的方法签名相同,子类就会自动覆盖父类的方法。也就是说,java中默认实现了virtual和override关键字。而对于c#中的new关键字,java却没有相应的机制实现。这和c#正好是相反的,c#中如果子类的方法和父类的方法签名相同,则会隐藏父类的方法;java中如果子类的方法和父类的方法签名相同,则会覆盖父类的方法。请看下面的代码:
1 package study00.override; 2 3 class Person { 4 5 private String name; 6 7 public void sayHello() { 8 System.out.println("Hello, i am " + this.getName()); 9 } 10 11 public String getName() { 12 return name; 13 } 14 15 public void setName(String name) { 16 this.name = name; 17 } 18 } 19 20 class Student extends Person { 21 public void sayHello() { 22 System.out.println("Hello, i am a student, i am " + this.getName()); 23 } 24 } 25 26 public class Program { 27 public static void main(String[] args) { 28 Person p = new Student(); 29 p.setName("XiaoMing"); 30 p.sayHello(); 31 } 32 33 /** 34 * Output: 35 * Hello, i am a student, i am XiaoMing 36 */ 37 }
总结
- 多态是面向对象编程中最为重要的概念之一,而覆盖又是体现多态最重要的方面。
- c#中可以采用virtual和override关键字实现方法的覆盖,无法通过子类的对象获得父类的同名方法调用。override修饰的方法只能是父类中被abstract、virtual或者override关键字修饰的方法的同名方法。
- c#中可以采用new关键字修饰来实现隐藏父类的方法,在引用变量和实际对象的类型一致时与override关键字的效果相同,在引用变量是实际对象的父类会执行父类的方法,此时父类的方法不会被覆盖。
- java中默认实现覆盖,但是没有与c#中new关键字效果相同的机制。