【Java】弄清多态,看这一篇就够了|由浅入深,保姆级详解

在这里插入图片描述

  • 博主简介:努力学习的预备程序媛一枚~
  • 博主主页: @是瑶瑶子啦
  • 所属专栏: Java岛冒险记【从小白到大佬之路】

前言

在上篇【Java】还不理解继承?一篇文章看懂继承|继承入门,我们了解了继承的概念如何时两个类建立继承关系is-a、以及继承中的一些细节

但是,这只是庞大继承体系的一角。今天讲解在继承、封装基础上、方法重写的非常重要的一点—多态 polymorphic.

同时多态也是JAVA第三大重要特性。这点在开篇博客【Java基础篇】Java重要特性,JDK,JRE,JVM区别和联系,环境变量中已经讲到。

目录

  • 前言
  • Part1:基本介绍:
    • 1.1:多态的体现
    • 1.2:对象的多态
  • Part2:编译类型、运行类型
  • Part3:向上转型
    • 3.1:向上转型发生时机
      • 3.1.1:方法传参
      • 3.1.2:方法返回
    • 3.2:注意事项&细节:
  • Part4:动态绑定机制
    • 4.1:介绍
    • 4.2:动态绑定的意义
    • 4.3:注意事项
  • Part5:向下转型
    • 5.1:基本介绍
    • 5.2:使用细节&注意事项
    • 5.3:意义&使用场景
  • Part6:多态的优缺点
    • 6.1:使用多态的好处:
    • 6.2多态的缺陷
  • Part7:总结

Part1:基本介绍:

多态,其实就是指“一种”事物,可以有多种形态。比如之前讲到的方法重载,是多态(函数名相同,参数列表不同,但功能含义相同);还比如方法的重写(方法相同,但在父子类之中的实现不同);对象的多态,是指一个对象可以有不同的形态(类型)。那究竟什么是对象的多态呢?又是如何实现的。接下来我们来系统学习一下。

1.1:多态的体现

Java中的多态分为两个方面:

  • 方法的多态

    • 重载【Java】保姆级讲解|从0到1学会方法及方法重载 ( 入门,包懂)
    • 重写【Java】弄清方法重写,看这一篇就够了|由浅入深,保姆级讲解
  • 对象的多态

1.2:对象的多态

‍♀️ 对多态的简单理解:父类引用可以指向子类对象,且用该父类引用去调用子类重写过的实例方法时,不同的对象会产生不同的状态(动态绑定)。

Java中对象多态实现的条件(缺一不可)

  • 必须在继承体系下
  • 子类必须要对父类中方法进行重写(父类引用当然还是可以接收子类对象引用的,但这个时候其实体现不出来多态)
  • 通过父类引用调用重写方法

举例:

/**
 * Created with IntelliJ IDEA.
 * Description:
 * Author: yaoyao2024
 * Date: ${YEAR}-${MONTH}-${DAY}
 * Time: ${TIME}
 */
class Animal {
    String name;
    int age;

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

    public void eat() {
        System.out.println(this.name + "eating");
    }
}


class Cat extends Animal {
    String name;
    int age;

    public Cat(String name, int age) {
        super(name, age);
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(this.name + "吃小鱼干~");
    }
}

class Dog extends Animal {
    String name;
    int age;

    public Dog(String name, int age) {
        super(name, age);
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(this.name + "吃骨头~");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Cat("咪咪", 2);
        Animal animal2 = new Dog("旺旺", 3);
        animal1.eat();
        animal2.eat();
    }

}

【Java】弄清多态,看这一篇就够了|由浅入深,保姆级详解_第1张图片

可以看到,虽然animal1animal2是同属于Animal类型,但调用方法时,看似都是eat()方法,但实现效果不同,是调用实际类型(CatDog)各自的方法,这种一个类型对象,但是调用不同方法,呈现出不同行为,就是多态!!!

从这个例子我们也知道:当用父类引用指向子类对象时,调用重写方法,是调用子类中重写过的方法而不是父类当中的方法!

在内存中,父类引用和子类对象实体是如下关系:
【Java】弄清多态,看这一篇就够了|由浅入深,保姆级详解_第2张图片

Part2:编译类型、运行类型

再深入学习多态时,我们不得不先了解两个概念:编译类型、运行类型。这同时也是待会我们讲动态绑定的基础。

先看这句话:

//变量类型 变量名   对象
Animal animal01 = new Cat();
  • 等号左边:编译类型( Animal )
    • 解释:所谓编译类型,就是在编译时期确定的类型。通俗来说,就是编译器认为这个变量是什么类型。比如int a = 10就是告诉计算机,a这个变量的类型是int,这时在编译的时候编译器就确定好的。
    • 引用变量的类型在编译时确定(无可厚非,变量声明时都有类型,向计算机声明,这个变量是什么类型,这是在编译时即确定好了)
  • 等号右边:运行类型( Cat )
    • 为什么叫作运行类型呢?因为new 对象这条语句是在运行时期执行的,对象是什么类型,也是运行时期确定的。
    • 运行类型是对象的实际类型。即这个对象本质上是Cat,用Animal来表示,以提高代码通用性

如何理解呢?
变量是一个盒子,编译类型决定了这个盒子长什么样子(计算机如何去理解&看待),而实际类型决定了这个盒子里面放的是什么东西。

编译时描述了这个盒子的样子、类型,编译器只知道:这是一个放动物的盒子,所以你决定把猫咪放进去,它不报错 Animal animal = new Cat()(注意,这个时候,即程序没有运行之前,还没有把实际的猫咪放进去)

而运行时期,是真的创造了这只小猫咪,并且放进去。所以animal的实际类型/运行类型是:Cat

C++ 说: 由于编译时决定了 指针长度是父类,所以解析的时候就按照父类指针要求去解析

【补充】:instanceof–比较操作符,用于判断对象的运行类型是否为XX类型/XX类型的子类型:返回值是boolean类型

System.out.println(Cat instanceOf Animal);//true

Part3:向上转型

介绍
就是一种类型转换,相当于把子类对象实体的类型由子类类型,向上转换成父类类型。

为什么要叫向上呢?首先是继承的本质其实就是一种由子类,向上的逐级查找关系。其次,在绘制UML图,来表示类与类之间的关系时,我们都习惯将父类画在子类上方。所以我们就把这种类型转换称为“向上转型”,十分形象,表示:从子类向上,转换成父类。

写法:

父类类型 变量名 = new 子类类型();
eg:
Animal animal01 = new Cat();

理解
如何去理解这种这种向上转型呢?–is a

3.1:向上转型发生时机

向上转型发生的时机分为以下三种:

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

前文所讲的一直都是直接赋值,也很好理解。这里详细讲讲后面两种。

3.1.1:方法传参

public class Main {
    public static void main(String[] args) {
        Cat cat01 = new Cat();
        feed(cat01);
    }
    public static void feed(Animal animal) {
        animal.eat();
    }
}

其实传参的本质也是赋值:Animal animal = cat01;

3.1.2:方法返回

public class Main {
    public static void main(String[] args) {
        Animal animal = gain();
    }
    
    public static Animal gain(){
        return new Dog();
    }
}

返回类型是Animal,但返回对象的实际类型是Dog。在返回的时候进行了向上转型,把Dog类对象转换为父类Animal并返回

3.2:注意事项&细节:

父类引用指向子类对象时:(即发生向上转型时)

  • 该引用可以调用父类中的所有成员(但是必须遵守子类调用父类属性、方法的访问权限规则)
  • 不能调用子类的特有成员
    因为编译类型在编译时期就确实了,决定了计算机认为你这个引用类型就是父类的,用父类引用去调用子类的特有成员当然是错误的,编译时就会报错。
  • 该引用调用父类方法时,最终实现(运行效果),要先看子类是否重写

【Java】弄清多态,看这一篇就够了|由浅入深,保姆级详解_第3张图片

  • 该引用访问属性时,直接访问的是父类属性,不看子类。因为属性不可重写!

【总结】:调用方法看运行类型,访问属性看编译类型

Part4:动态绑定机制

在之前的文章中,我们讲了方法重写,也提到,方法重写和动态绑定的本质是一样的。只是动态绑定是方法重写的底层实现。

这里,既然编译类型和运行类型都已经讲了,我们现在来着重讲一下动态绑定:

注意!:动态绑定是方法重写的原理,是基于实例方法的,对于属性、静态方法、构造器不存在动态绑定这一说!

4.1:介绍

当对象引用调用实例方法时,该方法会和对象的内存地址/实际运行类型绑定

4.2:动态绑定的意义

方法重写和动态绑定的本质一样,所以动态绑定的意义其实也就是方法重写的意义。

这里结合多态再次说一下,希望大家能对多态&动态绑定有更好的理解。

那么为什么需要多态&动态绑定呢?

因为创建子类对象代码和操作子类对象代码通常情况下,并不是向我们举得例子那么简单,挨的那么近,而是经常不在一起。操作对象的代码往往不知道该对象的实际类型,往往只知道其父类类型。往往也只需要知道它是某种父类型即可
(再通俗来说:把确定对象实际运行类型,以及根据实际运行类型调用相应方法的工作交给了底层,JVM去帮我们完成)

public class Main {
    public static void main(String[] args) {
        Animal animal01 = new Cat();
        Animal animal02 = new Dog();
        Animal[] animals = {animal01, animal02};
        for (Animal animal : animals) {
            animal.eat();
        }
    }
}

一句话来说就是:方便统一管理、操作不同子类型对象,同时又能实现对象的特有行为–两全其美哉~

4.3:注意事项

当父类引用指向子类对象时,不可用父类引用去调用子类对象的特有方法,否则会报错

理解:因为声明是父类,编译器会去父类中找这个方法,如果找不到,则编译报错。

Part5:向下转型

5.1:基本介绍

学习了向上转型,向下转型其实也很好理解。

但是注意:向下转型是建立在向上转型基础上的。即,先有向上转型,才能有向下转型,不能直接把父类对象转成子类型!

//这是向上转型
父类类型 变量名 = new 子类类型();
Animal animal01 = new Cat();

//这是向下转型:
子类类型 变量名 (子类类型)父类引用
Cat cat01 = (Cat)animal;

5.2:使用细节&注意事项

  • 在强转父类引用之前,该父类引用必须指向子类型对象
  • 强转后,可以用该引用调用子类所有成员

5.3:意义&使用场景

向下转型其实是对向上转型的一种弥补,向上转型(基于继承、多态、动态绑定)后,有很多好处。但是我们也知道,无法通过父类引用访问子类特有属性和调用子类的特有方法。那么如何保证既可以统一管理子类型(降低耦合),在需要调用子类型特有属性时和方法时,可以调用到呢?

什么时候向下转型:

  • 需要获得运行类的属性

  • 需要调用运行类的特有方法

举例:(用到的类不变)

public class Main {
    public static void main(String[] args) {
        show(cat01)}

    public static void show(Animal animal) {
        if(animal instanceof Cat){
            ((Cat) animal).climb();
        }
    }
}

Tips:强转要合理,如果把实际运行类型是Cat的强转成Dog,势必要报错。所以出于安全性,在强转之前使用instanceof关键字来判断一下!

Part6:多态的优缺点

6.1:使用多态的好处:

  • 降低代码的“圈复杂度”,避免使用大量if-else

圈复杂度:描述代码复杂程度的方式。一段代码里面条件分支和循环语句越多,那么圈复杂度越高。
简单粗暴理解就是一段代码中条件语句和循环语句出现的个数就是“圈复杂度”,如果一个方法的圈复杂度太高,就需要考虑重构。
不同公司对圈复杂度都有各自的规范,一般不会超过10

这个当然好理解,用同一父类接收多个不同类型子类,不用判断类型,因为用父类引用调用重写方法是子类各自的重写过的方法!

  • 可扩展能力强

    当我们想要多加一个动物类型时,可以直接创建类和这个类的实例

6.2多态的缺陷

  • 属性没有多态性

    也就是说,当父类和子类有同名属性时,通过父类引用只能调用父类自己的属性
  • 在父类构造方法中调用重写方法有坑!

class Parent {
    public Parent() {
        func();
    }

    public void func() {
        System.out.println("parent.func()");
    }
}

class Child extends Parent {
    private final int num = 1;

    public void func() {
        System.out.println("child.func()" + num);
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
    }
}

结果为child.func():0,原因是创建子类时调用父类构造方法,而在父类构造方法中调用了子类重写的方法,此时触发动态绑定,调用子类的func,但是此时子类child还没有完成初始化,num为0!

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

Part7:总结

多态的核心就是让调用者不必关心对象的具体类型。降低使用成本,提高开发效率。

此篇文章讲的多态是建立在继承继承基础之上。其实文章开篇也有讲到,多态其实就是多种形态,是一个比较广泛的概念。在后面的接口抽象类中,还会用到。


在这里插入图片描述

  • Java岛冒险记【从小白到大佬之路】

  • LeetCode每日一题–进击大厂

  • Go语言核心编程

  • 算法

你可能感兴趣的:(java,python,开发语言)