什么是Java中的多态?又是一个纸老虎的概念,老套路,把它具体化,细分化,先想三个问题(注意,这里不是简单的化整为零,而是要建立在学习一个新概念时的思考框架):
1.这个东西有什么用?用来干什么的?它的意义在哪里?(显然,如果是没用的东西,就没必要浪费时间了;其实,弄懂了这个问题,就掌握了50%)
2.这个概念或者技能点怎么用?也就是它的表现形式,如关键字、修饰词、语法什么的。。。(这个占25%)
3.这个东西在用的过程中,有哪些关键点和细节点?(是的,也占25%)
上面三个问题搞清楚了,剩下的就是去用了。。。“无他,但手熟尔。”
一、第一个问题:多态有什么用?它存在的意义是什么?
多态的作用:使代码具有松耦合性,满足开闭原则。(多态,意味着一个对象有着多重特征,可以在特定的情况下,表现不同的状态,从而对应着不同的属性和方法。)WTF?又装x!松耦合是什么鬼?还开闭原则?能不能直给?好吧,所谓的“耦合”就是,当你扩展功能的时候,其他与之相应的源代码也要修改,有点像麻花一样纠缠不清;“松耦合”是指你虽然扩展了,但不需要修改其他的代码。这样就达到了对扩展开放,对修改关闭的效果。
具体怎样,看个例子就懂了:小强,一个农村热血青年,来到大城市打工,每天披星戴月,辛勤工作,终于存了些钱。于是小强带着钱回到老家,请村东头的媒婆王大娘给自己介绍对象。小强在焦急等待几天之后,看到王大娘笑眯眯走过来,“强啊,大娘给你找到姑娘了”。。。
故事先讲到这儿,我们用代码来把这件事实现一下:
1 public class Girl{ //父类--姑娘
2 public int faceScore=60; //颜值
3 public int love=0;
4 public void say(){ //自我介绍
5 System.out.println("hello world!");
6 }
7 public void addLove(){ //被求爱后,增加爱心值
8 }
9 }
10 public class HubeiGirl extends Girl{ //子类--湖北姑娘
11 public int faceScore=70;
12 public void say(){
13 System.out.println("我叫小红,我很聪明,也很会做饭。我的爱心值:"+love);
14 }
15 public void addLove() { //重写增加爱心值方法
16 love+=20;
17 System.out.println("我的爱心值是:"+love);
18 }
19 public void cook(){ //特有方法--做饭
20 System.out.println("红丸子,炸丸子,四喜丸子。。。");
21 }
22 }
23 public class HunanGirl extends Girl{ //子类--湖南姑娘
24 public int faceScore=85;
25 public void say(){
26 System.out.println("我叫小倩,我很可爱,也很会唱歌。我的爱心值:"+love);
27 }
28 public void addLove() { //重写增加爱心值方法
29 love+=10;
30 System.out.println("我的爱心值是:"+love);
31 }
32 public void sing(){ //特有方法--唱歌
33 System.out.println("辣妹子辣~辣妹子辣~~");
34 }
35 }
代码很简单,一个姑娘父类,两个子类分别是湖北姑娘和湖南姑娘,好,现在小强见了两个姑娘想向她们分别表达爱意(被求爱后,爱心值会增加),该怎么实现?代码如下:
1 public class XiaoQiang{ //小强类
2 public void courting(HubeiGirl b){ //向湖北姑娘表达爱意
3 b.addLove();
4 }
5 public void courting(HunanGirl n){ //向湖南姑娘表达爱意
6 n.addLove();
7 }
8 }
9 public class Test{
10 public static void main(String[] argrs){
11 HubeiGirl xiaohong = new HubeiGirl();
12 xiaohong.say();
13 HunanGirl xiaoqian = new HunanGirl();
14 xiaoqian.say();
15 XiaoQiang qiang = new XiaoQiang();
16 qiang.courting(xiaohong); //小强向湖北姑娘小红表达爱意
17 qiang.courting(xiaoqian); //小强向湖南姑娘小倩表达爱意
18 }
19 }
这里先定义了一个小强类,里面有两个表达爱意的方法,参数是不同的对象类型,属于重载。测试类中创建小红、小倩和小强对象,运行结果如下:
由结果可以看到,传入不同的对象类型参数,小强调用不同的表达爱意的方法courting(),要求满足,bingo!故事继续,王大娘的婚介事业已经走出中国,在向国际化发展,王大娘认识一个非洲姑娘玛丽卡(如上图),要把玛丽卡介绍给小强。。。这要怎么实现?如果照着上面来:
1 public class AfricaGirl extends Girl{ //子类--非洲姑娘
2 public int faceScore=80;
3 public void say(){
4 System.out.println("我叫玛丽卡,我很热情,也很会跳舞。我的爱心值:"+love);
5 }
6 public void addLove() { //重写增加爱心值方法
7 love+=15;
8 System.out.println("我的爱心值是:"+love);
9 }
10 public void dance(){ //特有方法--跳舞
11 System.out.println("动起来!gogogogo for it!动起来!");
12 }
13 }
14 public class XiaoQiang{ //小强类
15 public void courting(HubeiGirl b){ //向湖北姑娘表达爱意
16 b.addLove();
17 }
18 public void courting(HunanGirl n){ //向湖南姑娘表达爱意
19 n.addLove();
20 }
21 public void courting(AfricaGirl a){ //向非洲姑娘表达爱意
22 a.addLove();
23 }
24 }
这种做法是先定义子类非洲姑娘,这是必须的,在小强类里加了一个courting(AfricaGirl a)方法,这样也能实现要求,但是,这样有两个问题:1.增加非洲姑娘类的时候,改动了小强类,不是松耦合,不满足开闭原则;2.假如王大娘的业务发展的很好,要给小强介绍100个姑娘,难道要改变100次小强类,往里面加100个方法吗?
所以,问题来了:要怎样定义小强类,使得即使不断增加姑娘也不用改动小强类?这里就要用到多态了,也是开篇第二个问题的答案。
二、第二个问题:多态怎么用?
其实,就一句话:父类类型的引用指向子类的对象。用多态的思想来定义上面的小强类,如下:
1 public class XiaoQiang{ //小强类
2 public void courting(Girl g){ //参数为父类类型
3 g.addLove();
4 }
5 }
很神奇,可以看到用多态的思想来做,只需要一个参数为父类Girl类型的方法就够了,先来测试一下:
1 public class Test {
2 public static void main(String[] args) {
3 HubeiGirl xiaohong = new HubeiGirl();
4 xiaohong.say();
5 HunanGirl xiaoqian = new HunanGirl();
6 xiaoqian.say();
7 AfricaGirl malika = new AfricaGirl();
8 malika.say();
9 XiaoQiang qiang = new XiaoQiang();
10 qiang.courting(xiaohong); //小强向湖北姑娘小红表达爱意
11 qiang.courting(xiaoqian); //小强向湖南姑娘小倩表达爱意
12 qiang.courting(malika); //小强向非洲姑娘玛丽卡表达爱意
13
14 }
15 }
功能实现,而且现在无论王大娘给小强介绍多少姑娘都不用修改小强类了。那么,为什么把参数从子类类型变成父类类型就能达到如此效果,就是多态呢?
这里我们先来仔细看一下使用父类作为方法形参实现多态的过程,以“小强向非洲姑娘玛丽卡表达爱意”为例,qiang.courting(malika)执行的时候,由于对象作为参数时,传入的只是对象的引用,因此参数部分发生的是:Girl g = malika,也就是将父类类型Girl的引用g指向了子类对象malika(有的说法是,将子类类型的指针赋值给父类类型的指针,一个意思,一般指针是C和C++里的概念),是的,这就是Java机制允许的父类类型的引用指向子类的对象,就是多态。调用过程是:当我们用一个父类型引用指向子类对象时,会先访问子类中重写的父类方法(父类的方法不会再执行),如果子类没有重写父类的方法,才会执行父类中的方法。而这种调用方式,就是多态的一种状态,叫做向上转型,也是最为容易理解的一种多态方式。具体到上面的例子是,先访问子类AfricaGirl中的重写的addLove()方法,有,就不会再去访问父类Girl中的addLove()方法了。
好,故事继续,老人家讲过:凡是不以结婚为目的的谈恋爱都是耍流氓。小强是正经人,于是他要选一个最中意的姑娘,并把这个姑娘的对象返回出来,好让王大娘进行其它操作。。。这个需求怎么实现?
1 public class XiaoQiang{ //小强类
2 public void courting(Girl g){ //参数为父类类型
3 g.addLove();
4 }
5
6 public Girl chooseGirl(int num) { //选择姑娘的方法,返回值为Girl类型
7 switch(num) {
8 case 1:
9 HubeiGirl b = new HubeiGirl(); //num为1时,返回湖北姑娘的对象b
10 return b;
11 case 2:
12 HunanGirl n = new HunanGirl();
13 return n;
14 case 3:
15 AfricaGirl a = new AfricaGirl();
16 return a;
17 default:
18 break;
19 }
20 return null;
21 }
22 }
可以看到,小强类中增加了一个选择姑娘的方法,根据不同的数字返回不同的子类姑娘对象,重点是它的返回值是父类Girl类型,这就是使用父类作为返回值实现多态。与上面使用父类作为方法形参实现多态相对,这里是在返回值的时候将父类类型的引用指向子类对象。在测试类Test中调用一下:
1 public class Test {
2 public static void main(String[] args) {
3 XiaoQiang qiang = new XiaoQiang();
4 Girl g = qiang.chooseGirl(1); //返回值是Girl类型,将返回值赋给Girl类型的引用g
5 qiang.courting(g); //表达爱意,增加爱心值
6 g.say();
7 }
8 }
可以看到,传入数字1,返回的是湖北姑娘小红,既然小红很会做饭,那我们就调用一下她做饭的方法cook(),如下:
看样子不太妙,报错说cook()方法没有定义,有人可能会问子类HubeiGirl中不是有cook()方法吗,问题是现在的g是父类Girl类型的引用,而父类Girl中是没有cook()方法的。这就涉及到向上转型的一个特点:向上转型后,父类类型的引用只能调用子类中的重写的父类方法和父类中的方法,而不能调用子类中特有的方法。这里cook()是子类HubeiGirl特有的方法,所以不能调用。
娶个老婆居然不会做饭,小强肯定不高兴,那该怎么办?这里就需要将返回的父类型返回值强转为子类型,并将其赋值给相应子类型的引用。如下:
可以看到,调用cook()成功!如图所示,将父类Girl类型的返回值强转成子类HubeiGirl类型,再将其赋给子类HubeiGirl类型的引用g,就可以调用子类HubeiGirl中特有的方法了。这种将子类型对象向上转型成父类型后,再将其转回子类型的过程就是所谓的向下转型。(注:不能直接将父类型对象转型成子类型,一定要之前有向上转型这个过程才行。)
好,故事讲完,小强和小红从此过上了幸福的生活。
三、第三个问题:使用多态的关键点和细节?
这个问题在二中已经回答得差不多了,总结一下:
1.继承:多态是发生在有继承关系的子类和父类中的。
2.重写:多态就是多种形态。也就是说,当我们需要实现多态的时候,就需要有父类的方法被子类重写。否则,如果没有重写的方法,就看不出多态的特性,一切按照父类的方法来,还不如不要继承,直接在父类中添加相应的方法,然后在实例化好了。
3.向上转型:父类类型的引用指向子类的对象。
4.向下转型:将子类型对象向上转型成父类型后,再将其转回子类型的过程。
5.在Java中有两种形式可以实现多态,继承和接口。原理相同,本文只讲了继承情况。
四、经典例题
既然学了东西就得用,不然那学东西有什么用。下面是一道关于多态的经典例题,可以试一下不看答案能不能做出来:
1 public class A {
2 public String show(D obj) {
3 return ("A and D");
4 }
5 public String show(A obj) {
6 return ("A and A");
7 }
8 }
9
10 public class B extends A{
11 public String show(B obj){
12 return ("B and B");
13 }
14 public String show(A obj){
15 return ("B and A");
16 }
17 }
18
19 public class C extends B{
20
21 }
22 public class D extends B{
23
24 }
25
26 public class Test {
27 public static void main(String[] args) {
28 A a1 = new A();
29 A a2 = new B();
30 B b = new B();
31 C c = new C();
32 D d = new D();
33
34 System.out.println("1--" + a1.show(b));
35 System.out.println("2--" + a1.show(c));
36 System.out.println("3--" + a1.show(d));
37 System.out.println("4--" + a2.show(b));
38 System.out.println("5--" + a2.show(c));
39 System.out.println("6--" + a2.show(d));
40 System.out.println("7--" + b.show(b));
41 System.out.println("8--" + b.show(c));
42 System.out.println("9--" + b.show(d));
43 }
44 }
运行结果如下:
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
不知道大家做对了几个,有没有像福尔摩斯推理探案一样的感觉,现在就让我们戴上猎鹿帽,叼上烟斗,拿着放大镜开始吧,首先看看下面这张表示ABCD四个类关系的图片:
由上图可以看到:
1.B类中的show(A obj)是继承并重写了A类中的show(A obj)方法;
2.B类中的show(B obj)是重载,B相对A特有的方法;
3.B继承了A的方法,C和D继承了B的方法(包含了A的方法)。
现在来一个一个看:
1---- a1.show(b)
子类对象b作为参数,而a1只能调用show(D obj)和show(A obj)两个方法,显然调用show(A obj),典型的多态应用。
2----a1.show(c)
子类的子类对象c作为参数,同样的,a1只能show(D obj)和show(A obj)两个方法,显然调用show(A obj),这里表现的是多态中,子类对象不光可以赋给父类的引用,父类以上的引用都可以。而Java里最终极的父类是Object,所以,Object的引用可以指向任何对象。
3----a1.show(d)
子类的子类对象d作为参数,同样的,a1只能show(D obj)和show(A obj)两个方法,显然。。。额,里面有D类型作为参数的方法,所以显然调用show(D obj)。
4----a2.show(b)
A a2 = new B(); 其中,a2是父类型的引用指向了子类B类型的对象,多态的应用,所以a2能调用的方法有:A类中的show(D obj)、B类中的show(A obj)两个方法。为什么不能调用A类中的show(A obj)方法?因为子类B已经继承并重写了父类A类中的show(A obj), 所以只会访问子类B中的show(A obj),不会再去访问父类A中的show(A obj)。为什么不能调用B类中的show(B obj)方法?因为此方法是子类B特有的方法,多态中父类A类型的引用a2不能访问。现在是a2.show(b),参数是子类对象b,显然调用B类中的show(A obj)方法。
5----a2.show(c)
同上,a2能调用的方法有:A类中的show(D obj)、B类中的show(A obj)两个方法。B的子类对象c作为实参,显然调用B类中的show(A obj)方法,类C的父类的父类型作为形参实现多态。
6---a2.show(d)
同上,a2能调用的方法有:A类中的show(D obj)、B类中的show(A obj)两个方法。B的子类对象d作为实参,里面有D类型作为形参的方法,所以显然调用A类中的show(D obj)方法。
7----b.show(b)
B b = new B();这个就是调用B类中的show(B obj)。
8----b.show(c)-------------------它仍然要按照继承链中调用方法的优先级来确认。
B b = new B();其中,B类继承了A类的方法,b可以调用的方法有:A类中的show(D obj)、B类中的show(A obj)、B类中的show(B obj)三个方法。B的子类对象c作为实参,调用父类B中的show(B obj),使用父类作为形参实现多态。为什么不能调用B类中的show(A obj)方法?类A是类C父类的父类,将A类型的引用指向类C的对象c不也是多态允许的?理论上是的,但实际情况是,现在有父类作为形参和父类的父类作为形参实现多态这两种选择。这时它就要按照按照继承链中调用方法的优先级来确认,父类B比父类的父类A优先。其中优先级为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
9----b.show(d)
同上,b可以调用的方法有:A类中的show(D obj)、B类中的show(A obj)、B类中的show(B obj)三个方法。。。额,里面有D类型作为参数的方法,所以显然调用A类中的show(D obj)。
不悲 不喜 不二