深入理解Java虚拟机:从JVM角度理解类初始化、方法重载、重写
类初始化
在讲类初始化之前,我们先对类声明周期有一个大概的了解。如下图,
类的声明周期可以分为7个阶段,但今天我们只讲初始化阶段。我觉得除了使用和卸载步骤之外,初始化是我们平时学习的时候也算最简单最简单的了。笔试。如果想了解每一步,可以看《深入理解Java虚拟机》这本书。 让我们从初始化过程开始。 ?传递了 类变量 的初始值。例如,假设一个整数的初始值为 0,一个对象的初始值为 0。基本数据类型的初始值如下: 首先我们来想一个问题。当我们运行java程序时,每个类都会被初始化吗?如果不是每个类都会执行初始化过程,那么什么时候类会执行初始化过程呢? 答案是并不是所有的类都会执行初始化过程。想一想,如果这个类根本不被使用,为什么要初始化它呢?它充满了。 关于何时执行初始化过程,虚拟机规范严格规定了并且只有在5种情况下才会立即初始化类。 请注意, 只有。这五种行为称为对类的主动引用。 类的初始化过程做了什么? 类的初始化过程中,说白了,执行的是一个类的constructor()方法过程。请注意,这个 clinit 不是该类的构造函数(init())。 关于 clinit() 方法包含哪些内容? 事实上,clinit()方法是编译器自动从类变量和静态语句块中的语句merge的所有赋值操作中自动收集的,编译器收集的顺序语句由句子在源文件中出现的顺序决定。并且在静态语句块中只能访问静态语句块之前定义的变量。在它之后定义的变量可以在前面的静态语句块中赋值,但是无法访问。比如下面的程序。 给你一个练习 输出结果是什么? 答案是20,我想大家都知道为什么。因为父类是先初始化的。 不过,这里需要注意的是,对于类来说,当执行该类的 clinit() 方法时,会先执行父类的 clinit() 方法,但是对于接口来说,会先执行 clinit() 方法接口的( )方法不会被执行。父接口的clinit()方法不会被执行。只有当使用了父类接口中定义的变量时,才会执行父接口的clinit()方法。 上面提到的类初始化的五种情况称为主动引用。主动的存在也意味着存在所谓的被动参考。这里需要提到的是,被动引用不会触发类初始化。下面我们提供几个被动引用的例子: 输出结果 表示子类的初始化是未触发 输出结果什么都没有。 输出结果:hello 在编译阶段不会输出。对此常数进行了一些优化。例如,由于Test3类使用了常量“hello”,因此“hello”常量在编译阶段就已存储在Test3类的常量池中。将来,FatherClass.value 引用实际上将转换为 Test3 类对其自己的常量池的引用。也就是说,编译成class文件后,两个类就没有任何关系了。 对于重载,我想学过Java的人都明白,但今天我们就从虚拟机的角度来看一下重载。 首先看一段代码: 运行结果: 动物跑跑跑 动物跑跑跑 在此之前,我们先了解两个概念。 首先我们看一行代码: Animal dog = new Dog(); 变量dog的称为 所谓静态类型,是指在代码编译时就可以确定的类型。也就是说,dog的静态类型可以在编译时确定。但是在编译时没有办法知道变量dog的实际类型是什么。 现在我们来看看虚拟机选择哪种方法来重载。 对于静态类型相同但实际类型不同的变量,虚拟机在重载时根据参数的静态类型而不是实际类型进行选择。而静态类型是编译器已知的,这意味着在编译阶段就已经决定选择哪种重载方法。 由于狗和狮子的静态类型都是Animal,所以选择了run(动物)方法。 不过需要注意的是,有时可能会存在多个重载版本,即重载版本并不唯一。我们来看看下面的代码。 运行代码。我想大家都知道输出结果是 hello char 因为a的静态类型是char,所以会匹配sayHello(char arg); 但是如果我们注释掉方法sayHello(char arg )),又跑了。 结果输出: hello int 实际上,由于方法中目前没有静态类型字符方法,所以会自动进行类型转换。 ‘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的转换是不确定的。 (为什么不安全?这个留给你自己思考) 继续注释掉长类型的方法。结果是: helloCharacter 此时会发生自动装箱,并且'a'被封装为Character Type。 继续评论字符类型方法。输出 hello 可序列化 为什么? 字符或数字与序列化有什么关系?其实这是因为Serialized是Character类实现的接口。自动装箱后发现找不到装箱类,但是找到了装箱类实现的接口类型,所以发生了一次自动变换。 我们继续注释掉Serialiable。此时的输出为: hello Object 此时'a'被框架并转化为父类。如果有多个父类,则会继承自 关系,从下往上开始查找,即越靠近顶层,优先级越低。 继续注释对象方法。此时输出为: hello char... 此时'a'被转换为数组元素。 从上面的例子中我们可以看出,元素的静态类型不一定是固定的。 是在编译时按照优先级原则进行转换的。其实这也是Java语言中重载的本质 先看一段代码 运行结果: 小狗跑,狮子跑跑跑并且运行 我想大家都对这个结果感兴趣,这是毫无疑问的。它们的静态类型是相同的。虚拟机如何知道要执行哪个方法? 显然,虚拟机是根据实际类型来执行方法的。我们看一下main()方法的部分内容字节码 解释一下这个字节码: 第0-15行的作用是为dog和lion对象创建内存空间,并调用Dog、Instance构造函数属于狮子类型。对应代码: Animal Dog = new Dog(); 动物狮子 = new Lion();这两个对象的引用被推送到堆栈的顶部。 17和21是run()方法的调用指令。 从指令中可以看出,这两个方法的调用指令是完全一样的。但最终的目标执行方式并不相同。为什么? ?如果存在 run() 方法,则会检查访问权限。如果可以访问,则该方法将直接引用该方法并终止搜索;如果该方法不可用,则会抛出 java.lang.IllegalAccessError 异常。 因此,尽管第 17 行调用 run 方法时指令调用相同,但堆栈顶部存储的对象引用是 Dog,第 21 行是 Lion。 这就是Java语言中方法重写的本质。 作者:迪帅数据类型 初始值 数据类型 初始值 intlong 0L 浮动 0.0f 短 (短)0 双d。 '\u0000' reference null byte (byte)0 初始化过程
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);
}
}
复制代码
被动引用
/**
* 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
复制代码
class Test3{
public static void main(String[] args){
SonClass[] sonClass = new SonClass[10];//引用上面的SonClass类。
}
}
复制代码
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);
}
}
复制代码
重载
//定义几个类
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);
}
}
复制代码
猜出这个结果。但为什么选择这个方法来重载呢?如何选择虚拟机?
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');
}
}
复制代码
重写
//定义几个类
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();
}
}
复制代码
//声明:我只是挑出了一部分关键的字节码
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
复制代码
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。