1.🧀 回顾继承

那么在上一篇博客Java继承中,我们大致分析了继承的概念如何用父类和子类之间的关系来实现,以及各种应用场景,我们也另外分析了super,protected以及final在其中所起到的作用,并且介绍了组合的概念
image.png
那么接着上一篇的继承,我们在这一篇博客中介绍多态。

2.🥗 多态

2.1🥙 多态的概念

多态的概念:通俗来说,就是多种形态,那么在Java中,就是去完成某个行为,当不同的对象去完成时会产生不同的状态和表现
举两个简单的例子
无标题23.png
猫和狗.png
总的来说:同一件事,发生在不同对象身上,就会产生不同的结果

2.2🥪 多态实现条件

在Java中要实现多态,那么必须要满足以下几个条件,缺一不可:

  1. 必须在继承体系下
  2. 子类必须要对父类中的方法进行重写
  3. 通过父类的引用调用重写的方法

多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。

  1. public class Animal {
  2. String name;
  3. int age;
  4. public Animal(String name,int age){
  5. this.name = name;
  6. this.age = age;
  7. }
  8. public void eat(){
  9. System.out.println(name+"吃饭");
  10. }
  11. }
  12. public class Cat extends Animal{
  13. public Cat(String name, int age) {
  14. super(name, age);
  15. }
  16. @Override
  17. public void eat() {
  18. System.out.println(name+"吃鱼");
  19. }
  20. }
  21. public class Dog extends Animal{
  22. public Dog(String name, int age) {
  23. super(name, age);
  24. }
  25. @Override
  26. public void eat() {
  27. System.out.println(name+"吃骨头");
  28. }
  29. }
  30. ////////////////分割线/////////////////////
  31. public class TestAnimal {
  32. //编译器在编译代码的时候,并不知道要调用Dog还是Cat中eat的方法
  33. //等程序运行起来之后,形参a引用的具体对象确定后,才知道调用哪个方法
  34. //此时要注意:此处的形参类型必须是父类类型才可以,也就是向上转型
  35. public static void eat(Animal animal){
  36. animal.eat();
  37. }
  38. public static void main(String[] args){
  39. Cat cat = new Cat("元宝",2);
  40. Dog dog = new Dog("小七",1);
  41. eat(cat);
  42. eat(dog);
  43. }
  44. }

运行结果👇
image.png
在上述代码中,分割线上方的代码是类的实现者 编写的,分割线下方的代码是类的调用者编写的
当类的调用者在编写eat();这个方法的时候,参数类型为Animal(父类),此时在该方法内部并不知道,也并不关注当前的animal引用指向的是哪个类型(哪个子类)的实例。此时animal这个引用调用eat方法可能会又多种不同的表现(和animal引用的实例对象相关),这种行为就叫做多态
Java多态 - 图5

2.3🌮 重写

重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等进行重新编写的一个过程,返回值和形参都不能改变即外壳不改变,核心重写
重写的好处在于子类可以根据需要,定义特定的属于子类自己的行为。
也就是说子类能够根据需要来实现父类的方法,又和父类的方法不完全一样,实现自己的特色

2.3.1 🤯 [方法重写的规则]

  • 子类在重写父类的方法时,一般必须与父类方法原型一致:修饰符 返回值类型 方法名(参数列表)要完全一致
  • JDK7以后,被重写的方法返回值类型可以不同,但是必须是具有父子关系
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为protected
  • 父类被static、private修饰的方法都不能被重写
  • 子类和父类在同一个包中,那么子类可以重写父类中的所有方法,除了声明为private和final的方法
  • 子类和父类不在同一个包中,那么子类只能够重写父类的 声明为public 和protected的非final方法
  • 重写的方法,可以使用 @Override 注解来显式指定。有了这个注解能够帮我们检查这个方法有没有被正确重写。例如不小心讲方法名拼写错了,此时编译器就会发现父类中并没有这个方法,就会编译报错,构不成重写。

image.png

2.3.2 🤯 [generate小技巧]

我们右击,点击generate,然后发现这个选项
image.png
image.png
这样就可以自动生成重写的方法了
image.png

2.3.3 🤯 [重写和重载的区别]

区别点 重载(overloading) 重写(override)
参数列表 必须修改 一定不能修改
返回类型 可以修改 一定不能修改
访问限定符 可以修改 不能做出更严格的限制(子类权限大于父类)

即:方法重载式一个类的多态性的表现,而方法重写式子类与父类的一种多态性表现
11.png
image.png

2.3.4🤯 [重写的设计原则]

对于已经投入使用的类,我们要做到尽量不去进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程中,我们不应该在原来的老的类上进行修改,因为原来的类可能还有用户在使用,直接修改会影响到这些用户的使用效果,正确做法应该是新建一个手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求
112.png

🥫 静态绑定

也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用哪个方法。
典型代表函数重载

🫔 动态绑定

也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法

2.4🌯 向上转型和向下转型

2.4.1🍠 向上转型

image.png
向上转型:实际上就是创建一个子类对象,将其当成父类对象来舒勇
语法格式: 父类类型 对象名 = new 子类类型();

  1. Animal animal = new Cat("元宝"2);

animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换
Java多态 - 图14
无标题.png
猫和狗都是动物,因此将子类对象转化为父类引用时合理的,大范围可以囊括小范围,是安全的
🐲【使用场景】

  1. 直接赋值
  2. 方法传参
  3. 方法返回

    1. public class TestAnimal {
    2. //2.方法传参:形参为父类型引用,可以接受任意子类的
    3. public static void eatFood(Animal a){
    4. a.eat();
    5. }
    6. //3.作为返回值:返回任意子类对象
    7. public static Animal buyAnimal(String var){
    8. if("狗"==var){
    9. return new Dog("狗狗"1);
    10. }else if("猫"==var){
    11. return new Cat("猫猫",1);
    12. }else{
    13. return null;
    14. }
    15. }
    16. public static void main(String[] args){
    17. //1.直接赋值:子类对象赋值给父类对象
    18. Animal cat = new Cat("元宝"2);
    19. Dog dog = new Dog("小七",1);
    20. eatFood(cat);
    21. eatFood(dog);
    22. Animal animal = buyAnimal("狗");
    23. animal.eat();
    24. animal = buyAnimal("猫");
    25. animal.eat();
    26. }
    27. }

    向上转型的优点:让代码实现更加简单灵活
    向上转型的缺陷:不能调用到子类特有的方法

    2.4.2🥩 向下转型

    将一个子类对象经过向上转型之后当成父类对象使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
    image.png

    1. public class TestAnimal {
    2. Cat cat = new Cat("元宝",2);
    3. Dog dog = new Dog("小七",1);
    4. //向上转型
    5. Animal animal = cat;
    6. animal.eat();
    7. animal = dog;
    8. animal.eat();
    9. //下面这种情况会编译失败
    10. //编译时编译器将animal当作Animal的对象处理
    11. //而Animal类中并没有bark方法,因此编译就会失败
    12. //animal.bark();
    13. //向上转型
    14. //程序可以通过编译,但是运行的时候还是会抛出异常
    15. //因为:animal实际上指向的是狗的对象
    16. //现在要强制还原为猫则无法正常还原,运行时抛出:ClassCastException
    17. cat = (Cat)animal;
    18. cat.mew();
    19. //animal本来指向的就是狗,因此将animal还原为狗也是安全的
    20. dog = (Dog)animal;
    21. dog.bark();
    22. }

    向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛出异常。Java中为了提高向下转型的安全性,引入了instanceof,如果该表达式为true,则可以安全转换

    1. public class TestAnimal {
    2. public static void main(String[] args){
    3. Cat cat = new Cat("元宝",2);
    4. Dog dog = new Dog("小七",1);
    5. //向上转型
    6. Animal animal = cat;
    7. animal.eat();
    8. animal = dog;
    9. animal.eat();
    10. if(animal instanceof Cat){
    11. cat = (Cat)animal;
    12. cat.mew();
    13. }
    14. if(animal instanceof Dog){
    15. dog = (Dog)animal;
    16. dog.bark();
    17. }
    18. }
    19. }

    2.5🍗 多态的优缺点

    使用多态的好处
    1.能够降低代码的“圈复杂度”,避免使用大量的if-else

    什么叫“圈复杂度”? 圈复杂度是一种描述一段代码复杂程度的方式。 一段代码如果平铺直叙,那么就比较简单容易理解。 而如果有很多的条件分支或者循环语句,就认为理解起来更复杂 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个 数就称为“圈复杂度”。如果一个方法的圈复杂度太高,就需要考虑重构 不同公司对于代码的圈复杂度的规范不一样,一般不会超过10

例如我们现在需要打印的不是一个形状了,而是多个形状,如果不基于多态,实现代码如下👇

  1. public class DrawShapes {
  2. Rect rect = new Rect();
  3. Cycle cycle = new Cycle();
  4. Flower flower = new Flower();
  5. String[] shapes = {"cycle","rect","cycle","rect","flowers"};
  6. public static void main(String[] args) {
  7. Rect rect = new Rect();
  8. Cycle cycle = new Cycle();
  9. Flower flower = new Flower();
  10. String[] shapes = {"cycle","rect","cycle","rect","flower"};
  11. for(String shape:shapes){
  12. if(shape.equals("cycle")){
  13. cycle.draw();
  14. }else if(shape.equals("rect")){
  15. rect.draw();
  16. }else if(shape.equals("flower")){
  17. flower.draw();
  18. }
  19. }
  20. }
  21. }

输出结果为👇
image.png
如果使用多态,则不必写出这么多的if-else分支语句,代码将更加简单

    public static void drawShapes(){
        Shapes[] shapes = {new Cycle(),new Rect(),new Cycle(),
                           new Rect(),new Flower()};
        for(Shapes shape = shapes){
            shape.draw();
        }
    }

2.可扩展能力更强
如果要新增一种新的形状,使用多态的方式代码改动成本也比较低

class Triangle extends Shape {
    @Override
    public void draw(){
        System.out.println("▲");
    }
}

对于类的调用者来说,(drawShapes方法),只要创建一个新类的实例就可以了,改动成本很低。
而对于不用多态的情况,就要把drawShapes中的if-else进行一定的修改,改动成本更高
多态的缺陷:代码运行的效率降低

2.6🍖 避免在构造方法中调用重写的方法

这里介绍一个埋着坑的代码,我们创建两个类,B是父类,D是子类,D中重写了func的方法。并且B的构造方法中调用了func

class B{
    public B(){
        func();
    }
    public void func(){
        System.out.println("B.func()");
    }
}
class D extends B{
    private int num = 1;
    public void func(){
        System.out.println("D.func()"+num);
    }
}
public class Test {
    public static void main(String[] args){
        D d = new D();
    }
}

image.png
为啥这里的执行结果是0而不是1捏?

  • 构造D对象的时候,会调用B的构造方法。
  • B的构造方法中调用了func方法,此时会触发动态绑定,会调用到D中的func
  • 此时D对象自身还没有构造,此时num处在未初始化的状态,值为0

结论:“用尽量简单的方式使对象进入可工作状态”,尽量不要在构造器中调用方法(如果这个方法被子类重写,就会触发动态绑定,但是这个时候子类对象还没有构造完成),可能就会出现一些隐藏的且极难被发现的问题。

多态就介绍到这里
希望能帮到你
感谢阅读~