- 博主简介:努力学习的预备程序媛一枚~
- 博主主页: @是瑶瑶子啦
- 所属专栏: 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();
}
}
可以看到,虽然animal1
和animal2
是桶属于Animal
类型,但调用方法时,看似都是eat()
方法,但实现效果不同,是调用实际类型(Cat
和Dog
)各自的方法,这种一个类型对象,但是调用不同方法,呈现出不同行为,就是多态!!!
从这个例子我们也知道:当用父类引用指向子类对象时,调用重写方法,是调用子类中重写过的方法而不是父类当中的方法!
在内存中,父类引用和子类对象实体是如下关系:
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:注意事项&细节:
当父类引用指向子类对象时:(即发生向上转型时)
- 该引用可以调用父类中的所有成员(但是必须遵守子类调用父类属性、方法的访问权限规则)
- 不能调用子类的特有成员
因为编译类型在编译时期就确实了,决定了计算机认为你这个引用类型就是父类的,用父类引用去调用子类的特有成员当然是错误的,编译时就会报错。 - 该引用调用父类方法时,最终实现(运行效果),要先看子类是否重写
- 该引用访问属性时,直接访问的是父类属性,不看子类。因为属性不可重写!
【总结】:调用方法看运行类型,访问属性看编译类型
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语言核心编程
-
算法