Java 基础系列(九) --- 认识多态,理解多态

发布于:2022-11-06 ⋅ 阅读:(398) ⋅ 点赞:(0)

多态

1 基本思想

 用一个图形类来说明多态的基本思想,每个图形都拥有绘制自己的能力,这种能力可以看做是该类具有的行为,如果将子类的对象看做是父类的实例对象,这样当绘制图形时,简单地调用父类也就是图形类绘制图形的方法即可绘制任何图形,这就是多态的思想,总结为一句话就是将父类的对象应用于子类的特征就是多态,多态类的实现并不依赖于具体类,而是依赖于抽象类(父类)和接口.

2 向上转型

2.1 为何叫向上转型

 在面向对象程序设计中,针对一些复杂的场景,我们通常画一个UML图来表示各个类之间的关系,通常父类画在子类的上方,因此我们就称之为"向上转型",表示往父类的方向转.
在这里插入图片描述
向上转型发生的时机:

  • 直接赋值;
  • 方法传参;
  • 方法返回.
    在这里插入图片描述

2.2 理解向上转型

我们先来看以下代码(以下代码为上文代码中的一部分):
在这里插入图片描述
猫咪本身是一种动物,cat是一个父类(Animal)的引用,指向的是一个子类(Cat)的实例,这种写法称之为向上转型.

3 动态绑定

 问题来了!!当子类和父类中出现同名方法的时候,再去调用会出现什么情况呢???
在这里插入图片描述
示例如下:

class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }
    public void eat(String food) {
        System.out.println("我是一只动物!");
        System.out.println(this.name + "正在吃" + food);
    }
}
class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    @Override
    public void eat(String food) {
        System.out.println("我是一只小鸟!");
        System.out.println(this.name + "正在吃" + food);
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Animal animal1 = new Animal("麻雀");
        animal1.eat("小米");
        Animal animal2 = new Bird("鹦鹉");
        animal2.eat("大米");
    }
}

运行结果:
在这里插入图片描述
解读:

  • animal1和animal2虽然都是Animal类型的引用,但是animal1指向Animal类型的实例,animal2指向Bird类型的实例;
  • 针对animal1和animal2分别调用eat方法,发现animal1.eat()实际调用了父类的方法,而animal2.eat()实际调用了子类的方法.

在Java中,调用某个类的方法,究竟执行了哪段代码(是执行的父类方法啊还是执行的子类方法啊),要看引用指向的是父类还是子类对象,这个过程是程序运行时决定的,而不是在编译的时候决定的,因此称之为动态绑定.

4 方法重写与重载

针对上文刚写的eat方法来说,子类实现父类的重名方法,并且参数的类型和个数完全相同,这种情况我们称之为重写/覆写/覆盖(Override).

4.1 注意事项

  • 普通方法可以重写,static修饰的静态方法不能重写;
  • 重写中子类的方法的访问权限不能低于父类的方法的访问权限;
  • 重写的方法返回值类型不一定和父类的方法相同(在这里我们建议写成相同);
  • 重写和重载不要混淆,这是两种不同的概念,下文中将进行区分解释.
    有关访问权限的示例如下:
    在这里插入图片描述

4.2 方法的重载

同一个方法名字,提供不同版本的实现,称之为方法重载.

重载规则:

  • 针对的是同一个类;
  • 方法名相同;
  • 方法的参数不同(参数个数或者参数类型);
  • 方法的返回类型不影响重载.
    示例如下:
public class TestDemo {
    public static int add (int x,int y) {
        return x + y;
    }
    public static double add(double x,double y) {
        return x + y;
    }
    public static double add(double x,double y,double z) {
        return x + y + z;
    }
    public static void main(String[] args) {
        int a1 = 10;
        int b1 = 20;
        int ret1 = add(a1,b1);
        System.out.println("ret1 = " + ret1 );

        double a2 = 10.5;
        double b2 = 20.2;
        double ret2 = add(a2,b2);
        System.out.println("ret2 = " + ret2);

        double a3 = 10.5;
        double b3 = 20.2;
        double c3 = 30.1;
        double ret3 = add(a3,b3,c3);
        System.out.println("ret3 = " + ret3);
    }
}

运行结果:
在这里插入图片描述
解读 : 方法的名字都叫add,但是有的add是计算int相加,有的是double相加;有的是两个数字相加,有的是三个数字相加;提供了同一个add方法名字,但是又不同的版本实现,这就是重载.

4.3 总结重载和重写的区别(*)

在这里插入图片描述

5 正式进入多态学习(*)

5.1 示例

 通过下面的代码来体会一下多态的写法:

class Shape {
    public void draw() {} //啥也不干
}
class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("画了一个○");
    }
}
class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("画了一个□");
    }
}
class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("画了一朵小红花❀");
    }
}
public class TestDemo {
    public static void drawMap(Shape shape) {
        shape.draw();
    }
    public static void main(String[] args) {
        Shape shape1 = new Cycle();
        Shape shape2 = new Rectangle ();
        Shape shape3 = new Flower();
        drawMap(shape1);
        drawMap(shape2);
        drawMap(shape3);
        /*shape1.draw();
        shape2.draw();
        shape3.draw();*/
    }
}

运行结果:
在这里插入图片描述
代码解读: 可以这样理解,Shape就相当于一支画笔,我们用它来画了圆/正方形/小红花;当类的调用者在编写drawMap这个方法的时候,参数类型为Shape(父类),此时在该方法内部是不知道的,也不关注当前的shape引用指向的是哪个类型/子类的实例,此时shape这个引用调用draw方法可能会有多种不同的表现(和shape对应的实例相关),这种行为就称之为多态.
核心思想: 一个引用,能表现出多种不同的形态;一个引用到底是指向父类对象还是某个子类对象(可能有多个),要根据代码来确定.

5.2 使用多态的好处

  • 类调用者对类的使用成本进一步降低;
    +封装让类的调用者不需要知道类的实现细节;
    +多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可;
    +可以理解为多态是封装的更进一步.
  • 能够降低代码的"圈复杂度",避免使用大量的if-else;
    +圈复杂度是一种描述一段代码复杂程度的方式,简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个个数就称之为圈复杂度,如果一段代码中有许多条件分支和循环语句,我们理解起来也比较复杂.
  • 可扩展能力更强.
    +对于类的调用者来说,只要创建一个新的类的实例就可以了,改动成本很低.

6 向下转型

6.1 背景概念

 由上文我们可以知道向上转型是子类对象转成父类对象,那么向下转型便是父类对象转成子类对象,也就是说子类对象总是父类的一个实例,但父类对象不一定是子类的实例,相比于向上转型,向下转型不是很常见.
 学习向下转型,我们还要知道另外一个概念:显示类型转换.

显示类型转换:越是具体的对象,具有的特性越多;越是抽象的对象,具有的特性越少.在做向下转型操作时,将特性范围小的对象转换为特性范围大的对象肯定会出现问题,所以这时候必须得告知编译器某些特性,将父类对象强制转换为某个子类对象,这便是显示类型转换.

6.2 问题示例

我们将上文中动态绑定中的部分代码稍作修改,如下所示:
在这里插入图片描述
代码解读: 编译过程中,animal类型是Animal,此时编译器只知道这个类中有一个eat方法,没有fly方法,虽然animal实际引用的是一个Bird对象,但是编译器是以animal的类型来查看有哪些方法的,针对

Animal animal = new Bird(“麻雀”)

这样的代码:

  • 编译器检查有哪些方法存在时,看的是Animal这个类型;
  • 执行时究竟执行的是父类的方法还是子类的方法,则看的是Bird这个类型.

在这里插入图片描述
如果我们要想实现fly()这个方法,该如何做呢???
方法一: 如果将animal进行强制类型转换可以么?? 我们来试一下:

public static void main(String[] args) {
        Animal animal = new Bird("麻雀");
        animal.eat("小米");
        Bird bird = (Bird) animal;
        bird.fly();
    }

运行结果:
在这里插入图片描述
虽然已经运行成功了,但是总觉得有些不靠谱,如下代码:

public static void main(String[] args) {
        Animal animal = new Cat("猫咪");
        Bird bird = (Bird)animal;
        bird.fly();
    }

运行结果:
在这里插入图片描述
因为animal本质上引用的是一个Cat对象,是不能转成Bird对象的,因此就抛出了异常,所以这样的向下转型有局限性,是行不通的.
方法二: 为了避免在向下转型时抛出异常,我们可以先判定一下animal本质上是不是一个Bird实例,然后再进行转换,代码如下:

public static void main(String[] args) {
        Animal animal = new Cat("猫咪");
        if (animal instanceof Bird) {
            Bird bird = (Bird)animal;
            bird.fly();
        }
    }

这时候就不会抛出异常!!

instanceof可以判定一个引用是否是某个类的实例,如果是返回true.
在Java语言中的关键字都是小写!

7 super关键字

7.1 背景信息

 上面的代码由于使用了重写机制,所以调用到的是子类的方法,那么如果要在子类内部调用父类方法怎么办? super关键字就是来解决这个问题的!

7.2 super关键字的使用

1)使用super来调用父类的构造器

class Cat extends demo6.Animal {
    public Cat(String name) {
        super(name);
    }
}

2)使用super来调用父类的普通方法
在这里插入图片描述
说明: 如果在子类的eat方法中直接调用eat,那么此时就认为是调用的自己的eat,也就是递归了.
注意点: 尽量不要在构造器中调用方法,如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没有构造完成,可能会出现一些隐藏的并且极难发现的问题.

7.3 super与this的区别

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到