今天是比较抽象的多态,希望能给大家带来帮助

主要内容

多态

为什么使用多态

多态的形式

多态的概念

多态的劣势

多态存在的必然条件

类型转换

多态的实现原理

多态的分类

运行时多态的形式

实现原理

常量池

方法调用方式

动态绑定实现多态

写在最后

多态

先说好不钻牛角尖哈,多态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

Descriptor: cp info #18

Name: cp info #28

Descriptor: cp info #10

里面的V表示返回值为空

CONSTANT_Methodref_info

保存方法的方法名称的索引和该方法所属的类名的索引,这个相当于中间表

Class_name: cp info #22

Name_and_type: cp info #23

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中就没有这个方法,就不能进行动态绑定了

那大家可以想一下强制类型转换到底是在干嘛

写在最后

写这篇文章之前我是完全不知道多态是怎么实现的,我也是一边查资料一边研究,希望能帮助大家理解多态

java 多态判断非空_重拾JavaSE基础——多态及其实现方式相关推荐

  1. java final类的写法_重拾JavaSE基础——抽象类、接口、代码块、final和枚举

    今天继续回顾Java基础,有些东西用得不多,大家看看知道语法就好 主要内容 抽象类 抽象方法 抽象方法的写法 抽象方法是否可以私有化 抽象类的特征 抽象类有无构造器,能否实例化对象 抽象类的结构 抽象 ...

  2. java 多态判断非空_收藏Java 面试题全梳理

    脚本之家 你与百万开发者在一起 来源 | Java建设者(ID:javajianshe) 作者 |cxuan 如若转载请联系原公众号 Java 基础篇 Java 有哪些特点 并发性的:你可以在其中执行 ...

  3. java 多态判断非空_跳槽涨薪季面试题之java基础(一)

    点击上方[全栈开发者社区]→右上角[...]→[设为星标⭐] 为迎接金九银十跳槽涨薪季,小编汇总了java精编版面试题,大概从java基础.java8特性.多线程.spring.springboot. ...

  4. java 多态判断非空_Java 面试知识点解析基础知识

    文本公众号来源: 我没有三颗心脏作者: 我没有三颗心脏 (一)Java 基础知识点 1)面向对象的特性有哪些? 答:封装.继承和多态(应要多算一个那就是抽象) 封装是指将对象的实现细节隐藏起来,然后通 ...

  5. java 对象验证非空_判断Bean对象指定字段非空

    判断Bean对象指定字段非空. 方案: 在bean对象上增加注解,指定字段非空,返回异常信息有明确字段描述,省去不必要的if.else判断. 新建注解类 /** * 参数校验,判断字段非空.返回异常文 ...

  6. java 多态判断非空_Java核心技术(四):继承

    本章目录: 一.类.超类和子类 1.多态 2.动态绑定 3.阻止继承:final类和方法 4.抽象类 5.访问修饰符总结 二.Object类:所有类的父类 1.equals方法 2.hashcode方 ...

  7. java 多态判断非空_Java多态性理解

    Java中多态性的实现 什么是多态 面向对象的三大特性:封装.继承.多态.从一定角度来看,封装和继承几乎都是为多态而准备的.这是我们最后一个概念,也是最重要的知识点. 多态的定义:指允许不同类的对象对 ...

  8. java 多态判断非空_Java 多态

    父类的属性变量(例如变量 int a)可以被继承,同时在子类中也会同时继承该变量(super.int a,继承的变量),子类中也可以再次声明一个同名(可以同类型)的变量(double a,自己声明的同 ...

  9. java优雅的非空判断

    字符串非空判断: lang3包下的StringUtils StringUtils.isNotBlack(str) StringUtils.isNotEmpty(str) isNotEmpty判断非空不 ...

最新文章

  1. 利用MingW检验程序运行内存
  2. Spring控制反转(依赖注入)的最简单说明
  3. Intel Realsense D435 在C/C++中表示的frame_set就是python中的frames?【wait_for_frames()】
  4. 删除online日志測试及ora-600 [4194]错误的处理
  5. 安全——《微服务设计》读书笔记
  6. nginx python webpy 配置安装
  7. 结合 Mist 在本地测试网络上实现代币智能合约
  8. 阅读众包文献中一些值得mark 的小收获
  9. 【报告分享】2019年中国智能门锁发展与应用白皮书.pdf
  10. GitHubPage博客搭建学习专栏
  11. android 广播 飞行模式,Android 开启飞行模式的几种方式
  12. Python大数据处理方案
  13. win10怎么修复网络连接服务器失败,微软发布修复补丁修复Win10无网络连接问题...
  14. 解决谷歌导入Vue开发工具没反应的问题
  15. 电脑主机组装总结——自己动手,丰衣足食
  16. 选用什么的域名后缀好
  17. 华为的服务器固态硬盘LE系列和VE系列,02311TJY 800GB SSD FusionServer RH5885 V3华为服务器硬盘...
  18. VC无进程木马下载器源码(利用IE隐藏进程)
  19. 常用分辨率、帧率、码率
  20. h3c 云服务器操作系统,产品技术-H3C CloudOS云操作系统电信版-新华三集团-H3C

热门文章

  1. 启动Storm的nimbus和supervisor时报错: ImportError: No module named argparse
  2. C语言 用泰勒展开公式计算sin(x)的值
  3. android11原生录屏,终于把安卓这项功能实现了!iOS 11录屏功能演示:超好用
  4. Windows系统下上架iOSAPP
  5. 中兴和华为的面试经历
  6. 【旧文集】一生伏首拜阳明-记于2017
  7. TCP/IP网络编程之多进程服务端(一)
  8. JBoot 全网上最好用的 API 文档工具
  9. pyqt5 PDM下载工具 Persepolis Download Manager 记录
  10. 【机器学习】完整的机器学习项目演练:第一部分