类初始化和实例初始化
Posted lyw-hunnu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了类初始化和实例初始化相关的知识,希望对你有一定的参考价值。
先通过一个例子让大家先体会一下类初始化和实例化对象时的一些顺序问题。
上面两个类的成员变量和方法几乎都是一样的,包括成员变量、静态变量(类变量)、静态代码块、构造方法、非静态代码块、成员方法、静态方法等,其中,Son 类继承了 Parent 类,main 启动方法写在子类 Son 中。
注意的是。Parent 类和 Son 类中的方法 text 、method 和 textSon 和 methodSon ,这里为了更好的验证加载和初始化的,先不采用重写,等一下再具体讲解重写。
运行结果如下:
这里分为两个问题:类初始化问题 和 对象实例化问题。
一、类加载问题
让我们先直接跳过前面的 加载、连接,从初始化开始讲起:
1. 当虚拟机启动时,用户需要指定一个要执行的方法,也就是 main 方法。虚拟机会先初始化这个类。
main 方法在子类 Son 中,所以虚拟机首先要加载和初始化 Son类。
另外还有三种情况会触发类的初始化,当然在初始化之前会完成 加载 和 准备的过程。
2. 遇到这四个字节码指令时: new:使用 new 关键字实例化对象时 getstatic, putstatic:读取或设置一个类的静态变量时。(被 final 修饰,已以在编译期把结果放在常量池的字段除外) invokestatic :调用一个类的静态方法时。 3. 使用 java.lang.reflect 包的方法对类进行发射调用的时候 4. 当初始化一个类时,发现其父类还没有初始化,会先触发这个父类的初始化。
2. 初始化 Son 类时发现其父类 Parent 还美哟初始化,会先初始化父类 Parent。
3. 初始化一个类实际上就是调用 <clinit>() 方法,这个方法不是你自己写的,是编译器自动收集类中所有的 类变量的赋值语句 和 静态语句块(static{} ) 中的语句产生的,并且是按 顺序 由上至下 执行。
在上面的代码中,会先运行父类 Parent 的 <clinit>() 方法,
父类 <clinit>() 方法: 1. b = method(); 打印”Parent method 方法“ 2. 静态代码块 打印”Parent 静态代码块“ static{}
接着再运行 子类Son 的 <clinit>() 方法。
子类 <clinit>() 方法: 1. b = methodSon(); 打印”Son methodSon 方法“ 2. 静态代码块 打印”Son 静态代码块“ static{}
需要注意的是,静态方法在类初始化的时候并不会被收集到 <clinit>() 方法中。你可以尝试在 Parent 类中加入一下代码,就会发现,并没有打印 ”Pareng print“ 语句。只有被调用时才会打印。
简单的介绍一下类加载:
当一个类从 java 文件被编译成一个 class 文件后就开始了一个类的生命周期,分为 类加载,使用(对象实例化),消亡。
类加载分为三步:加载、连接、初始化,其中,连接又分为验证、准备,解析。
加载: 1.通过一个类的全限定名来获取定义此类的二进制字节流(这个字节流可以就是编译得到的 class 文件,也可以是 jar 包,甚至是从网络中获取的)。 2. 将这个字节流所代表的静态存储结构存放中方法区中(方法区是虚拟内存中的概念) 3. 然后在 java 堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区这些数据的访问入口 在类放射机制中,类的实例化是调用 java.lang.Class 对象的 newInstance() 方法,同时这个 Class 对象可以得到该类的构造方法和成员方法等
(在这里这么啰嗦是因为,我当初对存储在方法区中的静态存储结构是什么真是想得都快秃头了,还以为是 static 关键修饰的方法,不过如果我说错l,麻烦知道的你一定要告诉我)
连接(加载阶段和连接阶段是可以交叉进行的) 连接分为 验证,准备,解析 1. 验证:这一阶段的目的时为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求 包括对 Class 文件格式的检查,对字节码信息进行语义分析以符合java 语言规范,对方法体进行分析以保证运行时不会危害虚拟机安全等等。 2. 准备:正式为 "类变量"(被 static 修饰的变量)在 "方法区" 中分配内存并设置初始值。 注意是是为"类变量" 而不是实例变量,实例变量在对象实例化时随着对象分配到 "java 堆"中。 ”一般情况“ 下初始值设置为该数据类型的 零值。 ”特殊情况“:当这个类变量同时被 final 关键字修饰,那么这个类变量中存在 ConstantValue 字段,在准备阶段会根据 ConstantValue 的值进行赋值。 这个常量字段在编译时就被放置在常量池当中
将 Parent 中的 method 方法进行改写,为
运行结果为
可以看到,在初始化阶段,没有被 final 修饰的 static 对象只有初始 零值。
初始化的 <clinit>() 方法 除了前面已经提到过的 <clinit>() 方法的构成(静态变量赋值语句,静态代码块构成),和父类的 <clinit>() 必须在 子类的 <clinit>() 前执行外,还有三点: 1. <clinit>() 方法对于类和接口来说不是必须的。如果一个类中没有静态变量赋值语句,也没有静态代码块,那么编译器可以不为类生成 <clinit>() 方法。 2. 执行类和接口的 <clinit>() 方法时,不一定要执行 ” 父接口“ 的 <clinit>() 方法。 接口中美哟静态代码块,只可能有静态变量的赋值语句,只有需要使用到这些静态变量时才初始化这个父接口 3. 虚拟机会保证一个类的 <clinit>() 会被正确地加锁和同步,确保一段时间内,只有一个进程可以调用 <clinit>() 方法。
二、对象实例化问题。
对象实例化会执行 .class (二进制字节流)文件中的 <init>() 方法。
1. 在初始化子类对象之前必须要先初始化父类的对象,也就是说,父类的 <init>() 方法在子类的 <init>() 方法前执行。
这是因为 <init>() 方法的首行为 super() 或 super(实参),即父类的 <init>
2. <init>() 方法可能被重载,有几个构造器就会有几个 <init>() 方法。
3. <init>() 方法由 非静态变量赋值语句 和 非静态代码块对应构造器的代码组成,
其中,非静态变量的赋值语句 和 非静态代码块 按照顺序从上到下排序,构造方法的代码在最后。
因此,先执行父类的 <init>() 方法,为:
1. a = text(); ”Parent text 方法" 2. 非静态代码块 "Parent 非静态代码块“ { } 3. 构造方法 ”Parent 构造方法“
子类同。
4. 每创建一个对象时,都会调用对应构造器中的 <init>() 方法。
上面的代码中,在 main 函数中实例化了 Son 的两个对象,因此调用了两次构造器中的 <init>() 方法
三、方法重写。
修改一下上面的代码
1. 有几类方法是不支持重写的:被 final 关键字修饰的方法,静态方法,private,缺省 等子类不可见方法。
所以在类的初始化时,虽然 method 方法在 父类和子类中都存在,但是不存在重写。运行结果依旧如之前。
2. 非静态方法前面有一个默认的对象 this
this 在构造器中表示当前正在创建的对象。
因为现在正在创建子类 Son ,所以调用非静态方法 text() 时调用的时子类重写的 text() 方法。
这不禁让人回想其一个说法,静态方法不允许使用 this,因为 this 是属于 实例的。
以上是关于类初始化和实例初始化的主要内容,如果未能解决你的问题,请参考以下文章