不知道各位在Java在面试中有没有遇到过类的初始化加载顺序的面试题,举个?
这些面试题如果没有很好理解类的初始化顺序,一般都会在面试过程中栽跟头。
比如说:
这里给出一道面试题,可以稍微来验证一下对这块知识是否掌握,请根据代码写出控制台输出的内容
class Mug {
Mug(int marker) {
System.out.println("Mug(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
} }
public class Mugs {
Mug c1;
static int x;
Integer y;
{
c1 = new Mug(1);
System.out.println("x="+x);
System.out.println(y);
}
static{
x++;
System.out.println("x="+x);
}
Mugs() {
System.out.println("Mugs()");
}
static Mug c2= new Mug(2);
public static void main(String[] args) {
// 请写出控制台输出的内容
System.out.println("Inside main()");
Mugs x = new Mugs();
}
}
x=1
Mug(2)
Inside main()
Mug(1)
x=1
null
Mugs()
解析:
如果没有做出来,就可以参考下面的图来尝试再解答一次,本文也会按照这个图的内容,由浅入深告诉你为什么是这样的顺序加载执行
首先我们来回顾一下Java中类的成员组成内容,一般是由下面四块内容组成
回顾完这些基础内容后,我们再来看看看变量初始值有关的问题
Java 尽力保证:所有变量在使用前都能得到恰当的初始化,这是Java的一大准则
对于定义于方法内部的局部变量, Java 以编译时刻错误的形式来贯彻这种保证。所以如果你这么写:
void f() {
int i;
i++; // Error -- i not initialized
}
就会得到一条出错消息,告诉你 i 可能尚未初始化。
既然知道未初始化的变量在没有赋值时不能使用,那为什么不直接给默认值防止这个错误?
比方说让编译器自动加上默认值,比如编译器也可以为 i 赋予一个缺省值,这是因为在Java设计者看来未初始化这种情况看起来更象是程序员的疏忽,所以采用缺省值反而会掩盖这种失误
但是另外一边,Java的成员变量又是可以在声明时不赋值,这不是跟??上面的准则相反了吗?
比如,文中的面试题,这三个都是没有在声明时赋值,为什么编译器不报错?
Mug c1;// 引用类型成员变量
static int x;// 基本类型静态变量
Integer y;// 引用类型成员变量
这是因为以下原因:
public class InitialValues {
// 基本类型初始值(默认值)
boolean t; // false
char c; // 0 你可以在main方法调用println方法验证是否是无输出值,实际它的默认值是0
byte b;// 0
short s;// 0
int i;// 0
long l;// 0
float f;// 0.0
double d; // 0.0
// 引用类型初始值
Integer x;// null
void print(String s) { System.out.println(s); }
}
尽管数据成员的初值没有给出,但它们确实有初值(char 值为 0,所以显示为空白)。
因为成员变量有初始值,所以你至少不会冒“未初始化变量”的风险了,这也能保证上面Java设计准则。
看到这里,你应该就会明白文初的面试题,为什么Integer y
这个变量在代码块中输出的是null值了
因为引用类型成员变量默认就是为null
回顾一下类的成员变量赋值方式:
int x = 0
在运行时刻,你可以调用方法或执行某些动作来确定初值,这 为你在编程时带来了更大的灵活性。但要牢记:你无法屏蔽自动初始化的进行,它将在构造 器被调用之前发生。因此,假如使用下述代码:
class Counter {
int i;
Counter() { i = 7; }
成员变量int i
首先会被置 0,然后变成 7,对于基本类型和对象引用,包括在定义时已经指定初值的 变量,这种情况都是成立的。因此,编译器不会强制你一定要在构造器的某个地方或在使用 它们之前对元素进行初始化——因为初始化早已得到了保证
静态方法(Static Method)与静态成员变量一样,属于类本身,在类装载的时候被装载到内存(Memory),不自动进行销毁,会一直存在于内存中,直到JVM关闭。
非静态方法(Non-Static Method)又叫实例化方法,属于实例对象,实例化后才会分配内存,必须通过类的实例来引用。不会常驻内存,当实例对象被JVM 回收之后,也跟着消失。
由于方法是在被调用时,才会对执行顺序有影响,这个只要看清方法前被调用的对象,就能比较清晰知晓执行顺序。
// 当构造器被用来创建一个Tag对象时,你看到下面按照下面注释的数字顺序执行
class Tag {
Tag(int marker) {
System.out.println("Tag(" + marker + ")");
}
}
class Card {
Tag t1 = new Tag(1); // 先于构造器执行 ①
Card() {
// 在构造器执行时执行
System.out.println("Card()");
t3 = new Tag(33); // 重新初始化t3 ④
}
Tag t2 = new Tag(2); // 定义在构造器执行之后 ②
void f() {
System.out.println("f()");
}
Tag t3 = new Tag(3); // 定义在最后 ③
}
public class OrderOfInitialization {
public static void main(String[] args) {
Card t = new Card();
t.f(); // 表明构造器已经被执行完毕
}
}
这里控制台输出的内容为:
Tag(1)
Tag(2)
Tag(3)
Card()
Tag(33)
f()
举这个例子是为了说明:程序在main方法启动后
new Tag(3)
的对象引用不再被用,之后垃圾回收器会回收new Tag(3)
引用对于静态成员变量初始化赋值,它跟成员变量赋值一样:
唯一不同就是:静态成员变量在内存中只有一份,无论创建多少对象,它的变量值用于都只有一份。
// 下面代码按照数字顺序执行
class Bowl {
Bowl(int marker) {
System.out.println("Bowl(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
} }
class Table {
static Bowl b1 = new Bowl(1);
Table() {
System.out.println("Table()");
b2.f(1);
}
void f2(int marker) {
System.out.println("f2(" + marker + ")");
}
static Bowl b2 = new Bowl(2);
}
class Cupboard {
Bowl b3 = new Bowl(3);
static Bowl b4 = new Bowl(4);
Cupboard() {
System.out.println("Cupboard()");
b4.f(2);
}
void f3(int marker) {
System.out.println("f3(" + marker + ")");
}
static Bowl b5 = new Bowl(5);
}
public class StaticInitialization {
public static void main(String[] args) {
System.out.println("Creating new Cupboard() in main"); // ③
new Cupboard(); // ④
System.out.println("Creating new Cupboard() in main");// ⑤
new Cupboard();// ⑥
t2.f2(1); // ⑦
t3.f3(1);// ⑧
}
static Table t2 = new Table(); // 注意此处会先执行 ①
static Cupboard t3 = new Cupboard(); // 注意此处会先执行 ②
}
上面的代码的执行顺序:
Bowl(1)
Bowl(2)
Table()
f(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
f2(1)
f3(1)
我一开始对这个结果是不理解,后面发现捋一捋还是有道理可遵循的
注意事项:
举这个例子是为了说明:
Java 除了在构造器和方法外进行成员变量赋值,还允许将初始化动作组织成一个特殊的“子句”中(有时也叫作“代码块”)。 就象下面这样:
class Spoon {
static int i;
int j;
static {
// 静态修饰叫做静态代码块
i = 47;
}
{
// 代码块
j = 48;
}
尽管上面的代码看起来象个方法,但静态代码块实际只是一段跟在 static 关键字后面的代码。与其他 静态初始化动作一样,这段代码仅执行一次:当你首次生成这个类的一个对象时,或者首次 访问属于那个类的一个静态成员时(即便从未生成过那个类的对象)。
这里为了方便静态代码块和非静态代码块,可以从以下去它们的区别去记忆:
静态代码块:
非静态代码块【即代码块】:
class Cup {
Cup(int marker) {
System.out.println("Cup(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Cups {
static Cup c1;
static Cup c2;
static {
c1 = new Cup(1);
c2 = new Cup(2);
}
Cups() {
System.out.println("Cups()");
}
}
public class ExplicitStatic {
static Test monitor = new Test();
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.c1.f(99); // (1)
monitor.expect(new String[] {
"Inside main()",
"Cup(1)",
"Cup(2)",
"f(99)"
});
}
// static Cups x = new Cups(); // (2)
// static Cups y = new Cups(); // (2)
}
无论是通过标为(1)的那行程序访问静态的 c1 对象,还是把(1)注释掉,让它去运行标为(2) 的那行,Cups 的静态初始化动作都会得到执行。如果把(1)和(2)同时注释掉,Cups 的静态初 始化动作就不会进行。此外,激活一行还是两行(2)代码都无关紧要,静态初始化动作只进 行一次。
在涉及到子类继承父类并创建子类对象的时候,类的初始化顺序会一丢丢不同,一般来说顺序是这样的:
父类静态相关内容初始化->子类静态相关内容初始化->父类对象内容初始化->子类对象内容初始化
具体的内容可以看下面的图
这里我们也可以举个例子来验证上面的内容
class ParentClass {
// 父类私有普通成员变量
private int i = 9;
// 子类可以访问的父类的成员变量
protected int j;
// 父类构造器
ParentClass() {
// super.Object()
System.out.println("i = " + i + ", j = " + j);
j = 39;
}
// 父类私有静态静态变量
private static int x1 = print("static ParentClass.x1 initialized");
// 父类静态方法
static int print(String s) {
System.out.println(s);
return 47;
}
}
public class ChildrenClass extends ParentClass {
// 子类私有普通成员变量
private int k = print("ChildrenClass.k initialized");
// 子类构造器
public ChildrenClass() {
// 隐式调用 super.ParentClass()
System.out.println("k = " + k);
System.out.println("j = " + j);
}
// 子类私有静态变量
private static int x2 = print("static ChildrenClass.x2 initialized");
public static void main(String[] args) {
//System.out.println(ParentClass.print("ParentClass print method"));
System.out.println("ChildrenClass constructor");
ChildrenClass child = new ChildrenClass();
}
}
控制台输出的内容
static ParentClass.x1 initialized
static ChildrenClass.x2 initialized
ChildrenClass constructor
i = 9, j = 0
ChildrenClass.k initialized
k = 47
j = 39
执行过程分析:
new ChildrenClass()
注释掉,来证明这一点。)这里有几个关键点必须要理解:
ChildrenClass constructor
再加载子类和父类?为什么main方法一开始不是先执行首条语句输出ChildrenClass constructor
再加载子类和父类?
因为main方法的首条语句有可能依赖父类的数据成员,比如注释中依赖父类的静态方法,假设去除main方法第一行代码注释,然后执行 main依赖父类方法print()
,父类方法print
等待main方法执行完首行后,再加载才能初始化静态方法,形成死循环。
同理也有可能依赖子类或者其他类的数据成员,这样自然就好理解为什么是要等到类加载后再执行main方法方法体内容。
为什么要先加载父类静态内容再加载子类静态内容,即为什么先初始化父类先?
因为子类可以能引用到父类的数据,比如静态数据、静态方法(静态初始化可能会依赖于基类成员能否被正确初始化的)比如子类private int k = print("ChildrenClass.k initialized");
虽然是实例变量依赖父类的print()
方法,可能会有偏差,如果我加上个static
修饰,变成子类静态变量依赖于父类的静态方法,这样就自然能想明白了。
为什么不是创建子类对象调用子类构造器时要先执行父类的构造器?
同样,这个方法也可以理解为,实例化子类对象要依赖父类的数据成员,比如子类的构造器调用System.out.println("j = " + j);
就是实例的时候需要用到父类的数据成员,那么就自自然很容易理解。
而且为了方便理解,你可以理解为子类如果存在继承关系时,构造器首行编译器会隐式调用super()
,即隐式调用父类无参数构造器,这样更加方便理解为什么父类构造器总是比子类构造器先执行。
而且Java也确实是这样的做,如果我们在子类构造器手动调用父类的构造器super()
并且不是放在子类构造器首行时,编译器会报错,目的就是要保证父类先实例化,保证子类如果有依赖父类数据成员时,能够准确无误调用。
为了方便理解:你可以将静态代码块类比成静态变量,非静态代码块【代码块】类比非静态变量【普通成员变量】
最后本文可以理解为简单几句话:
最后类的初始化顺序可以用一张图来简化理解
涉及到子类和父类的初始化顺序时可以看下面的图来简化理解