当前位置: 首页 > news >正文

方法的重载与重写_如何从jvm角度看懂类初始化、方法重载、重写

类初始化

在讲类的初始化之前,我们先来大概了解一下类的声明周期。

类的声明周期可以分为7个阶段,但今天我们只讲初始化阶段。

我们我觉得出来使用和卸载阶段外,初始化阶段是最贴近我们平时学的,也是笔试做题过程中最容易遇到的,假如你想了解每一个阶段的话,可以看看深入理解Java虚拟机这本书。

下面开始讲解初始化过程。

注意:

这里需要指出的是,在执行类的初始化之前,其实在准备阶段就已经为类变量分配过内存,并且也已经设置过类变量的初始值了。

例如像整数的初始值是0,对象的初始值是null之类的。基本数据类型的初始值如下:

6590956a466e8c1bc55095035ced2ddf.png

大家先想一个问题,当我们在运行一个java程序时,每个类都会被初始化吗?

假如并非每个类都会执行初始化过程,那什么时候一个类会执行初始化过程呢?

答案是并非每个类都会执行初始化过程,你想啊,如果这个类根本就不用用到,那初始化它干嘛,占用空间。

至于何时执行初始化过程,虚拟机规范则是严格规定了有且只有 5中情况会马上对类进行初始化。

1. 当使用new这个关键字实例化对象、读取或者设置一个类的静态字段,以及调用一个类的静态方法时会触发类的初始化(注意,被final修饰的静态字段除外)。2. 使用java.lang.reflect包的方法对类进行反射调用时,如果这个类还没有进行过初始化,则会触发该类的初始化。3. 当初始化一个类时,如果其父类还没有进行过初始化,则会先触发其父类。4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。5. 当使用JDK 1.7的动态语言支持时,如果一个…..(省略,说了也看不懂,哈哈)。

注意是有且只有。这5种行为我们称为对一个类的主动引用。

初始化过程

类的初始化过程都干了些什么呢?

在类的初始化过程中,说白了就是执行了一个类构造器()方法过程。

注意,这个clinit并非类的构造函数(init())。

至于clinit()方法都包含了哪些内容?

实际上,clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序则是由语句在源文件中出现的顺序来决定的。

并且静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。如下面的程序。

public class Test1 {    static {        t = 10;//编译可以正常通过        System.out.println(t);//提示illegal forward reference错误    }    static int t = 0;}

给大家抛个练习.

public class Father {    public static int t1 = 10;    static {        t1 = 20;    }}class Son extends Father{    public static int t2 = t1;}//测试调用class Test2{    public static void main(String[] args){        System.out.println(Son.t2);    }}

输出结果是什么呢?

答案是20。我相信大家都知道为啥。因为会先初始化父类啊。

不过这里需要注意的是,对于类来说,执行该类的clinit()方法时,会先执行父类的clinit()方法,但对于接口来说,执行接口的clinit()方法并不会执行父接口的clinit()方法。

只有当用到父类接口中定义的变量时,才会执行父接口的clinit()方法。

被动引用

上面说了类初始化的五种情况,我们称之为称之为主动引用。

居然存在主动,也意味着存在所谓的被动引用。这里需要提出的是,被动引用并不会触发类的初始化。

下面,我们举例几个被动引用的例子:

1. 通过子类引用父类的静态字段,不会触发子类的初始化

/** * 1.通过子类引用父类的静态字段,不会触发子类的初始化 */public class FatherClass {    //静态块    static {        System.out.println("FatherClass init");    }    public static int value = 10;}class SonClass extends FatherClass {    static {        System.out.println("SonClass init");    }} class Test3{    public static void main(String[] args){        System.out.println(SonClass.value);    }}

输出结果.

FatherClass init

说明并没有触发子类的初始化

2. 通过数组定义来引用类,不会触发此类的初始化。

 class Test3{    public static void main(String[] args){        SonClass[] sonClass = new SonClass[10];//引用上面的SonClass类。    }       }

输出结果是啥也没输出。

3. 引用其他类的常量并不会触发那个类的初始化.

public class FatherClass {    //静态块    static {        System.out.println("FatherClass init");    }    public static final String value = "hello";//常量}class Test3{    public static void main(String[] args){        System.out.println(FatherClass.value);    }}

输出结果:hello

实际上,之所以没有输出”FatherClass init”,是因为在编译阶段就已经对这个常量进行了一些优化处理.

例如,由于Test3这个类用到了这个常量”hello”,在编译阶段就已经将”hello”这个常量储存到了Test3类的常量池中了.

以后对FatherClass.value的引用实际上都被转化为Test3类对自身常量池的引用了。也就是说,在编译成class文件之后,两个class已经没啥毛关系了。

重载

对于重载,我想学过java的都懂,但是今天我们中虚拟机的角度来看看重载是怎么回事。

首先我们先来看一段代码:

//定义几个类public abstract class Animal {}class Dog extends Animal{}class Lion extends Animal{}class Test4{    public void run(Animal animal){        System.out.println("动物跑啊跑");    }    public void run(Dog dog){        System.out.println("小狗跑啊跑");    }    public void run(Lion lion){        System.out.println("狮子跑啊跑");    }    //测试    public static void main(String[] args){        Animal dog = new Dog();        Animal lion = new Lion();;        Test4 test4 = new Test4();        test4.run(dog);        test4.run(lion);    }}

运行结果:

动物跑啊跑动物跑啊跑

相信大家学过重载的都能猜到是这个结果。但是,为什么会选择这个方法进行重载呢?

虚拟机是如何选择的呢?

在此之前我们先来了解两个概念。

先来看一行代码:

Animal dog = new Dog();

对于这一行代码,我们把Animal称之为变量dog的静态类型,而后面的Dog称为变量dog的实际类型。

所谓静态类型也就是说,在代码的编译期就可以判断出来了,也就是说在编译期就可以判断dog的静态类型是啥了。但在编译期无法知道变量dog的实际类型是什么。

现在我们再来看看虚拟机是根据什么来重载选择哪个方法的。

对于静态类型相同,但实际类型不同的变量,虚拟机在重载的时候是根据参数的静态类型而不是实际类型作为判断选择的。并且静态类型在编译器就是已知的了,这也代表在编译阶段,就已经决定好了选择哪一个重载方法。

由于dog和lion的静态类型都是Animal,所以选择了run(Animal animal)这个方法。

不过需要注意的是,有时候是可以有多个重载版本的,也就是说,重载版本并非是唯一的。

我们不妨来看下面的代码。

public class Test {    public static void sayHello(Object arg){        System.out.println("hello Object");    }    public static void sayHello(int arg){        System.out.println("hello int");    }    public static void sayHello(long arg){        System.out.println("hello long");    }    public static void sayHello(Character arg){        System.out.println("hello Character");    }    public static void sayHello(char arg){        System.out.println("hello char");    }    public static void sayHello(char... arg){        System.out.println("hello char...");    }    public static void sayHello(Serializable arg){        System.out.println("hello Serializable");    }    //测试    public static void main(String[] args){        char a = 'a';        sayHello('a');    }}

运行下代码。

相信大家都知道输出结果是.

hello char

因为a的静态类型是char,随意会匹配到sayHello(char arg);

但是,如果我们把sayHello(char arg)这个方法注释掉,再运行下。

结果输出:

hello int

实际上这个时候由于方法中并没有静态类型为char的方法,它就会自动进行类型转换。

‘a’除了可以是字符,还可以代表数字97。因此会选择int类型的进行重载。

我们继续注释掉sayHello(int arg)这个方法。结果会输出:

hello long。

这个时候’a’进行两次类型转换,即 ‘a’ -> 97 -> 97L。所以匹配到了sayHell(long arg)方法。

实际上,’a’会按照char ->int -> long -> float ->double的顺序来转换。

但并不会转换成byte或者short,因为从char到byte或者short的转换是不安全的。(为什么不安全?留给你思考下)

继续注释掉long类型的方法。输出结果是:

hello Character

这时发生了一次自动装箱,’a’被封装为Character类型。

继续注释掉Character类型的方法。输出

hello Serializable

为什么?

一个字符或者数字与序列化有什么关系?实际上,这是因为Serializable是Character类实现的一个接口,当自动装箱之后发现找不到装箱类,但是找到了装箱类实现了的接口类型,所以再一次发生了自动转型。

我们继续注释掉Serialiable,这个时候的输出结果是:

hello Object

这时是’a’装箱后转型为父类了,如果有多个父类,那将从继承关系中从下往上开始搜索,即越接近上层的优先级越低。

继续注释掉Object方法,这时候输出:

hello char…

这个时候’a’被转换为了一个数组元素。

从上面的例子中,我们可以看出,元素的静态类型并非就是一定是固定的,它在编译期根根据优先级原则来进行转换。其实这也是java语言实现重载的本质

重写

我们先来看一段代码.

//定义几个类public abstract class Animal {    public abstract void run();}class Dog extends Animal{    @Override    public void run() {        System.out.println("小狗跑啊跑");    }}class Lion extends Animal{    @Override    public void run() {        System.out.println("狮子跑啊跑");    }}class Test4{    //测试    public static void main(String[] args){        Animal dog = new Dog();        Animal lion = new Lion();;        dog.run();        lion.run();    }}

运行结果:

小狗跑啊跑
狮子跑啊跑

我相信大家对这个结果是毫无疑问的。他们的静态类型是一样的,虚拟机是怎么知道要执行哪个方法呢?

显然,虚拟机是根据实际类型来执行方法的。我们来看看main()方法中的一部分字节码.

//声明:我只是挑出了一部分关键的字节码public static void (java.lang.String[]);    Code:    Stack=2, Locals=3, Args_size=1;//可以不用管这个    //下面的是关键    0:new #16;//即new Dog    3: dup    4: invokespecial #18; //调用初始化方法    7: astore_1    8: new #19 ;即new Lion    11: dup    12: invokespecial #21;//调用初始化方法    15: astore_2    16: aload_1; 压入栈顶    17: invokevirtual #22;//调用run()方法    20: aload_2 ;压入栈顶    21: invokevirtual #22;//调用run()方法    24: return

解释一下这段字节码:

0-15行的作用是创建Dog和Lion对象的内存空间,调用Dog,Lion类型的实例构造器。

对应的代码:

Animal dog = new Dog();Animal lion = new Lion();

接下来的16-21句是关键部分,16、20两句分分别把刚刚创建的两个对象的引用压到栈顶。

17和21是run()方法的调用指令。

从指令可以看出,这两条方法的调用指令是完全一样的。可是最终执行的目标方法却并不相同。这是为啥?

实际上:

invokevirtual方法调用指令在执行的时候是这样的:

找到栈顶的第一个元素所指向的对象的实际类型,记作C.

如果类型C中找到run()这个方法,则进行访问权限的检验,如果可以访问,则方法这个方法的直接引用,查找结束;如果这个方法不可以访问,则抛出java.lang.IllegalAccessEror异常。

如果在该对象中没有找到run()方法,则按照继承关系从下往上对C的各个父类进行第二步的搜索和检验。

如果都没有找到,则抛出java.lang.AbstractMethodError异常。

所以虽然指令的调用是相同的,但17行调用run方法时,此时栈顶存放的对象引用是Dog,21行则是Lion。

这,就是java语言中方法重写的本质。

相关文章:

  • 简单代码画皮卡丘_超酷!用 Python 教你绘制皮卡丘和哆啦A梦
  • 分析函数hive计算均值_Hive第六天——Hive函数(开窗函数之累计统计)
  • 蓝卡攻略_剑与远征:4.18版本的新手攻略,崛起的三巨头
  • 关抢占 自旋锁_Linux学习第28节,什么是自旋锁?内核是如何设计,如何实现它的...
  • 2019pro与air怎么选_iPad Air 2019 VS iPad Pro 10.5 | 普通人的角度简单思考
  • mysql安装教程与启动_MySql安装启动两种方法教程详解
  • apparmor mysql_Ubuntu 上更改 MySQL 数据库数据存储目录
  • mysql工程师需要会哪些_MySQL面试高频100问(工程师方向)
  • mysql 客户端 连接数_监控mysql上客户端的连接数
  • mysql带库名查询_MySQL优化
  • docker mysql 差8小时_docker之容器日志输出与系统时间相差8小时解决办法
  • java白盒测试问题_白盒测试项目实践经验总结(三)-返回码问题
  • 9点到17点半 cron_定时任务Quartz简单配置与cron表达式
  • python3 web服务器_python3实现微型的web服务器
  • python if else简写_python代码简写(推导式 if else for in)
  • 11111111
  • CentOS 7 防火墙操作
  • gulp 教程
  • JavaScript HTML DOM
  • javascript数组去重/查找/插入/删除
  • JS+CSS实现数字滚动
  • Ruby 2.x 源代码分析:扩展 概述
  • SwizzleMethod 黑魔法
  • webpack4 一点通
  • 大数据与云计算学习:数据分析(二)
  • 技术攻略】php设计模式(一):简介及创建型模式
  • 简单易用的leetcode开发测试工具(npm)
  • 普通函数和构造函数的区别
  • 前端
  • 算法---两个栈实现一个队列
  • 腾讯优测优分享 | Android碎片化问题小结——关于闪光灯的那些事儿
  • 智能网联汽车信息安全
  • ​Base64转换成图片,android studio build乱码,找不到okio.ByteString接腾讯人脸识别
  • ​Linux Ubuntu环境下使用docker构建spark运行环境(超级详细)
  • # 透过事物看本质的能力怎么培养?
  • (02)vite环境变量配置
  • (10)STL算法之搜索(二) 二分查找
  • (2)MFC+openGL单文档框架glFrame
  • (Git) gitignore基础使用
  • (k8s中)docker netty OOM问题记录
  • (四)c52学习之旅-流水LED灯
  • (转)linux自定义开机启动服务和chkconfig使用方法
  • (转)socket Aio demo
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • **登录+JWT+异常处理+拦截器+ThreadLocal-开发思想与代码实现**
  • . NET自动找可写目录
  • .net core Swagger 过滤部分Api
  • .net framework4与其client profile版本的区别
  • .Net MVC + EF搭建学生管理系统
  • .net redis定时_一场由fork引发的超时,让我们重新探讨了Redis的抖动问题
  • .NET项目中存在多个web.config文件时的加载顺序
  • .Net转Java自学之路—基础巩固篇十三(集合)
  • 。Net下Windows服务程序开发疑惑
  • ::before和::after 常见的用法
  • [ 渗透测试面试篇 ] 渗透测试面试题大集合(详解)(十)RCE (远程代码/命令执行漏洞)相关面试题