Java学习苦旅(十三)——多态

本篇博客将详细讲解Java中的多态。

文章目录

  • 多态
    • 向上转型
    • 动态绑定
    • 向下转型
    • 理解多态
    • 多态的优势
    • 总结多态
  • 抽象类
    • 语法规则
    • 抽象类的作用
  • 接口
    • 语法规则
    • 实现多个接口
    • 常用接口
      • Comparable
      • Comparator
      • Cloneable
  • 结尾

多态

向上转型

在理解多态之前,我们先来看看什么叫向上转型。

看看这段代码:

class Animal {
    public String name;
    public int age;
    
    public Animal(String name) {
        this.name = name;
    }
}

class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    
    public void fly() {
        System.out.println("飞");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Bird bird = new Bird("圆圆");
        Animal bird2 = bird;
    }
}

main函数内部的代码其实可以写出这样:

Animal bird2 = new Bird("圆圆");

此时 bird2 是一个父类 (Animal)的引用,指向一个子类(Bird)的实例。这种写法称为 向上转型 .

向上转型一般在三种情况下发生:

  • 直接赋值
  • 方法传参
  • 方法返回

直接赋值在上文已经提到了,下面展示一个方法传参和方法返回。

方法传参:

public class Test { 
    public static void main(String[] args) {
        Bird bird = new Bird("圆圆");
        feed(bird);
    }
    
    public static void feed(Animal animal) {
        animal.fly();
    }
}

方法返回:

public class Test {
    public static void main(String[] args) {
        Ainmal animal = findMyAnimal();
    }
    
    public static Animal findMyAnimal() {
        Bird bird = new Bird("圆圆");
        return bird;
    }
}

动态绑定

动态绑定是指父类引用引用了子类的对象,通过这个父类引用调用父类和子类同名的覆盖方法。

同名的覆盖方法也称为 重写

重写:

1、方法名相同。

2、参数列表相同。

3、返回值相同(返回值可以是协变类型)。

4、在父子类情况下使用。

我们来看这段代码:

class Animal {
    public String name;
    public int age;

    public void eat() {
        System.out.println("我是一只小动物");
        System.out.println(this.name + "吃");
    }

    public Animal(String name) {
        this.name = name;
    }
}

class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println("我是一只鸟");
        System.out.println(name + "吃");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Animal animal = new Bird("圆圆");
        animal.eat();
    }
}

这段代码运行的结果为:

Java学习苦旅(十三)——多态_第1张图片

这段代码并没有运行Animal中的eat()方法,而是运行了Bird中的eat()方法,这就是动态绑定。

动态绑定的注意事项:

  1. 方法不可以是static。
  2. 子类的访问修饰限定,要大于等于父类的访问修饰限定。
  3. private方法不能重写。
  4. 被final修饰的方法不能重写。

此外,还需知道一点: 通过父类引用,只能访问父类自己的成员

向下转型

向上转型是子类对象转成父类对象,向下转型就是父类对象转成子类对象。相比于向上转型来说,向下转型没那么常见,但是也有一定的用途。

例如:

class Animal {
    public String name;
    public int age;

    public void eat() {
        System.out.println("我是一只小动物");
        System.out.println(this.name + "吃");
    }

    public Animal(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    public String color;
    
    public Dog(String name) {
        super(name);
    }
}

class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }

    public void fly() {
        System.out.println(name+"飞");
    }

    @Override
    public void eat() {
        System.out.println("我是一只鸟");
        System.out.println(name + "吃");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Animal animal = new Bird("圆圆");
        Bird bird = (Bird) animal;
        bird.fly();
    }
}

运行结果为:

Java学习苦旅(十三)——多态_第2张图片

这就是向下转型。

但是如果main函数中代码写出这样:

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

结果为:

Java学习苦旅(十三)——多态_第3张图片

因为animal本身是个Dog对象,无法转成Bird对象。

所以为了让向下转型更安全,我们可以这样写代码:

Animal animal = new Bird("圆圆");
if (animal instanceof Bird) {
    Bird bird = (Bird) animal;
    bird.fly();
}

instanceof 可以判定一个引用是否是某个类的实例。如果是,则返回 true。

理解多态

我们可以先看看这段代码:

class Shape {
    public void draw() {
        System.out.println("draw");
    }
}

class Rect 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) {
        Rect rect = new Rect();
        drawMap(rect);
        Flower flower = new Flower();
        drawMap(flower);
    }
}

运行结果为:

Java学习苦旅(十三)——多态_第4张图片

在这个代码中,TestDemo上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的。

当类的调用者在编写 drawMap 这个方法的时候,参数类型为 Shape (父类),此时在该方法内部并 不知道也不关注 当前的 shape 引用指向的是哪个类型(哪个子类)的实例。此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关),这种行为就称为 多态

多态的优势

1、类调用者对类的使用成本进一步降低。

  • 封装是让类的调用者不需要知道类的实现细节。

  • 多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可。

因此,多态可以理解成是封装的更进一步,让类调用者对类的使用成本进一步降低。

2、能够降低代码的 “圈复杂度”,避免使用大量的if - else。

例如这段代码:

public static void drawShapes() { 
     // 我们创建了一个 Shape 对象的数组. 
     Shape[] shapes = {new Rect(), new Rect(), new Flower()}; 
     for (Shape shape : shapes) { 
         shape.draw(); 
     } 
}

如果没有多态,那么代码只能这样写:

public static void drawShapes() { 
     Rect rect = new Rect(); 
     Flower flower = new Flower(); 
     String[] shapes = {"rect", "rect", "flower"}; 

     for (String shape : shapes) { 
         if (shape.equals("rect")) { 
             cycle.draw(); 
         }else (shape.equals("flower")) { 
             flower.draw(); 
         } 
     } 
}

而此时,代码量就大大增加了。

什么叫 “圈复杂度” ?

圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙,那么就比较简单容易理解。而如果有很多的条件分支或者循环语句,就认为理解起来更复杂。

因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个个数就称为 “圈复杂度”。如果一个方法的圈复杂度太高,就需要考虑重构。

3、可扩展能力更强。

如果要新增一种新的形状,对于类的调用者来说(drawShapes方法),只要创建一个新类的实例就可以了,改动成本很低。而对于不用多态的情况,就要把 drawShapes 中的 if - else 进行一定的修改,改动成本更高。

总结多态

多态是面向对象程序设计中比较难理解的部分。我们会在后面的抽象类和接口中进一步体会多态的使用,重点是多态带来的编码上的好处。另一方面,如果抛开 Java,多态其实是一个更广泛的概念,和 “继承” 这样的语法并没有必然的联系。

  • C++ 中的 “动态多态” 和 Java 的多态类似,但是 C++ 还有一种 “静态多态”(模板),就和继承体系没有关系了。

  • Python 中的多态体现的是 “鸭子类型”,也和继承体系没有关系。

  • Go 语言中没有 “继承” 这样的概念,同样也能表示多态。

无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型 ,这是降低用户使用成本的一种重要方式。

抽象类

语法规则

在刚才的打印例子中,我们发现父类Shape中的draw()方法没有什么实际的意义, 主要的绘制图形都是由Shape 的各种子类的draw()方法来完成的。像这种没有实际工作的方法,我们可以把它设计成一个 抽象方法 ,包含抽象方法的类称为 抽象类 。例如:

abstract class Shape {
    public abstract void draw();
}
  • 在 draw 方法前加上 abstract 关键字,表示这是一个抽象方法。同时抽象方法没有方法体,也就是没有 { },不能执行具体代码。

  • 对于包含抽象方法的类,必须加上 abstract 关键字表示这是一个抽象类。

注意事项:

1、抽象类是不可以被实例化的。

2、抽象类当中也可以包含和普通类一样的成员和方法。

3、抽象类最大的作用,就是为了被继承。

4、一个普通类继承了一个抽象类,那么这个普通类当中,需要重写这个抽象类的所有的抽象方法。

5、一个抽象类A继承了一个抽象类B,那么这个抽象类A可以不实现抽象父类B的抽象方法。

6、结合第5点,当A类再次被一个普通类继承后,那么A和B这两个抽象类当中的抽象方法必须被重写。

7、抽象类不能被final修饰,抽象方法也不可以被final修饰。

抽象类的作用

抽象类存在的最大意义就是为了被继承。

抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法。使用抽象类相当于多了一重编译器的校验。

使用抽象类的场景就如上面的代码,实际工作不应该由父类完成,而应由子类完成。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题。

很多语法存在的意义都是为了 “预防出错”,例如我们曾经用过的 final也是类似。创建的变量用户不去修改,不就相当于常量嘛?但是加上final能够在不小心误修改的时候,让编译器及时提醒我们。充分利用编译器的校验,在实际开发中是非常有意义的。

接口

接口是抽象类的更进一步。抽象类中还可以包含非抽象方法和字段。而接口中包含的方法都是抽象方法,字段只能包含静态常量在刚才的打印图形的示例中,我们的父类 Shape 并没有包含别的非抽象方法,也可以设计成一个接口。

语法规则

interface IShape {
    void draw();
}

class Cycle implements IShape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        IShape shape = new Cycle();
        shape.draw(); 
    }
}

注意事项:

1、接口是使用interface来修饰

2、接口中的普通方法不能有具体的实现。如果需要实现,只能通过关键字default来修饰这个方法。

3、接口中可以有static的方法。

4、接口里的所有方法都是public。

5、接口中的抽象方法默认是public abstract。

6、接口是不可以被通过关键字new来实例化。

7、类和接口之间的关系是通过implements实现的。

8、当一个类实现了一个接口,就必须要重写接口当中的抽象方法。

9、接口中的成员变量,默认是public static final修饰的。

10、当一个类实现一个接口之后,重写这个方法的时候,这个方法前面必须加上public。

11、一个类可以通过关键字extends继承一个抽象类或者普通类,但是只能继承一个类。同时,也可以通过implements实现多个接口,接口之间使用逗号隔开即可。先extends再implements。

12、接口与接口之间可以使用extends来操作它们的关系。

实现多个接口

有的时候我们需要让一个类同时继承自多个父类,这件事情在有些编程语言通过 多继承 的方式来实现的。然而 Java 中只支持单继承,一个类只能 extends 一个父类。但是可以同时实现多个接口,也能达到多继承类似的效果。例如:

class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }
}

interface IFlying {
    void fly();
}

interface IRunning {
    void run();
}

interface ISwimming {
    void swim();
}

class Cat extends Animal implements IRunning {
    public Cat(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(this.name + "跑");
    }
}

class Duck extends Animal implements IRunning, ISwimming, IFlying {
    public Duck(String name) {
        super(name);
    }

    @Override
    public void fly() {
        System.out.println(this.name + "飞");
    }
    @Override
    public void run() {
        System.out.println(this.name + "跑");
    }
    @Override
    public void swim() {
        System.out.println(this.name + "游");
    }
}

public class TestDemo {

    public static void run(IRunning running) {
        running.run();
    }

    public static void main(String[] args) {
        Cat cat = new Cat("猫");
        run(cat);
        Duck duck = new Duck("鸭子");
        run(duck);
    }
}

运行结果为:

Java学习苦旅(十三)——多态_第5张图片

常用接口

Comparable

我们先来看这样一段代码:

class Student {
    public int age;
    public String name;
    public double score;

    public Student(int age, String name, double score) {
        this.age = age;
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", score=" + score +
                '}';
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(18,"张三",100.0);
        students[1] = new Student(19,"李四",75.5);
        students[2] = new Student(20,"王五",60.0);
        System.out.println(Arrays.toString(students));
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}

如果直接运行的话,那么就会报错:

Java学习苦旅(十三)——多态_第6张图片

出现错误的原因就是我们并没有指定如何对学生进行排序。那么假如我们按照年龄,对学生从小到大排序,具体代码如下:

class Student implements Comparable<Student> {
    public int age;
    public String name;
    public double score;

    public Student(int age, String name, double score) {
        this.age = age;
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", score=" + score +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        return this.age - o.age;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(18,"张三",100.0);
        students[1] = new Student(19,"李四",75.5);
        students[2] = new Student(20,"王五",60.0);
        System.out.println("排序前:" + Arrays.toString(students));
        Arrays.sort(students);
        System.out.println("排序后:" + Arrays.toString(students));
    }
}

在这里就使用到了Comparable接口,运行结果为:

Java学习苦旅(十三)——多态_第7张图片

如果想从大到小排序的话,那么只需把Student里的compareTo修改一下:

public int compareTo(Student o) {
    return o.age - this.age;
}

运行结果为:

Java学习苦旅(十三)——多态_第8张图片

虽然Comparable接口可以对数据进行排序,但是它有一个缺点: 对类的侵入性非常强。一旦写好了,不能轻易改动

Comparator

Comparator接口便可以解决Comparable接口的缺点,因为可以用Comparator实现比较器。比如根据年龄比较:

class Student {
    public int age;
    public String name;
    public double score;

    public Student(int age, String name, double score) {
        this.age = age;
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", score=" + score +
                '}';
    }
}

class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(18,"张三",100.0);
        students[1] = new Student(19,"李四",75.5);
        students[2] = new Student(20,"王五",60.0);
        System.out.println("排序前:" + Arrays.toString(students));
        AgeComparator ageComparator = new AgeComparator();
        Arrays.sort(students,ageComparator);
        System.out.println("排序后:" + Arrays.toString(students));
    }
}

具体结果为:

Java学习苦旅(十三)——多态_第9张图片

如果想根据分数比较,那么只需要创建一个分数比较器即可,例如:

class ScoreComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return (int)(o1.score - o2.score);
    }
}

然后调用即可:

public class TestDemo {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(18,"张三",100.0);
        students[1] = new Student(19,"李四",75.5);
        students[2] = new Student(20,"王五",60.0);
        System.out.println("排序前:" + Arrays.toString(students));
        ScoreComparator scoreComparator = new ScoreComparator();
        Arrays.sort(students,scoreComparator);
        System.out.println("排序后:" + Arrays.toString(students));
    }
}

运行结果为:

Java学习苦旅(十三)——多态_第10张图片

如果想根据姓名比较,那么:

class NameComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(18,"zhangsan",100.0);
        students[1] = new Student(19,"lisi",75.5);
        students[2] = new Student(20,"wangwu",60.0);
        System.out.println("排序前:" + Arrays.toString(students));
        NameComparator nameComparator = new NameComparator();
        Arrays.sort(students,nameComparator);
        System.out.println("排序后:" + Arrays.toString(students));
    }
}

运行结果为:

image-20220129223245975

Cloneable

我们先来看一下java中是怎样定义Cloneable这个接口的:

Java学习苦旅(十三)——多态_第11张图片

我们可以发现,这个接口是个空接口,那么它为什么是个空接口呢?

空接口又称标志接口,代表当前这个类是可以被克隆的。

我们可以先来看这样一段代码:

class Person implements Cloneable {
    public int age;

    public void eat() {
        System.out.println("吃!");
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class TestDemo {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person();
        Person person2 = (Person)person.clone();
        System.out.println(person2);
    }
}

而这段代码在内存中开辟的空间可以这样表示:

Java学习苦旅(十三)——多态_第12张图片

这样也就实现了对象的克隆,也可以说创建了一个对象。

结尾

本篇博客到此结束。
上一篇博客:Java学习苦旅(十二)——继承
下一篇博客预告:Java学习苦旅(十四)——String

你可能感兴趣的:(Java学习苦旅,java,开发语言,后端)