- 博主简介:努力学习的预备程序媛一枚~
- 博主主页: @是瑶瑶子啦
- 所属专栏: Java岛冒险记【从小白到大佬之路】
在上篇【Java】还不理解继承?一篇文章看懂继承|继承入门,我们了解了继承的概念、如何时两个类建立继承关系is-a
、以及继承中的一些细节。
但是,这只是庞大继承体系的一角。今天讲解在继承、封装基础上、方法重写的非常重要的一点—多态 polymorphic.
同时多态也是JAVA第三大重要特性。这点在开篇博客【Java基础篇】Java重要特性,JDK,JRE,JVM区别和联系,环境变量中已经讲到。
多态,其实就是指“一种”事物,可以有多种形态。比如之前讲到的方法重载,是多态(函数名相同,参数列表不同,但功能含义相同);还比如方法的重写(方法相同,但在父子类之中的实现不同);对象的多态,是指一个对象可以有不同的形态(类型)。那究竟什么是对象的多态呢?又是如何实现的。接下来我们来系统学习一下。
Java中的多态分为两个方面:
方法的多态
对象的多态
♀️ 对多态的简单理解:父类引用可以指向子类对象,且用该父类引用去调用子类重写过的实例方法时,不同的对象会产生不同的状态(动态绑定)。
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
)各自的方法,这种一个类型对象,但是调用不同方法,呈现出不同行为,就是多态!!!
从这个例子我们也知道:当用父类引用指向子类对象时,调用重写方法,是调用子类中重写过的方法而不是父类当中的方法!
再深入学习多态时,我们不得不先了解两个概念:编译类型、运行类型。这同时也是待会我们讲动态绑定的基础。
先看这句话:
//变量类型 变量名 对象
Animal animal01 = new Cat();
int a = 10
就是告诉计算机,a这个变量的类型是int
,这时在编译的时候编译器就确定好的。new 对象
这条语句是在运行时期执行的,对象是什么类型,也是运行时期确定的。如何理解呢?
变量是一个盒子,编译类型决定了这个盒子长什么样子(计算机如何去理解&看待),而实际类型决定了这个盒子里面放的是什么东西。
编译时描述了这个盒子的样子、类型,编译器只知道:这是一个放动物的盒子,所以你决定把猫咪放进去,它不报错 Animal animal = new Cat()
(注意,这个时候,即程序没有运行之前,还没有把实际的猫咪放进去)
而运行时期,是真的创造了这只小猫咪,并且放进去。所以animal的实际类型/运行类型是:Cat
C++ 说: 由于编译时决定了 指针长度是父类,所以解析的时候就按照父类指针要求去解析
【补充】:instanceof–比较操作符,用于判断对象的运行类型是否为XX类型/XX类型的子类型:返回值是boolean
类型
System.out.println(Cat instanceOf Animal);//true
介绍:
就是一种类型转换,相当于把子类对象实体的类型由子类类型,向上转换成父类类型。
为什么要叫向上呢?首先是继承的本质其实就是一种由子类,向上的逐级查找关系。其次,在绘制UML图,来表示类与类之间的关系时,我们都习惯将父类画在子类上方。所以我们就把这种类型转换称为“向上转型”,十分形象,表示:从子类向上,转换成父类。
写法:
父类类型 变量名 = new 子类类型();
eg:
Animal animal01 = new Cat();
理解:
如何去理解这种这种向上转型呢?–is a
向上转型发生的时机分为以下三种:
前文所讲的一直都是直接赋值,也很好理解。这里详细讲讲后面两种。
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;
public class Main {
public static void main(String[] args) {
Animal animal = gain();
}
public static Animal gain(){
return new Dog();
}
}
返回类型是Animal,但返回对象的实际类型是Dog。在返回的时候进行了向上转型,把Dog类对象转换为父类Animal并返回
当父类引用指向子类对象时:(即发生向上转型时)
【总结】:调用方法看运行类型,访问属性看编译类型
在之前的文章中,我们讲了方法重写,也提到,方法重写和动态绑定的本质是一样的。只是动态绑定是方法重写的底层实现。
这里,既然编译类型和运行类型都已经讲了,我们现在来着重讲一下动态绑定:
注意!:动态绑定是方法重写的原理,是基于实例方法的,对于属性、静态方法、构造器不存在动态绑定这一说!
当对象引用调用实例方法时,该方法会和对象的内存地址/实际运行类型绑定
方法重写和动态绑定的本质一样,所以动态绑定的意义其实也就是方法重写的意义。
这里结合多态再次说一下,希望大家能对多态&动态绑定有更好的理解。
那么为什么需要多态&动态绑定呢?
因为创建子类对象代码和操作子类对象代码通常情况下,并不是向我们举得例子那么简单,挨的那么近,而是经常不在一起。操作对象的代码往往不知道该对象的实际类型,往往只知道其父类类型。往往也只需要知道它是某种父类型即可。
(再通俗来说:把确定对象实际运行类型,以及根据实际运行类型调用相应方法的工作交给了底层,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();
}
}
}
一句话来说就是:方便统一管理、操作不同子类型对象,同时又能实现对象的特有行为–两全其美哉~
当父类引用指向子类对象时,不可用父类引用去调用子类对象的特有方法,否则会报错
理解:因为声明是父类,编译器会去父类中找这个方法,如果找不到,则编译报错。
学习了向上转型,向下转型其实也很好理解。
但是注意:向下转型是建立在向上转型基础上的。即,先有向上转型,才能有向下转型,不能直接把父类对象转成子类型!
//这是向上转型
父类类型 变量名 = new 子类类型();
Animal animal01 = new Cat();
//这是向下转型:
子类类型 变量名 (子类类型)父类引用
Cat cat01 = (Cat)animal;
向下转型其实是对向上转型的一种弥补,向上转型(基于继承、多态、动态绑定)后,有很多好处。但是我们也知道,无法通过父类引用访问子类特有属性和调用子类的特有方法。那么如何保证既可以统一管理子类型(降低耦合),在需要调用子类型特有属性时和方法时,可以调用到呢?
什么时候向下转型:
需要获得运行类的属性
需要调用运行类的特有方法
举例:(用到的类不变)
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关键字来判断一下!
圈复杂度:描述代码复杂程度的方式。一段代码里面条件分支和循环语句越多,那么圈复杂度越高。
简单粗暴理解就是一段代码中条件语句和循环语句出现的个数就是“圈复杂度”,如果一个方法的圈复杂度太高,就需要考虑重构。
不同公司对圈复杂度都有各自的规范,一般不会超过10
这个当然好理解,用同一父类接收多个不同类型子类,不用判断类型,因为用父类引用调用重写方法是子类各自的重写过的方法!
当我们想要多加一个动物类型时,可以直接创建类和这个类的实例
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!
结论:“用尽量简单的方法使对象进入可工作状态”,尽量不要在构造器中调用方法。因为如果此时子类构造还没完成,就会触发动态绑定,这样可能会出现一些隐藏但是极难发现的问题!
多态的核心就是让调用者不必关心对象的具体类型。降低使用成本,提高开发效率。
此篇文章讲的多态是建立在继承继承基础之上。其实文章开篇也有讲到,多态其实就是多种形态,是一个比较广泛的概念。在后面的接口、抽象类中,还会用到。
Java岛冒险记【从小白到大佬之路】
LeetCode每日一题–进击大厂
Go语言核心编程
算法