前言
之前說了類載入的過程,但是有的讀者表示還是有些知識點沒弄清楚,相關面試題也不能思考出結果,所以今天就來總結下類載入、物件例項化
方面的知識點/面試題,幫助大家加深印象。
全是乾貨,一網打盡類的基礎知識
!先看看下面的問題都能回答上來嗎?
- 描述new一個物件的過程,並結合例子說明。
- 類初始化的觸發時機。
- 多執行緒進行類的初始化會出問題嗎?
- 類的例項化觸發時機。
<clinit>()
方法和<init>()
方法區別。- 在類都沒有初始化完畢之前,能直接進行例項化相應的物件嗎?
- 類的初始化過程與類的例項化過程的異同?
- 一個例項變數在物件初始化的過程中會被賦值幾次?
描述new一個物件的過程
先上圖,再描述:
Java
中物件的建立過程包括 類初始化和類例項化兩個階段。
而new
就是建立物件的一種方式,一種時機。
當執行到new
的位元組碼指令的時候,會先判斷這個類是否已經初始化,如果沒有初始化就要進行類的初始化,也就是執行類構造器<clinit>()
方法。
如果已經初始化了,就直接進行類物件的例項化。
類的初始化
,是類的生命週期中的一個階段,會為類中各個類成員賦初始值。類的例項化
,是指建立一個類的例項的過程。
但是在類的初始化之前,JVM
會保證類的裝載,連結(驗證、準備、解析)
四個階段都已經完成,也就是上面的第一張圖。
裝載
是指Java
虛擬機器查詢.class
檔案並生成位元組流,然後根據位元組流建立java.lang.Class
物件的過程。連結
是指驗證建立的類,並將其解析到JVM
中使之能夠被JVM
執行。
那到底類載入的時機是什麼時候呢?JVM
並沒有規範何時具體執行,不同虛擬機器的實現會有不同,常見有以下兩種情況:
隱式裝載
:在程式執行過程中,當碰到通過new
等方式生成物件時,系統會隱式呼叫ClassLoader
去裝載對應的 class 到記憶體中;顯示裝載
:在編寫原始碼時,主動呼叫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
總結一下執行流程
就是:
-
父類靜態變數和靜態程式碼塊;
-
子類靜態變數和靜態程式碼塊;
-
父類普通成員變數和普通程式碼塊;
-
父類的建構函式;
-
子類普通成員變數和普通程式碼塊;
-
子類的建構函式。
最後,大家再結合流程圖
好好梳理一下:
類初始化的觸發時機
在同一個類載入器下,一個型別只會被初始化一次,剛才說到new物件
是類初始化的一個判斷時機,其實一共有六種
能夠觸發類初始化的時機:
-
虛擬機器啟動時,初始化包含
main
方法的主類; -
遇到
new
等指令建立物件例項時,如果目標物件類沒有被初始化則進行初始化操作; -
當遇到訪問靜態方法或者靜態欄位的指令時,如果目標物件類沒有被初始化則進行初始化操作;
-
子類的初始化過程如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化;
-
使用反射
API
進行反射呼叫時,如果類沒有進行過初始化則需要先觸發其初始化; -
第一次呼叫
java.lang.invoke.MethodHandle
例項時,需要初始化MethodHandle
指向方法所在的類。
多執行緒進行類的初始化會出問題嗎
不會,<clinit>()
方法是阻塞的,在多執行緒環境下,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()
,其他執行緒都會被阻塞。
類的例項化觸發時機
- 使用
new
關鍵字建立物件 - 使用Class類的
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
拜拜
有一起學習的小夥伴可以關注下❤️ 我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。公眾號回覆111可獲得面試題《思考與解答》以往期刊。