【面向对象的四大特征】以前在本科阶段第一次学习Java语言的时候,老师经常会告诉我,面向对象的三大特征是:继承、封装和多态。而今天在看书的时候,书中对于面向对象的特征主要列举了四个,细细一想也是有道理的。面向对象的主要特征有四,分别为:抽象、继承、封装和多态。而接下来,我们主要谈一谈多态这一特性。
【什么是多态】多态是指允许不同类型的对象对同一消息做出响应。多态包括参数化多态和包含多态。它表示当同一操作作用在不同对象时,会有不同的语义,从而产生不同的结果。例如同样是执行“+”操作,“3+4”的结果为整数7,而“3”+“4”则变成了字符串的拼接,结果当然为字符串“34”。在Java语言中,多态主要有以下两种表现形式:
(1)方法的重载(Overload)。重载是指同一个类中有多个同名方法,但是这些方法有着不同的参数,因此在编译时就可以确定到底调用的是哪个方法,它是一种编译时多态。重载可以被看作一个类中的方法多态性。
(2)方法的重写(覆盖,Override)。子类可以覆盖父类的方法,因此同样的方法会在父类与子类中有着不同的表现形式。在Java语言中,基类的引用变量不仅可以指向基类的实例对象,也可以指向其子类的实例对象(这就是向上转型,但是向上转型后必定会造成一定方法和属性的丢失,即父类的引用可以调用父类中的属性和方法,而对于只存在于子类中的方法和属性将无法调用,尽管是重载该方法。若子类重写了父类的某些方法,在调用该些方式时,必定是使用子类中定义的方法。)。同样,接口的引用变量也可以指向其实现类的实例对象。而程序调用的方法在运行期间才动态绑定(绑定是指将一个方法调用和一个方法主体连接到一起),就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。通过这种动态的方法实现了多态。由于只有在运行时才能确定调用了哪个方法,因此通过方法覆盖实现的多态也可以称为运行时多态。
【关于多态的一个误区】多态这个概念是类中方法才具有的概念,类中的成员变量没有多态的概念。成员变量是无法实现多态的,成员变量的取值父类还是子类并不取决于创建时对象的类型,而是取决于所定义变量的类型,这是在编译期间确定的。
【再谈重载】重载是一个类中多态性的一种表现,是指在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型。在使用重载时,需要注意一下几点:
① 重载是通过不同的方法参数来区分的,例如不同的参数个数、不同的参数类型或不同的参数顺序。
② 不能通过方法的访问权限、返回值类型(返回值可以相同也可以不同)和抛出的异常类型来进行重载。
③ 对于继承来说,如果基类方法的访问权限为private,那么就不能在派生类对其进行重载。如果派生类中也定义了一个同名的函数,这只是一个新的方法,不会达到重载的效果。
【再谈覆盖】覆盖是指派生类函数覆盖基类函数。覆盖一个方法并对其进行重写,以达到不同的作用。在使用时应注意以下几点:
① 派生类中的覆盖方法必须要和基类中被覆盖的方法有相同的函数名和参数。
② 派生类中覆盖方法的返回值必须和基类中被覆盖的方法的返回值相同。
③ 派生类中的覆盖方法所抛出的异常必须和基类(或是其子类)中被覆盖的方法所抛出的异常一致。
④ 基类中被覆盖的方法不能为private,否则其子类只是定义了一个方法,并没有对其覆盖。
⑤ 子类重写的方法的可见性必须大于或等于父类中被重写方法的可见性。
⑥ 不能用子类的静态方法隐藏父类的实例方法,也不能用子类的实例方法隐藏父类的静态方法。
【重写与覆盖的主要区别】重写与覆盖的主要区别如下:
① 覆盖是子类和父类之间的关系,是垂直关系;重载是用一个类中方法之间的关系,是水平关系。
② 覆盖只能由一个方法或只能由一堆方法产生关系;重载是多个方法之间的关系。
③ 覆盖要求参数列表相同;重载要求参数列表不同。
④ 覆盖关系中,调用方法体是根据对象的类型(对象对应的存储空间类型)来决定;重载关系是根据调用时的实参表与形参表来选择方法体的。
【多态举例】
首先定义父类,英雄类
package test; public class Hero { public String name; public Hero(){ } public String getName() { return name; } public void setName(String name) { this.name = name; } public void p(){ System.out.println("英雄的被动技能"); } public void q(){ System.out.println("英雄的Q技能"); } public void w(){ System.out.println("英雄的W技能"); } public void e(){ System.out.println("英雄的E技能"); } public void r(){ System.out.println("英雄的R技能"); } }
然后定义其子类,VN类
package test; public class VN extends Hero { public String name; public VN(){ } public String getName() { return name; } public void setName(String name) { this.name = name; } public void p(){ //对父类方法的重写 System.out.println("VN的被动技能,朝着敌方英雄移动会加速"); } public void q(){ //对父类方法的重写 System.out.println("VN的Q技能,翻滚一段距离"); } public void w(){ //对父类方法的重写 System.out.println("VN的W技能,三环基于最大生命值"); } public void e(){ //对父类方法的重写 System.out.println("VN的E技能,击退敌人一段距离"); } public void r(){ //对父类方法的重写 System.out.println("VN的R技能,加攻击力"); } public void play(){ System.out.println("直接平A三下打出三环"); } public void play(boolean can_play){ //对play()方法的重写 if(can_play){ System.out.println("平A两下后Q出三环"); }else{ System.out.println("AQEARQAEQAAAA......"); } } public String play(String player_name){ //对play()方法的重写 if("uzi".equals(player_name)) return "666,厉害,还是有东西的"; else return "还是差了点"; } }
最后为测试类
package test; public class Test { public static void main(String[] args) { Hero hero = new VN(); //父类对象指向子类的引用,即向上转型 hero.p(); //hero.play(); //该方法为子类特有的方法,向上转型后,将丢失该方法 } }
此时程序的执行结果为:
VN的被动技能,朝着地方英雄移动会加速
【结论1】由于任何类都继承自Object类,因此Object类、Hero类和VN类的继承关系为:VN——>Hero——>Object。由此我们可以得出:当子类重写的父类方法被调用时,只有对象继承链中的最末端的方法才会被调用。
【结论2】当父类对象引用变量(hero)引用子类对象时,被引用对象的类型(VN类型)(而不是引用变量的类型,即Hero类型)决定了调用谁的成员方法,但是这个被调用的方法(方法p)必须是在超类中定义过的,也就是说被子类覆盖的方法,但是它仍然要根据继承链中方法调用的优先级来确认方法,该优先级为:
this.show(O)-->super.show(O)-->this.show((super)O)-->super.show((super)O)。
【经典例题】
package test; class A { public String show(D obj) { return ("A and D"); } public String show(A obj) { return ("A and A"); } } class B extends A{ public String show(B obj){ return ("B and B"); } public String show(A obj){ return ("B and A"); } } class C extends B{ } class D extends B{ } public class Test { public static void main(String[] args) { A a1 = new A(); A a2 = new B(); B b = new B(); C c = new C(); D d = new D(); System.out.println("1--" + a1.show(b)); System.out.println("2--" + a1.show(c)); System.out.println("3--" + a1.show(d)); System.out.println("4--" + a2.show(b)); System.out.println("5--" + a2.show(c)); System.out.println("6--" + a2.show(d)); System.out.println("7--" + b.show(b)); System.out.println("8--" + b.show(c)); System.out.println("9--" + b.show(d)); } }
【执行结果】
1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D
【分析第5个例子】第5个列子为输出a2.show(c);的结果。由于A a2 = new B();是一个向上转型的情况,根据结论2,首先分析其中的a2为父类对象的引用变量引用了子类B类的对象,即a2的类型为A类型,即this为A。但是由被引用对象的类型,即B类型决定了调用的是B类中的方法,但是这个方法必须在A类中定义过。此时的show()方法的参数为C类型,由于在A类中没有show(C object)方法,因此我们直接跳到第三阶段,即this.show((super) O)。其中C类的父类有B类和A类,而此时this指的是A,即A.show(B)或者A.show(A)方法。由于要满足B类决定调用的方法,并且该方法在A类中必须定义的要求,因此最后实际调用的是B类中的show(A object)方法,最后的结果也自然是:B and A。
【部分参考自】http://cmsblogs.com/?p=52