16

一网打尽“类”的初始化实例化知识点

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzIxNzU1Nzk3OQ%3D%3D&%3Bmid=2247492037&%3Bidx=1&%3Bsn=bdcdcae4880b238ea58eb9c81d8c1fd7
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

之前说了类加载的过程,但是有的读者表示还是有些面试题还是答不来,所以今天就来总结下 类加载、对象实例化 方面的知识点/面试题,帮助大家加深印象。

全是干货,一网打尽类的 基础知识 !先看看下面的问题都能回答上来吗?

  • 描述new一个对象的过程,并结合例子说明。

  • 类初始化的触发时机。

  • 多线程进行类的初始化会出问题吗?

  • 类的实例化触发时机。

  • <clinit>() 方法和 <init>() 方法区别。
  • 在类都没有初始化完毕之前,能直接进行实例化相应的对象吗?

  • 类的初始化过程与类的实例化过程的异同?

  • 一个实例变量在对象初始化的过程中会被赋值几次?

描述new一个对象的过程

先上图,再描述:

F3mYnaq.png!mobile

类加载链接

Rb2IfeI.png!mobile

类初始化

A36VzqI.png!mobile

对象实例化

Java 中对象的创建过程包括 类初始化和类实例化两个阶段。而 new 就是创建对象的一种方式,一种时机。

当执行到 new 的字节码指令的时候,会先判断这个类是否已经初始化,如果没有初始化就要进行类的初始化,也就是执行类构造器 <clinit>() 方法。如果已经初始化了,就直接进行类对象的实例化。

  • 类的初始化 ,是类的生命周期中的一个阶段,会为类中各个类成员赋初始值。
  • 类的实例化 ,是指创建一个类的实例的过程。

但是在类的初始化之前, JVM 会保证类的 装载,链接(验证、准备、解析) 四个阶段都已经完成,也就是上面的第一张图。

  • 装载
    Java
    .class
    java.lang.Class
    
  • 链接
    JVM
    JVM
    

那到底类加载的时机是什么时候呢? JVM 并没有规范何时具体执行,不同虚拟机的实现会有不同,常见有以下两种情况:

  • 隐式装载
    new
    ClassLoader
    
  • 显示装载 :在编写源代码时,主动调用 Class.forName() 等方法也会进行 class 装载操作,这种方式通常称为显示装载。

所以到这里, 大的流程框架 就搞清楚了:

  • JVM 碰到 new 字节码的时候,会先判断类是否已经 初始化 ,如果没有初始化(有可能类还没有加载,如果是隐式装载,此时应该还没有类加载,就会先进行 装载、验证、准备、解析 四个阶段),然后进行类 初始化

  • 如果已经初始化过了,就直接开始类对象的 实例化 工作,这时候会调用类对象的 <init> 方法。

结合例子说明

然后说说具体的逻辑,结合一段类代码:

public class   Run   {

public static void main (String[] args) {

new Student();

}

}

public class   Person {

public static int value1 =  100 ;

public static final int value2 =  200 ;

public int value4 =  400 ;

static {

value1 =  101 ;

System.out.println( "1" );

}

{

value1 =  102 ;

System.out.println( "3" );

}

public Person () {

value1 =  103 ;

System.out.println( "4" );

}

}

public class   Student   extends   Person {

public static int value3 =  300 ;

public int value5 =  500 ;

static {

value3 =  301 ;

System.out.println( "2" );

}

{

value3 =  302 ;

System.out.println( "5" );

}

public Student () {

value3 =  303 ;

System.out.println( "6" );

}

}

  • 首先是类装载,链接(验证、准备、解析)。

  • 当执行类准备过程中,会对类中的 静态变量 分配内存,并设置为初始值也就是 “0值” 。比如上述代码中的 value1,value3 ,会为他们分配内存,并将其设置为0。但是注意,用final修饰静态常量 value2 ,会在这一步就设置好初始值102。

  • 初始化阶段,会执行类构造器 <clinit> 方法,其主要工作就是初始化类中静态的(变量,代码块)。但是在当前类的 <clinit> 方法执行之前,会保证其父类的 <clinit> 方法已经执行完毕,所以一开始会执行最上面的父类 Object的<clinit> 方法,这个例子中会先初始化父类Person,再初始化子类Student。

  • 初始化中,静态变量和静态代码块顺序是由语句在源文件中出现的顺序所决定的,也就是谁写在前面就先执行谁。所以这里先执行父类中的 value1=100,value1 = 101 ,然后执行子类中的 value3 = 300,value3 = 301

  • 接着就是创建对象的过程,也就是类的实例化,当对象被类创建时,虚拟机会 分配内存 来存放对象自己的实例变量和父类继承过来的实例变量,同时会为这些事例变量赋予默认值(0值)。

  • 分配完内存后,会初始化父类的普通成员变量 (value4 = 400) ,和执行父类的普通代码块 (value1=102) ,顺序由代码顺序决定。

  • 执行父类的构造函数 (value1 = 103)

  • 父类实例化完了,就实例化子类,初始化子类的普通成员变量 (value5 = 500) ,执行子类的普通代码块 (value3 = 302) ,顺序由代码顺序决定。

  • 执行子类的构造函数 (value3 = 303)

所以上述例子打印的结果是:

123456

总结一下 执行流程 就是:

  1. 父类静态变量和静态代码块;

  2. 子类静态变量和静态代码块;

  3. 父类普通成员变量和普通代码块;

  4. 父类的构造函数;

  5. 子类普通成员变量和普通代码块;

  6. 子类的构造函数。

最后,大家再结合 流程图 好好梳理一下:

F3mYnaq.png!mobile

类加载链接

Rb2IfeI.png!mobile

类初始化

A36VzqI.png!mobile

对象实例化

类初始化的触发时机

在同一个类加载器下,一个类型只会被初始化一次,刚才说到 new对象 是类初始化的一个判断时机,其实一共有 六种 能够触发类初始化的时机:

  • 虚拟机启动时,初始化包含 main 方法的主类;

  • 遇到 new 等指令创建对象实例时,如果目标对象类没有被初始化则进行初始化操作;

  • 当遇到访问静态方法或者静态字段的指令时,如果目标对象类没有被初始化则进行初始化操作;

  • 子类的初始化过程如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;

  • 使用反射 API 进行反射调用时,如果类没有进行过初始化则需要先触发其初始化;

  • 第一次调用 java.lang.invoke.MethodHandle 实例时,需要初始化 MethodHandle 指向方法所在的类。

多线程进行类的初始化会出问题吗

不会, <clinit>() 方法是阻塞的,在多线程环境下,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() ,其他线程都会被阻塞。

类的实例化触发时机

  • 使用 new 关键字创建对象
  • newInstance
    Constructor
    newInstance
    
  • 使用 Clone 方法创建对象
  • 使用(反)序列化机制创建对象

<clinit>() 方法和 <init>() 方法区别。

  • <clinit>() 方法发生在类初始化阶段,会执行类中的静态类变量的初始化和静态代码块中的逻辑,执行顺序就是语句在源文件中出现的顺序。

  • <init>() 方法发生在类实例化阶段,是默认的构造函数,会执行普通成员变量的初始化和普通代码块的逻辑,执行顺序就是语句在源文件中出现的顺序。

在类都没有初始化完毕之前,能直接进行实例化相应的对象吗?

刚才都说了先初始化,再实例化,如果这个问题可以的话那不是打脸了吗?

没错,要打脸了哈哈。

确实是先进行类的初始化,再进行类的实例化,但是如果我们在类的 初始化阶段 就直接实例化对象呢?比如:

public class   Run   {

public static void main (String[] args) {

new Person2();

}

}

public class   Person2   {

public static int value1 =  100 ;

public static final int value2 =  200 ;

public static Person2 p =  new Person2();

public int value4 =  400 ;

static {

value1 =  101 ;

System.out.println( "1" );

}

{

value1 =  102 ;

System.out.println( "2" );

}

public Person2 () {

value1 =  103 ;

System.out.println( "3" );

}

}

嘿嘿,这时候该怎么打印结果呢?

按照上面说过的逻辑,应该是先静态变量和静态代码块,然后普通成员变量和普通代码块,最后是构造函数。

但是因为静态变量又执行了一次 new Person2() ,所以实例化过程被 强行提前 了,在初始化过程中就进行了实例化。这段代码的结果就变成了:

23123

所以,实例化不一定要在类初始化结束之后才开始初始化,有可能在 初始化过程中 就进行了实例化。

类的初始化过程与类的实例化过程的异同?

学了上面的内容,这个问题就很简单了:

  • 类的初始化 ,是指在类装载,链接之后的一个阶段,会执行 <clinit>() 方法,初始化静态变量,执行静态代码块等。

  • 类的实例化 ,是指在类完全加载到内存中后创建对象的过程,会执行 <init>() 方法,初始化普通变量,调用普通代码块。

一个实例变量在对象初始化的过程中最多可以被赋值几次?

那我们就试试举例出最多的情况,其实也就是每个要经过的地方都对实例变量进行一次赋值:

  • 1、 对象被创建时候 ,分配内存会把实例变量赋予默认值,这是肯定会发生的。
  • 2、 实例变量本身初始化的时候 ,就给他赋值一次,也就是int value1=100。
  • 3、 初始化代码块的时候 ,也赋值一次。
  • 4、 构造函数中 ,在进行赋值一次。

一共四次,看代码:

public class   Person3   {

public int value1 =  100 ;

{

value1 =  102 ;

System.out.println( "2" );

}

public Person3 () {

value1 =  103 ;

System.out.println( "3" );

}

}

参考

https://blog.csdn.net/justloveyou_/article/details/72466416

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1860

https://www.jianshu.com/p/8a14ed0ed1e9

Android 11 内置原生壁纸!速度来取

Android OkHttp 拦截器(修改url)

三年经验 Android 开发面经总结

B3Q7faa.jpg!mobile

如果你有写博客的好习惯

欢迎投稿


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK