java 多态

本文转载自:https://github.com/kxchen/kxchen.github.io/issues/2

我们都知道面向对象有四个基本特性:抽象、封装、继承、多态。这四个特性,概括起来可以这么理解,抽象、封装、继承是多态的基础,多态是抽象、封装、继承的表现。多态,是Java中非常重要的一个部分,所以今天来谈一下多态(Polymorphism)。

什么是多态

多态,简而言之就是同一个行为具有多个不同表现形式或形态的能力。比如说,有一杯水,我不知道它是温的、冰的还是烫的,但是我一摸我就知道了。我摸水杯这个动作,对于不同温度的水,就会得到不同的结果。这就是多态。

多态的作用

简单讲就是解耦。再详细点讲就是,多态是设计模式的基础,不能说所有的设计模式都使用到了多态,但是23种中的很大一部分,都是基于多态的。

多态存在的三个条件

  1. 有继承关系(在多态中必须存在有继承关系的子类和父类。)  
  2. 子类重写父类方法(子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。)

补充:既然多态存在必须要有“子类重写父类方法”这一条件,那么以下三种类型的方法是没有办法表现出> 多态特性的(因为不能被重写):

  • static方法,因为被static修饰的方法是属于类的,而不是属于实例的
  • final方法,因为被final修饰的方法无法被子类重写
  • private方法和protected方法,前者是因为被private修饰的方法对子类不可见,后者是因为尽管被protected修饰的方法可以被子类见到,也可以被子类重写,但是它是无法被外部所引用的,一个不能被外部引用的方法,怎么能谈多态呢  
  1. 父类引用指向子类对象(向上转型。在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。)

多态的分类

  1. 编译时多态,即方法的重载,从JVM的角度来讲,这是一种静态分派(static dispatch)

重载式多态,也叫编译时多态。也就是说这种多态再编译时已经确定好了。重载大家都知道,方法名相同而参数列表不同的一组方法就是重载。在调用这种重载的方法时,通过传入不同的参数最后得到不同的结果。

  1. 运行时多态,即方法的重写,从JVM的角度来讲,这是一种动态分派(dynamic dispatch)

这种多态通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。
这种多态通过函数的重写以及向上转型来实现,我们上面代码中的例子就是一个完整的重写式多态。我们接下来讲的所有多态都是重写式多态,因为它才是面向对象编程中真正的多态。

向上转型与向下转型

向上转型

子类引用的对象转换为父类类型称为向上转型。通俗地说就是是将子类对象转为父类对象。此处父类对象可以是接口。

       public class Animal {
            public void eat(){
            System.out.println("animal eatting...");
            }
        }

      public class Cat extends Animal{
            public void eat(){
            System.out.println("我吃鱼");
            }
       }

      public class Dog extends Animal{
           public void eat(){
                System.out.println("我吃骨头");
           }

           public void run(){
                System.out.println("我会跑");
            }
      }

      public class Main {
            public static void main(String[] args) {
                 Animal animal = new Cat(); //向上转型
                 animal.eat();
                 animal = new Dog();
                 animal.eat();
            }
       }

     //结果:
     //我吃鱼
    //我吃骨头

这就是向上转型,Animal animal = new Cat();将子类对象Cat转化为父类对象Animal。这个时候animal这个引用调用的方法是子类方法。

转型过程中需要注意的问题
  • 向上转型时,子类单独定义的方法会丢失。比如上面Dog类中定义的run方法,当animal引用指向Dog类实例时是访问不到run方法的,animal.run()会报错。
  • 子类引用不能指向父类对象。Cat c = (Cat)new Animal()这样是不行的。
向上转型的好处
  • 减少重复代码,使代码变得简洁。
  • 提高系统扩展性。

举个例子:比如我现在有很多种类的动物,要喂它们吃东西。如果不用向上转型,那我需要这样写:

      public void eat(Cat c){
          c.eat();
      }

      public void eat(Dog d){
          d.eat();
      }
     //......

      eat(new Cat());
      eat(new Cat());
      eat(new Dog());
     //......

一种动物写一个方法,如果我有一万种动物,我就要写一万个方法,写完大概猴年马月都过了好几个了吧。好吧,你很厉害,你耐着性子写完了,以为可以放松一会了,突然又来了一种新的动物,你是不是又要单独为它写一个eat方法?开心了么?
那如果我使用向上转型呢?我只需要这样写:

      public void eat(Animal a){
          a.eat();
       }

     eat(new Cat());
     eat(new Cat());
     eat(new Dog());
     //.....

恩,搞定了。代码是不是简洁了许多?而且这个时候,如果我又有一种新的动物加进来,我只需要实现它自己的类,让他继承Animal就可以了,而不需要为它单独写一个eat方法。是不是提高了扩展性?

向下转型

与向上转型相对应的就是向下转型了。向下转型是把父类对象转为子类对象。(请注意!这里是有坑的。)

      //还是上面的animal和cat dog
      Animal a = new Cat();
      Cat c = ((Cat) a);
      c.eat();
      //输出  我吃鱼
      Dog d = ((Dog) a);
      d.eat();
      // 报错 : java.lang.ClassCastException:com.chengfan.animal.Cat cannot be cast to     com.chengfan.animal.Dog
      Animal a1 = new Animal();
      Cat c1 = ((Cat) a1);
      c1.eat();
      // 报错 : java.lang.ClassCastException:com.chengfan.animal.Animal cannot be cast to com.chengfan.animal.Cat

为什么第一段代码不报错呢?相比你也知道了,因为a本身就是Cat对象,所以它理所当然的可以向下转型为Cat,也理所当然的不能转为Dog,你见过一条狗突然就变成一只猫这种操蛋现象?

而a1为Animal对象,它也不能被向下转型为任何子类对象。比如你去考古,发现了一个新生物,知道它是一种动物,但是你不能直接说,啊,它是猫,或者说它是狗。

向下转型注意事项
  • 向下转型的前提是父类对象指向的是子类对象(也就是说,在向下转型之前,它得先向上转型)
  • 向下转型只能转型为本类对象(猫是不能变成狗的)

大概你会说,我特么有病啊,我先向上转型再向下转型??

我们回到上面的问题:喂动物吃饭,吃了饭做点什么呢?不同的动物肯定做不同的事,怎么做呢?

      public void eat(Animal a){
           if(a instanceof Dog){  
               Dog d = (Dog)a;
               d.eat();
               d.run();//狗有一个跑的方法      
           } 
           if(a instanceof Cat){  
               Cat c = (Cat)a;
               c.eat();
               System.out.println("我也想跑,但是不会"); //猫会抱怨    
           } 
         a.eat();//其他动物只会吃
       }
      eat(new Cat());
      eat(new Cat());
      eat(new Dog());
      //.....

现在,你懂了么?这就是向下转型的简单应用,可能举得例子不恰当,但是也可以说明一些问题。

经典案例分析多态

基本的多态和转型我们都会了,最后加点餐。看一个经典案例:

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 Demo {
    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

//能看懂这个结果么?先自分析一下。

前三个,强行分析,还能看得懂。但是第四个,大概你就傻了吧。为什么不是b and b呢?
这里就要学点新东西了。

当父类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,引用变量类型决定可调用的方法。如果子类中没有覆盖该方法,那么会去父类中寻找。

可能读起来比较拗口,我们先来看一个简单的例子:

class X {
    public void show(Y y){
        System.out.println("x and y");
    }

    public void show(){
        System.out.println("only x");
    }
}

class Y extends X {
    public void show(Y y){
        System.out.println("y and y");
    }
    public void show(int i){

    }
}

class main{
    public static void main(String[] args) {
        X x = new Y();
        x.show(new Y());
        x.show();
    }
}
//结果
//y and y
//only x

Y继承了X,覆盖了X中的show(Y y)方法,但是没有覆盖show()方法。

这个时候,引用类型为X的x指向的对象为Y,这个时候,调用的方法由Y决定,会先从Y中寻找。执行x.show(new Y());,该方法在Y中定义了,所以执行的是Y里面的方法;
但是执行x.show();的时候,有的人会说,Y中没有这个方法啊?它好像是去父类中找该方法了,因为调用了X中的方法。

事实上,Y类中是有show()方法的,这个方法继承自X,只不过没有覆盖该方法,所以没有在Y中明确写出来而已,看起来像是调用了X中的方法,实际上调用的还是Y中的。

这个时候再看上面那句难理解的话就不难理解了吧。X是引用变量类型,它决定哪些方法可以调用;show()show(Y y)可以调用,而show(int i)不可以调用。Y是被引用对象的类型,它决定了调用谁的方法:调用y的方法。

上面的是一个简单的知识,它还不足以让我们理解那个复杂的例子。我们再来看这样一个知识:

继承链中对象方法的调用的优先级:this.show(O)super.show(O)this.show((super)O)super.show((super)O)
如果你能理解这个调用关系,那么多态你就掌握了。我们回到那个复杂的例子:
abcd的关系是这样的:C/D —> B —> A

我们先来分析4 :a2.show(b)

  • 首先,a2是类型为A的引用类型,它指向类型为B的对象。A确定可调用的方法:show(D obj)show(A obj)
    -a2.show(b) ==>this.show(b),这里this指的是B。
  • 然后.在B类中找show(B obj),找到了,可惜没用,因为show(B obj)方法不在可调用范围内,this.show(O)失败,进入下一级别:super.show(O),super指的是A。
  • 在A 中寻找show(B obj),失败,因为没用定义这个方法。进入第三级别:this.show((super)O),this指的是B。
  • 在B中找show((A)O),找到了:show(A obj),选择调用该方法。
  • 输出:B and A

如果你能看懂这个过程,并且能分析出其他的情况,那你就真的掌握了。

我们再来看一下9:b.show(d)

  • 首先,b为类型为B的引用对象,指向类型为B的对象。没有涉及向上转型,只会调用本类中的方法。
  • 在B中寻找show(D obj),方法。现在你不会说没找到了吧?找到了,直接调用该方法。
  • 输出 A and D。

总结

本篇文章的内容大体上就是这些了。我们来总结一下。

  • 多态,简而言之就是同一个行为具有多个不同表现形式或形态的能力。
  • 多态的分类:运行时多态和编译时多态。
  • 运行时多态的前提:继承(实现),重写,向上转型
  • 向上转型与向下转型。
  • 继承链中对象方法的调用的优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值