文章目錄
- 一、賦值順序
- (1)賦值的位置及順序
- (2)舉例
- (3)字節碼文件
- (4)進一步探索
- (5)最終賦值順序
- (6)實際開發如何選
- 二、(超綱)關于字節碼文件中的<init>
- 三、面試題
- (1)面試題1
- (2)面試題2
- (3)面試題3
- (4)面試題4
一、賦值順序
(1)賦值的位置及順序
- 可以給類的非靜態的屬性(即實例變量)賦值的位置有:
① 默認初始化
② 顯式初始化
⑤ 代碼塊中初始化
③ 構造器中初始化
#############################
④ 有了對象以后,通過"對象.屬性"或"對象.方法"的方法進行賦值
(造對象之前叫初始化,造對象之后叫賦值)
- 執行的先后順序:
① - ② - ③ - ④
⑤ 代碼塊中初始化應該放在哪?
(2)舉例
【舉例】
先看一段代碼:
package yuyi06;/*** ClassName: FieldTest* Package: yuyi06* Description:** @Author 雨翼輕塵* @Create 2023/11/19 0019 16:25*/
public class FieldTest {public static void main(String[] args) {Order o1=new Order();System.out.println(o1.orderId); //1}
}class Order{int orderId=1;}
輸出結果:
這個很簡單,顯而易見。
現在整一個代碼塊,看一下它和顯式賦值誰先誰后。
public class FieldTest {public static void main(String[] args) {Order o1=new Order();System.out.println(o1.orderId); //2}
}class Order{int orderId=1;{orderId=2;}}
輸出結果:
那么一定是先有1,后有2。所以代碼塊初始化肯定是在顯示初始化之后。
接下來是構造器和代碼塊。
創建一個空參構造器,那么在創建對象的時候一定會調用它。
ublic class FieldTest {public static void main(String[] args) {Order o1=new Order();System.out.println(o1.orderId); //3}
}class Order{int orderId=1;{orderId=2;}public Order(){orderId=3;}}
輸出結果:
結果是3,所以3將2覆蓋了。所以代碼塊初始化在構造器初始化前面。
所以目前來看,執行順序是這樣的:① - ② - ⑤ - ③ - ④
(3)字節碼文件
將光標放在Order類中,看一下字節碼文件。
插件在這里:
先運行然后重新編譯一下,確保生成的字節碼文件和代碼一致。
然后點擊這個即可:
構造器會以<init>
方法的方式呈現在字節碼文件中,如下:
看一下代碼:
方法里面對應的是個棧幀,棧幀里面會放局部變量,
aload_0
就是指局部變量第0個位置–this
,表示當前正在創建的對象,通過aload_0調用現在的方法。
如下:
畫個圖解釋一下Code的意思:
(4)進一步探索
根據上面得出來的結論,代碼塊賦值在顯式賦值之后,那么將它們倆的代碼換個位置呢?
如下:
public class FieldTest {public static void main(String[] args) {Order o1=new Order();System.out.println(o1.orderId); }
}class Order{{orderId=2;}int orderId=1;public Order(){//orderId=3;}}
輸出結果:
怎么是1了呢?肯定是先有2,后有1。
看字節碼文件:
這樣來看,代碼塊賦值又先于了顯示賦值。
剛才的① - ② - ⑤ - ③ - ④ 明顯不太對。
②和⑤就是看誰先聲明,誰就先執行。
所以應該是這樣的:① - ②/⑤ - ③ - ④
💬為啥將代碼塊寫在顯示賦值上面,不會報錯,這時候變量還未聲明啊?
其實這個地方一直有個誤區,舉個例子:
可以看到,在eat()
方法中可以調用sleep()
方法。
若是按照剛才的說法,先有eat(),后有sleep(),怎么一上來就可以sleep(),此時sleep()還沒有聲明啊,但是怎么沒有報錯?
我們只需要考慮,編譯的時候,會看到eat()里面調用了sleep()方法,這個方法找一下有沒有,發現有,那能確保調用sleep()的時候,內存中有嗎?
其實在加載類的時候(將類放入了方法區),其實sleep()也好,eat()也好,方法都加載了的。
所以只需要確保調用這個方法之前,這個方法加載了就行。
回到這里:
//代碼塊賦值
{orderId=2;
}//顯示賦值
int orderId=1;
現在這種情況也可以用類似的方式去解釋,以后再說類加載的詳細過程,現在就說最核心的點。
orderId
在整個類的加載中有一個過程,在其中某一個環節,就已經將orderId給加載了,而且還給了一個默認賦值0,這個時候orderId屬性就已經有了。在后續的環節中,才開始做顯示賦值和代碼塊的賦值。
現在是先有代碼塊的賦值,那么就將orderId改為2,后面又顯示賦值,將它改為1。
一般習慣將代碼塊寫顯示賦值的下面
(5)最終賦值順序
可以給類的非靜態的屬性(即實例變量)賦值的位置有:
① 默認初始化
② 顯式初始化 或 ⑤ 代碼塊中初始化
③ 構造器中初始化
#############################
④ 有了對象以后,通過"對象.屬性"或"對象.方法"的方法進行賦值
(造對象之前叫初始化,造對象之后叫賦值)
執行的先后順序:
① - ②/⑤ - ③ - ④
(6)實際開發如何選
💬 給實例變量賦值的位置很多,開發中如何選?
顯示賦值
:比較適合于每個對象的屬性值相同的場景。構造器中賦值
:比較適合于每個對象的屬性值不相同的場景(通過形參的方式給它賦值)。非靜態代碼塊
:用的比較少,在構造器里面基本能完成。靜態代碼塊
:靜態(與類相關)屬性不會選擇在構造器(與對象相關)中賦值。靜態的變量要么默認賦值,要么顯示賦值,要么代碼塊中賦值。
二、(超綱)關于字節碼文件中的
(超綱)關于字節碼文件中的的簡單說明(通過插件jclasslib bytecode viewer
查看)
剛才查看字節碼文件的時候,可以看到,這里做個簡單說明,便于大家理解。
🚗說明
①<init>
方法在字節碼文件中可以看到。每個方法都對應著一個類的構造器。(類中聲明了幾個構造器就會有幾個)
既然構造器和一 一對應,在字節碼文件中也看不到“構造器”這一項。
所以構造器就是以方法的形式呈現在字節碼文件中的。
比如這里聲明了倆構造器:
class Order{{orderId=2;}int orderId=1;public Order(){//orderId=3;}public Order(int orderId){this.orderId=orderId;}public void eat(){sleep();}public void sleep(){}}
看一下字節碼文件有兩個,如下:
點開第二個,一起來看一下它的Code:
角標為1的值:
所以通過第二個有參構造器去造對象的時候,也會有顯示賦值和代碼塊的執行,然后才是構造器。對應字節碼文件中就是方法。
②編寫的代碼中的構造器在編譯以后就會以<init>
方法的方式呈現。(方法和構造器不是一回事)
③<init>
方法內部的代碼包含了實例變量的顯示賦值、代碼塊中的賦值和構造器中的代碼。
④<init>
方法用來初始化當前創建的對象的信息的。
構造器和方法不是一回事,字節碼文件中沒有“構造器”,是以方法的形式呈現的。
三、面試題
(1)面試題1
下面代碼輸出結果是?
package yuyi06;//技巧:由父及子,靜態先行。class Root{//靜態代碼塊static{System.out.println("Root的靜態初始化塊");}//非靜態代碼塊{System.out.println("Root的普通初始化塊");}//構造器public Root(){super();System.out.println("Root的無參數的構造器");}
}class Mid extends Root{static{System.out.println("Mid的靜態初始化塊");}{System.out.println("Mid的普通初始化塊");}public Mid(){System.out.println("Mid的無參數的構造器");}public Mid(String msg){//通過this調用同一類中重載的構造器this();System.out.println("Mid的帶參數構造器,其參數值:"+ msg);}
}class Leaf extends Mid{static{System.out.println("Leaf的靜態初始化塊");}{System.out.println("Leaf的普通初始化塊");}public Leaf(){//通過super調用父類中有一個字符串參數的構造器super("雨翼輕塵");System.out.println("Leaf的構造器");}
}public class LeafTest{public static void main(String[] args){new Leaf(); //涉及到當前類,以及它的父類、父類的父類的加載包括相應功能的執行// System.out.println();// new Leaf();}
}
🤸分析
new Leaf();
涉及到當前類,以及它的父類、父類的父類的加載包括相應功能的執行。
分析先后執行的順序。
上面的類中,分別都有靜態代碼塊、非靜態代碼塊和構造器。
首先應該是靜態代碼塊
,進行類加載的時候,一定先加載父類的,然后才是子類。
之前說的方法的重寫,一定是先有父類的方法,才能覆蓋它。(先加載父類)
當我們通過leaf()
造對象,首先會通過super()
找到父類。(沒有寫也是super)
畫個圖看一下邏輯:
所以最先加載的類是Object
,只不過改不了代碼,也沒有輸出語句,
所以看似好像沒加載,其實是先加載它,其次是Root類,然后就是Root類里面的static代碼塊
,下面的非靜態代碼塊和無參構造器就別先執行了,因為要先將類的加載都執行了。
如下:
所以,看一下執行結果:(前面三行是“靜態初始化塊”)
類的加載
就完成了。
下面才涉及造對象。
靜態加載之后,先去new了一個leaf(),然后執行super(),一直到最上層的Root類,先考慮它的構造器的加載(涉及到非靜態結構的加載,然后才是子類),代碼塊的執行又早于構造器,所以會先輸出代碼塊中的內容。
剛才說到調用的過程如下:
輸出的話,就是反過來:
運行結果如下:
技巧:由父及子,靜態先行。(先加載父類,后加載子類,靜態結構早于非靜態(init方法)的,非靜態代碼塊的執行又早于構造器的執行)
方法包括代碼塊的,每個構造器都默認調用父類的構造器。
方法不是通過對象.
去調用的,而是自動執行的。
(2)面試題2
下面代碼輸出結果是?
class HelloA {public HelloA() {System.out.println("HelloA");}{System.out.println("I'm A Class");}static {System.out.println("static A");}
}class HelloB extends HelloA {public HelloB() {System.out.println("HelloB");}{System.out.println("I'm B Class");}static {System.out.println("static B");}}public class Test01 {public static void main(String[] args) {new HelloB();}
}
🤸分析
畫個圖演示一下:
執行輸出順序:①->②->③->④->⑤->⑥
先將類的加載搞定。
HelloA中,有靜態先調用靜態,輸出“static A”,
然后回到HelloA中,調用靜態,輸出“static B”。
然后考慮當前要創建的對象的構造器HelloB(),此時第一行會調用super(),
調用HelloA()構造器。
再HelloA()構造器中,有非靜態代碼塊,先執行它,輸出“I’m A Class”,
然后輸出構造器中“HelloA”。
super()執行結束之后,回到HelloB(),此時HelloB類中也有非靜態代碼塊,
所以先輸出代碼塊中“I’m B Class”,最后輸出HelloB()構造器中“HelloB”。
👻代碼運行結果
(3)面試題3
下面代碼輸出結果是?
public class Test02 {static int x, y, z;static {int x = 5;x--;}static {x--;}public static void method() {y = z++ + ++z;}public static void main(String[] args) {System.out.println("x=" + x);z--;method();System.out.println("result:" + (z + y + ++z));}
}
🤸分析
畫個圖:(執行順序:①->②->③->④->⑤->⑥)
👻輸出結果:
(4)面試題4
下面代碼輸出結果是?
public class Test03 {public static void main(String[] args) {Sub s = new Sub();}
}
class Base{Base(){method(100);}{System.out.println("base");}public void method(int i){System.out.println("base : " + i);}
}
class Sub extends Base{Sub(){super.method(70);}{System.out.println("sub");}public void method(int j){System.out.println("sub : " + j);}
}
🤸分析
畫個圖:(執行順序:①->②->③->④->⑤->⑥->⑦->⑧)
🚗調試
大家也可以自行調試,這里就做個示范。
👻輸出結果: