内容简介:昨天看到群里面有人分享了一道题目,我答错了,于是趁机了解了下Java的类/对象初始化过程:程序A主要考察的是程序B考察的则是
昨天看到群里面有人分享了一道题目,我答错了,于是趁机了解了下 Java 的类/对象初始化过程:
程序A主要考察的是 类实例初始化 。简单验证了下,类实例初始化过程如下:
- 父类实例初始化
- 构造块/变量初始化(按照文本顺序执行)
- 构造函数
程序B考察的则是 类初始化 。类初始化的过程如下:
- 父类初始化
- static变量初始化/static块(按照文本顺序执行)
但是我们必须做到 面向接口编程,而不是面向实现编程(Program to an ‘interface’, not an ‘implementation’) 。
于是就得看看 Java Language Specification 了。其中 类初始化过程 如下:
- 每个类都有一个初始化锁LC,进程获取LC(如果没有获取到,就一直等待)
- 如果C正在被其他线程初始化,释放LC并等待C初始化完成
- 如果C正在被本线程初始化,即 递归初始化 ,释放LC
- 如果C已经被初始化了,释放LC
- 如果C处于erroneous状态,释放LC并抛出异常NoClassDefFoundError
- 否则,将C标记为正在被本线程初始化,释放LC;然后, 初始化那些final且为基础类型的类成员变量
- 初始化C的父类SC和各个接口SI_n (按照implements子句中的顺序来) ;如果SC或SIn初始化过程中抛出异常,则获取LC,将C标记为erroneous,并通知所有线程,然后释放LC,然后再抛出同样的异常。
- 从classloader处获取assertion是否被打开
- 接下来, 按照文本顺序执行类变量初始化和静态代码块,或接口的字段初始化,把它们当作是一个个单独的代码块。
- 如果执行正常,获取LC,标记C为已初始化,并通知所有线程,然后释放LC
- 否则,如果抛出了异常E。若E不是Error,则以E为参数创建新的异常ExceptionInInitializerError作为E。如果因为OutOfMemoryError导致无法创建ExceptionInInitializerError,则将OutOfMemoryError作为E。
- 获取LC,将C标记为erroneous,通知所有等待的线程,释放LC,并抛出异常E。
可以看到JLS确实规定了父类先初始化(7)、static块和类变量赋值按照文本顺序来(9)。
然后看看 类实例的初始化 :
- 开始调用构造函数(给参数赋值)
- 如果这个构造函数在开始就调用了其他构造函数,那么调用新的构造函数,并按照本规则处理。如果执行过程中抛出异常,则整个过程也抛出同样的异常。如果正常,继续。
- 如果构造函数没有在开始就调用其他构造函数。如果本类不是Object,那么构造函数会隐式或者显式的 调用父类的构造方法 。父类构造方法也依本规则处理。如果执行过程中抛出异常,则整个过程也抛出同样的异常。如果正常,继续。
- 执行实例初始化和实例变量初始化。顺序 按照文本顺序 来处理——从左到右、从上到下。如果执行过程中抛出异常,则整个过程也抛出同样的异常。如果正常,继续。
- 执行剩下的构造函数。如果执行过程中抛出异常,则整个过程也抛出同样的异常。
JLS特意提到,如果子类覆盖了父类的方法,则在构造函数中, 调用的方法也是子类的 。
接下来一个一个看代码:
// 程序A // 父类 class Parent { int i = 1; Parent() { System.out.println(i); int x = getValue(); System.out.println(x); } {i = 2;} protected int getValue() {return i;} } // 子类 class Son extends Parent { int j = 1; Son() {j = 2;} protected int getValue() {return j;} } class Test { public static void main(String[] args) { Son son = new Son(); System.out.println(son.getValue()); } }
- 21行,开始调用Son的构造函数
- 16行,Son的构造函数开始之前,初始化Parent
- 4行,开始执行i的变量初始化
- 10行,开始执行构造代码块
- 6-8行,开始执行Parent的构造函数。注意,调用的getValue方法是子类的,而此时Son.j还没有被构造函数、变量赋值语句初始化,此时Son.j是0。(输出2,0)
- 回到16行,继续执行Son的构造函数
- 22行,打印此时Son.j的值。(输出2)
所以,程序A的输出是:
接下来看程序B:
// 程序B public class MagimaTest { public static void main(String[] args) { magimaFunction(); } static MagimaTest st = new MagimaTest(); static { System.out.println("1"); } { System.out.println("2"); } MagimaTest() { System.out.println("3"); System.out.println("a=" + a + ",b=" + b); } public static void magimaFunction() { System.out.println("4"); } int a = 110; static int b = 112; }
- 3行,在执行main之前,需要初始化MagimaTest类。
- 6行,初始化st成员变量,开始初始化st实例。
- 13开始调用构造函数,但是开始前,需要处理成员变量初始化
- 10行,执行构造代码块(输出2)
- 20行,初始化a变量
- 14行,继续执行构造函数。此时a为110,b尚未初始化,所以是0(输出3,a=110,b=0)
- 7行,st成员变量初始化结束,执行下一个static代码块(输出1)
- 21行,继续初始化下一个static成员变量b
- 4行,调用magimaFunction(输出4)
所以输出是:
2 3 a=110,b=0 1 4
好了,看完了解析。那么我再出一个题目吧: 如果将程序B中的MagimaTest.b改为final的,输出会变化吗?
以上所述就是小编给大家介绍的《Java的类/实例初始化过程》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- vue 源码学习(二) 实例初始化和挂载过程
- Spring源码解析:Bean实例的创建和初始化
- Vue 源码解析(实例化前) - 初始化全局API(二)
- Vue 源码解析(实例化前) - 初始化全局API(三)
- vue 源码解析(实例化前) - 初始化全局 API(最终章)
- C++ 的一大误区——深入解释直接初始化与复制初始化的区别
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
从Python开始学编程
Vamei / 电子工业出版社 / 2016-11-24 / CNY 49.00
改编自Vamei博客的《Python快速教程》。本书以Python为样本,不仅介绍了编程的基本概念,还着重讲解编程语言的主流范式:面向过程、面向对象、面向函数。读者不仅可以轻松学会Python,以后再学习其他编程语言时也会更加容易。一起来看看 《从Python开始学编程》 这本书的介绍吧!