今天是比较抽象的多态,希望能给大家带来帮助
多态
多态的实现原理
实现原理
先说好不钻牛角尖哈,多态 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
中就没有这个方法,就不能进行动态绑定了
那大家可以想一下强制类型转换到底是在干嘛
写这篇文章之前我是完全不知道多态是怎么实现的,我也是一边查资料一边研究,希望能帮助大家理解多态