0%

类初始化顺序和类加载基础概念

[toc]


很多时候提到类加载,大家总是没法马上回忆起顺序,这篇文章会用一个例子为你把类加载的诸多问题一次性澄清。

Java类的加载顺序#

引用1个网上的经典例子,并做稍许改动,以便大家更好地理解。
原例子引用自:https://blog.csdn.net/zfx2013/article/details/89453482

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Animal {
private int i = test();
private static int j ?= method();
static {
System.out.println("a");
}
Animal(){
System.out.println("b");
}
{
System.out.println("c");
}
public int test(){
System.out.println("d");
return 1;
}
public static int method(){
System.out.println("e");
return 1;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Dog extends Animal{
{
System.out.println("h");
}
private int i = test();
static {
System.out.println("f");
}
private static int j ?= method();

Dog(){
System.out.println("g");
}
public int test(){
System.out.println("i");
return 1;
}
public static int method(){
System.out.println("j");
return 1;
}
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println();
Dog dog1 = new Dog();
}
}

执行这段main程序,会输出什么?
答案是
eafjicbhig
icbhig

为了方便大家一个个细节去理解, 我换一种方式去提问。

Q: 什么时候会进行静态变量的赋值和静态代码块的执行?#

A:?

  • 第一次创建某个类或者某个类的子类的实例
  • 访问类的静态变量、调用类的静态方法
  • 使用反射方法forName
  • 调用主类的main方法(本例子的第一次静态初始化其实属于这个情况,调用了Dog的main方法)
    注: 类初始化只会进行一次, 上面任何一种情况触发后,之后都不会再引起类初始化操作。

Q:初始化某个子类时,也会对父类做静态初始化吗?顺序呢?#

A:如果父类之前没有被静态初始化过,那就会进行, 且顺序是先父类再子类。 后面的非静态成员初始化也是如此。
所以会先输出eafj。


Q: 为什么父类的method不会被子类的method重写?#

A: 静态方法是类方法,不会被子类重写。毕竟类方法调用时,是必定带上类名的。


Q: 为什么第一个输出的是e而不是a?#

A: 因为类变量的显示赋值代码和静态代码块代码按照从上到下的顺序执行。
Animal的静态初始化过程中,method的调用在static代码块之前,所以先输出e再输出a。
而Dog的静态初始化过程中,method的调用在static代码块之后,因此先输出f,再输出j


Q: 没有在子类的构造器中调用super()时,也会进行父类对象的实例化吗?#

A: 会的。会自动调用父类的默认构造器。 ?super()主要是用于需要调用父类的特殊构造器的情况。
因此会先进行Animal的对象实例化,再进行Dog的对象实例化


Q: 构造方法、成员显示赋值、非静态代码块(即输出c和h的那2句)的顺序是什么?#

A:?

  1. 成员显示赋值、非静态代码块(按定义顺序)
  2. 构造方法
    因此Animal的实例化过程输出icb(如果对输出i有疑问,见下面一题)
    接着进行Dog的实例化,输出hig

Q: 为什么Animal实例化时, i=test()中输出的是i而不是d?#

A:因为你真正创建的是Dog子类,Dog子类中的test()方法由于签名和父类test方法一致,因此test方法被重写了。?
此时即使在父类中调用,也还是用使用子类Dog的方法。除非你new的是Animal。


Q: 同上题, 如果test方法都是private或者final属性, 那么上题的情况会有变化吗??#

A: ?
因为private和final方法是不能被子类重写的。
所以Animal实例化时,i=test输出d。


总结一下顺序:

  1. 父类静态变量显式赋值、父类静态代码块(按定义顺序)
  2. 子类静态变量显式赋值、子类静态代码块(按定义顺序)
  3. 父类非静态变量显式赋值(父类实例成员变量)、父类非静态代码块(按定义顺序)
  4. 父类构造函数
  5. 子类非静态变量(子类实例成员变量)、子类非静态代码块(按定义顺序)
  6. 子类构造函数。

类加载过程#

Q:类加载的3个必经阶段是:#

A:

  1. 加载(类加载器读取二进制字节流,生成java类对象)
  2. 链接(验证,分配静态域初始零值)
  3. 初始化(前面的题目讲的其实就是初始化时的顺序)
    更详细的如下:
    image.png

被动引用中和类静态初始化的关系#

Q:new某个类的数组时,会引发类初始化吗?#

像下面输出什么

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
static class A{
public static int a = 1;
static{
System.out.println("initA");
}
}
?
public static void main(String[] args) {
A[] as = new A[5];
}
}

A:?
new数组时,不会引发类初始化。
什么都不输出。


Q:引用类的final静态字段,会引发类初始化吗?#

像下面输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
static class A{
public static final int a = 1;
static{
System.out.println("initA");
}
}

public static void main(String[] args) {
System.out.println("A.a=" + A.a);
}
}

A: 不会引发。
不会输出initA。 去掉final就会引发了。
(注意这里必须是基本类型常量, 如果是引用类型产量,则会引发类初始化)


Q:子类引用了父类的静态成员,此时子类会做类初始化嘛?#

如下会输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
static class A{
public static int a = 1;
static{
System.out.println("initA");
}
}

static class B extends A{
static {
System.out.println("initB");
}
}

public static void main(String[] args) {
System.out.println("B.a=" + B.a);
}
}

A:?
子类不会初始化。
打印initA,却不会打印initB。

类加载器#

双亲委派#


类加载时的双亲委派模型,不知道能怎么出题。。。反正就记得优先去父类加载器中看类是否能加载。
就贴个图吧
image.png


注意,上面的图有问题。
Bootsrap不是ClassLoader的子类,他是C++编写的。
而ExtClassLoader和AppClassLoader都是继承自ClassLoader的

Q:java中, 是否类和接口的包名和名字相同, 那么就一定是同一个类或者接口?#

A:
错误。
1个jvm中, 类和接口的唯一性由 二进制名称以及它的定义类加载器 共同决定。
因此2个不同的加载器加载出来相同的类或接口时, 实际上是不同的。


Q: 讲一下ClassLoader原理, 以及应用场景#

loaderClass 双亲加载实现(这里会体现先去父亲找,再自己)
findClass 如何根据名字,生成1个class(内部需要借助defineClass)
defineClass 通过这个方法生成1个class类

例如需要根据类目,从某个远端网络加载获取这个类, 而且获取过来的时候还是加密的,需要在findClass里对byte数组做解密并加载。

https://blog.csdn.net/zzti_erlie/article/details/82757435


Q:如果在你项目中建一个java.lang.String的类,那系统中用的String类是你定义的String类,还是原生api中的String类?#

A:
用的原生api中的string, 因为双亲委派机制。


Q : 为什么要用双亲委托,有什么好处?#

A:

  • 对于任意一个类,都需要由加载它的"类加载器和这个类本身"来一同确立其在Java虚拟机中的唯一性。那么双亲委派可以保证顺序加载的特性。
  • 核心类的安全。Object类如果不使用双亲委派原则的话,那么A创建的Object对象就可能和B创建的Object是不一样的。不使用双亲委派原则无法保证一些java核心类库的唯一性
  • 例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。

Q: 那我如果真的有需求, 不想用双亲的机制呢?#

A:
按照上 面说的, 自己重写类加载的loaderClass就行了, 不走双亲机制的那块代码。


Q: 数组类是怎么做加载的?#

A:
数组类是由AppClassLoader加载的。
数组类打印className时,前面会有个[Lxx类
二维数组就是[[Lxxx类
数组类的父类型是Object

注意此时加载的是数组类,而数组类里面的对象是不会做自动加载的
因此xx类的静态代码并不会被直接调用