今天是比较抽象的多态,希望能给大家带来帮助
主要内容
-
多态
- 为什么使用多态
- 多态的形式
- 多态的概念
- 多态的劣势
- 多态存在的必然条件
- 类型转换
-
多态的实现原理
- 多态的分类
- 运行时多态的形式
-
实现原理
- 常量池
- 方法调用方式
- 动态绑定实现多态
- 写在最后
多态
先说好不钻牛角尖哈,多态Java
的特性之一,先不着急说他的概念,先看看为什么要使用多态,多态给我们带来什么好处
为什么使用多态
举个例子吧,老奶奶喜欢养宠物,领养了一只加菲猫,加菲猫是只小动物,要吃饭,老奶奶每天负责喂它。Java
翻译过来就是下面这样子的
// 老奶奶
public class Granny {
public static void main(String[] args) {
// 领养一只加菲猫,这里简单的new出来了
Garfield garfield = new Garfield();
// 抱起加菲猫给它喂食
feed(garfield);
}
public static void feed(Garfield garfield) {
// 加菲猫吃东西
garfield.eat();
}
}
class Garfield extends Animal{
@Override
public void eat() {
System.out.println("加菲猫吃饱了");
}
}
abstract class Animal {
public abstract void eat();
}
一切都很顺畅。但是这时候老奶奶又去领养了一只牧羊犬,牧羊犬也是小动物,也要吃饭,老奶奶也要给他喂食,这时候代码要添加一个牧羊犬类,老奶奶要添加一个给牧羊犬喂食的方法
public class Granny {
public static void main(String[] args) {
// 领养一只加菲猫,这里简单的new出来了
Garfield garfield = new Garfield();
// 抱起加菲猫给它喂食
feed(garfield);
// 领养一只牧羊犬
Shepherd shepherd = new Shepherd();
// 老奶奶给他喂食
shepherd.eat();
}
public static void feed(Garfield garfield) {
// 加菲猫吃东西
garfield.eat();
}
public static void feed(Shepherd shepherd) {
// 加菲猫吃东西
shepherd.eat();
}
}
class Shepherd extends Animal{
@Override
public void eat() {
Systen.out.println("牧羊犬吃的很开心");
}
}
// 加菲猫
class Garfield extends Animal{
// ...
}
如果老奶奶还想继续领养小动物,老奶奶又要给这只小动物创建一个新的喂食的方法。聪明的我给老奶奶指了条明路,只要把feed
方法的参数范围扩大一点,不要指定是加菲猫还是牧羊犬z,只要是小动物都给他喂食,反正小动物都有吃的方法。
public class Granny {
public static void main(String[] args) {
// 领养一只加菲猫,这里简单的new出来了
Garfield garfield = new Garfield();
// 抱起加菲猫给它喂食
feed(garfield);
// 领养一只牧羊犬
Shepherd shepherd = new Shepherd();
// 老奶奶给他喂食
shepherd.eat();
}
// 扩大了范围
public static void feed(Animal animal) {
// 给动物喂食
animal.eat();
}
}
这样老奶奶就舒服了,所以多态的好处之一就是方便传参。
后来老奶奶发现自己家里的动物越来越多,受不了了决定只养一只其他的都卖了,于是老奶奶选择留下加菲猫又回到了最初的日子
public class Granny {
public static void main(String[] args) {
// 领养一只加菲猫,这里简单的new出来了
Garfield garfield = new Garfield();
// 抱起加菲猫给它喂食
feed(garfield);
}
public static void feed(Garfield garfield) {
// 加菲猫吃东西
garfield.eat();
}
}
但是养了一段时间老奶奶觉得加菲猫老在家躺着没什么意思,想念牧羊犬了,于是把加菲猫丢了换回牧羊犬,将原来Garfield garfield = new Garfield();
改为
Shepherd shepherd = new Shepherd();
又过了一段时间老奶奶觉得不行,牧羊犬吃得太多了开销顶不住,还是加菲猫好,于是他又把代码改回来了,又将Shepherd shepherd = new Shepherd();
改回
Garfield garfield = new Garfield();
我见老奶奶都一把年纪了,改来改去还挺麻烦的,就跟她说你要不定义一个Animal
类的annimal
变量代表你的宠物把,像这样
Animal animal = new Garfield();
这样换宠物只要改new
后面的就行了,老奶奶一听觉得很有道理,所以多态的另一个好处就是右边的对象可以组件化切换,业务功能也会随之改变
在我们开发中也常常使用多态,大家回忆一下一个Service
需要依赖其他Service
,是不是这样写的
@Resource
private IUserServiceImpl userService;
总结:多态的优势可以总结成两个点:方便入参和实现组件化切换:
多态的形式
-
子类继承父类
父类 变量名称 = new 子类构造器
-
实现类实现接口
接口 变量名称 = new 实现类构造器
多态的概念
看完上面的内容,会有一种感觉,多态的风格其实是定义变量的时候把类型范围扩大,如上面的例子,老奶奶以后都会把他的宠物们定义成这样
Animal garfield = new Garfield();
Animal shepherd = new Shepherd();
定义加菲猫和牧羊犬的时候声明的都是Animal
类型,但他们的eat
方法是不一样的。同一种类型的对象执行同一个行为(方法)会得到不同的结果,这个就是多态的概念
多态只是一种编程风格,没有要求一定要遵循,只是使用了多态会有他的好处,多态已经成为大家公认且遵守的Java特性,顺着趋势走就OK
多态的劣势
这里有个小插曲,为什么老奶奶一开始会放弃加菲猫选择牧羊犬,因为牧羊犬可以帮忙看家,这是他的独有功能
class Shepherd extends Animal{
private Integer i = 0;
@Override
public void eat() {
Systen.out.println("牧羊犬吃的很开心");
}
public void lookDDoor() {
Systen.out.println("这是牧羊犬的超能力");
}
}
但是他发现自从用了多态后,再也无法让牧羊犬去看门了
public class Granny {
public static void main(String[] args) {
// 领养一只牧羊犬
Animal shepherd = new Shepherd();
// 看门
shepherd.lookDoor(); // 报错
}
}
大家可以先认为Animal shepherd = new Shepherd();
进行了自动转型,shepherd
已经没有看家的方法了,所以多态的劣势就是子类失去了独有的行为,而且连成员变量都不能直接访问(只能借助重写的方法去访问)
public static void main(String[] args) {
// 领养一只加菲猫,这里简单的new出来了
Garfield garfield = new Garfield();
garfield.i;// 报错
}
这时候需要使用强制类型转换来解决问题,至于为什么不能调用子类的方法相信看完后面你就懂啦
多态存在的必然条件
- 必须存在继承关系
- 必须是父类/接口类型变量引用子类/实现类类型变量
- 必须存在重写方法
类型转换
大家可以先记住语法,回头就能理解转换到底是在干嘛了
-
自动转换
Animal garfield = new Garfield();
子类类型会自动转换成父类类型,其实就是多态的默认写法
-
强制类型转换
子类 新变量名称 = (子类) 需要转换的变量名称
如
Animal garfield = new Garfield(); // garfield = (Garfield)garfield 必须用新的引用接收 Garfield garfield2 = (Garfield)garfield;
注意:必须使用新的变量去接收
强制类型转换的时候需要对类型进行判断
在老奶奶养加菲猫和牧羊犬的时候有一个小插曲,加菲猫很贪吃,一顿要吃多点,老奶奶也没办法,只能给他加餐,但是使用了多态,喂猫喂狗的方法都是同一个
`feed`
,有没有办法可以判断一下入参到底是加菲猫还是牧羊犬呢,那肯定是有的
public static void feed(Animal animal) {
// 判断是不是加菲猫,是的话给他加餐
if (garfield instanceof Garfield) {
System.out.println("加餐");
animal.eat();
}
}
到底是加菲猫还是牧羊犬只有代码运行的时候才知道,intanceof
可以判断运行引用animal
的实际类型是否为Garfield
类
多态的实现原理
一个对象变量可以指向多种实际类型的现象成为多态,这导致一个对象变量调用同一个方法的时候得到了不同的结果。感觉非常抽象,看下面的例子
一只猫有两个个eat
方法,一个无参一个有参
class Cat {
public void eat() {
System.out.println("猫会吃饭")
}
public void eat(Integer weight) {
System.out.println("猫会吃饭,吃了" + weight)
}
}
当主函数运行以下代码的时候
Cat cat = new Cat();
cat.eat();
cat.eat(10)
回想刚刚的概念,是不是同一个变量cat
,调用同一个方法eat
,但结果是不一样。这就是编译时多态,在编译成class
文件的时候就可以确定,程序执行的eat
方法是Cat
类中的成员方法,而且根据形参也可以知道是哪个eat
方法,
方法签名和返回参数相同看作同一个方法。这种形式成为方法重载(Overload
)
再看下一种情况,猫类继承了动物类,重写了动物类的eat
方法
ublic class Animal {
public void eat() {
System.out.println("动物可以走路");
}
}
class Cat {
@Override
public void eat() {
System.out.println("猫会走路");
}
}
现在有一个feed
喂养的方法,需要传入一个动物类型
public void feed(Animal animal) {
animal.eat();
}
在编译的时候不能确定animal
到底是什么类型的,可能是加菲猫可能是牧羊犬,准确点应该是计算机不知道animal
实际是什么类型的,但程序员知道。这种就是我们最常用的多态,叫运行时多态,由于不确定传入的参数是什么类型的,同一个变量animal
调用同一个方法eat
产生的结果是不一样的
多态的分类
根据上面的例子,多态可以分为
- 编译时多态(静态多态)
- 运行时多态(动态多态)
后面所提到的多态都是运行时多态
运行时多态的形式
就是上面提到过的那两种
-
子类继承父类
父类 变量名称 = new 子类构造器
-
实现类实现接口
接口 变量名称 = new 实现类构造器
实现原理
尽量用通俗的话去解释,如果理解有误麻烦评论区告诉我
常量值
大家肯定听过,编译器把源代码编译成class
文件的时候,会把一些常量信息统一放在class文件的一块区域,大家可以用字节码分析工具
随便打开一个class文件就能看到c常量池了,这种写在文件里面的常量信息被称为静态常量池,当class
文件被加载到虚拟机的时候,会在方法区开辟一段空间存放这些常量信息,这个区域就叫做运行时常量池
常量池存了哪些信息
可以看下图,其实很像我们的数据库,
注意:因为!class
文件还没被加载,所以现在用分析工具展示的是静态常量池,里面包含一些符号引用(就是一个名字),加载到方法区后会替换成直接引用(内存地址)
-
CONSTANT_utf8_info
基本信息都存在
CONSTANT_utf8_info
,里面保存了这个类里面的成员方法的名字、我们定义的字符串常量(System.out.println(...)
里面的字),引用类型类名(如Cat
、Animal
),变量名(如cat
)等等Length of bytes array; 6 length of String: 6 String: Animal
-
CONSTANT_Class_info
保存对其他类的符号引用(
Class_name
)和在CONSTANT_utf8_info
的引用Class_name:cp info #25
-
CONSTANT_NameAndType_info
保存方法和字段的类型和名称,还有描述符信息(入参和返回值)
Name: cp info #15 <feed> Descriptor: cp info #18 <(LAnimal;)V>
Name: cp info #28 <eat> Descriptor: cp info #10 <()V>
<(LAnimal;)V>
里面的V
表示返回值为空 -
CONSTANT_Methodref_info
保存方法的方法名称的索引和该方法所属的类名的索引,这个相当于中间表
Class_name: cp info #22 <Animal> Name_and_type: cp info #23 <eat>
-
CONSTANT_interfaceMethod_info
和
CONSTANT_Methodref_info
类似,保存了接口方法的名称和类型的索引和接口的索引
所有的表最终信息都保存在CONSTANT_utf8_info
种,看上去就像我们的数据库表设计一样
方法调用方式
Java的方法调用方式有两种,静态调用和动态调用
-
静态调用
顾名思义,就是A类调用B类的静态成员方法,也就是说调用的时候很明确,我要调用方法区里面那个叫B类的那个静态方法,最后会把B类的静态方法的字节码地址替换运行时常量池对应的表符号引用,替换的过程称为静态绑定,调用绑定后的方法称为静态调用
StringUtils.isBlank();
类调用(
invokestatic
)在编译的时候计算机已经很明确要调那个方法了,只要类被加载到方法区,一切都顺利注意:Java中只有被
private
、static
、final
修饰 的方法属于静态 -
动态调用
如果要调用动态成员变量的方法就比较麻烦了,必须先去堆中找到对应的对象,然后根据对象的信息找到对应的方法的字节码地址,保存到堆中,对象中为什么会有方法的字节码地址呢,这是动态绑定完成的操作,具体后面再说,调用动态绑定后的方法被称为动态调用
cat.eat();
实例调用(
invokevirtual
)就需要等到对象被创建的时候才能指定调用哪个方法
JVM
调用方法的指令:
- 静态调用:
invokestatic
、`invokespecial
- 动态调用:
invokeinterface
、invokevirtual
实例化
这里需要说明的是,类如
Animal cat = new Cat();
这种形式对于cat
来说他是Animal
类型的,但在堆中开辟的是Cat
类的对象空间,并由this
指针指向Cat
实例,所以cat
的实际类型其实是Cat
类
动态绑定实现多态
子类继承父类
方法表是在方法区中有一个集合,专门存放方法名称和代码指针,代码指针指向存放方法体字节码的内存地址。这里需要强调的是,如果是子类重写了父类的方法或者实现类实现了接口的方法,指针是指向重写的方法的
如下面的代码
public class Main {
public static void main(String[] args) {
Animal cat = new Cat();
cat.run();
}
}
class Animal {
public void play() {
System.out.println("父类方法");
}
public void run() {
System.out.println("父类方法");
}
public void eat() {
System.out.println("父类方法");
}
}
class Cat extends Animal {
@Override
public void run() {
System.out.println("子类方法");
}
}
对于Animal
和Cat
类,方法表是这样的
当调用Cat
的run
方法的时候,字节码为invokevirtual #15
,JVM
先在常量池查CONSTANT_Methodref_info
-> CONSTANT_NameAndType_info
-> CONSTANT_utf8_info
,查出来现在需要调用的是Animal
类中run
方法,然后去Animal
的方法表里面找run
方法,记录以下偏移量offset
,再调用invoke this,offset
,这时候的this
指针正指向的是堆中的Cat
对象,Cat
也有一张方法表,恰好数下来offset
就是子类的run
方法,于是找到Cat
类的run
方法的字节码地址,顺利调用。所以动态调用的核心就在于这个方法表和this
指针的设计
实现类实现接口
接口可以多继承的,大家看下面的例子会发现用偏移量无法实现动态调用
interface A {
public void a1();
public void a2();
public void a3();
}
interface B {
public void b1();
}
class TestA implements A{
// 重写三个方法
}
class TestAB implements A, B {
// 从写四个方法
}
public class Main {
B testAB = new TestAB();
testAB.b1();
}
很明显接口B
的b1
方法的偏移量和实现类TestAB
不一样,所以JVM
提供了invokeinterface
方法,它不再使用偏移量,而是使用搜索的方式寻找合适的方法,所以调用接口的方法会比调用子类的慢
为什么不能调用子类中非重写的方法
因为在父类的方法表压根就没与那个方法,例如上面的例子,如果run
是Cat
独有的方法,在父类Animal
中就没有这个方法,就不能进行动态绑定了
那大家可以想一下强制类型转换到底是在干嘛
写在最后
写这篇文章之前我是完全不知道多态是怎么实现的,我也是一边查资料一边研究,希望能帮助大家理解多态
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。