深入浅出Java复用类【从字节码角度看toString调用机制、对象代理、组合与继承、转型、final、初始化】
這個世界上有10種人:一種是懂二進制的,一種是不懂二進制的
你覺得類是在什么時候被加載的?【訪問static域時,為什么?看完9就明白了】
文章目錄
- 1、深入理解Java中toString方法的調用機制
- 1.1.關于Java代碼層面的toString的調用機制
- 1.2.從字節碼角度剖析toString觀察toString方法的調用
- 1.3.為什么如果沒有重寫toString()方法就會打印類似的地址呢?
- 2、深入字節碼探究基類與導出類的構造初始化
- 2.1.如何理解繼承中基類與導出類之間的關系
- 2.2.如何理解基類與導出類的構造器初始化
- 2.3.從字節碼角度解讀基類與導出類的構造初始化
- 3、為什么有時要對對象進行代理(Delegation)?
- 3.1、解析第一種原因
- 3.2、解析第二種原因
- 4、為什么我們要手動清理一些對象?
- 5、如何理解組合與繼承
- 6、淺析向上轉型
- 7、再臨final關鍵字
- 7.1.使用final修飾數據什么含義、什么作用?
- 7.2.使用final修飾數據初始化的時機
- 7.3.final參數
- 7.4.使用final修飾方法的含義、擴展內容
- 7.5.使用final修飾類的隱含意義
- 8、在繼承中靜態、非靜態、構造函數的初始化順序
- 9、你覺得類是在什么時候被加載的?
1、深入理解Java中toString方法的調用機制
每一個非基本類型的對象都有一個toString()方法,當編譯器需要一個String對象而你卻只有一個對象時,便會調用toString()方法。
觀看以下代碼分析toString()方法的調用機制
class Emp {private String s = "hello";@Overridepublic String toString() {System.out.println("調用了toString方法");return s;}public static void main(String[] args) {Emp p = new Emp();System.out.println("Emp="+p);} }結果如下所示:
分析結果如下:
"Emp="+p
1.1.關于Java代碼層面的toString的調用機制
對于這行代碼,編譯器會將得知你想要將一個String對象(“Emp”)同p對象相加。由于只能將一個String對象和另外一個String對象相加,因此編譯器將會告訴你:"我將要調用toString()方法,將p對象轉換為一個String!"之后便可以將兩個String對象連接到一起并將其傳入到System.out.println().
1.2.從字節碼角度剖析toString觀察toString方法的調用
相信看到這你還是會有疑問,我怎么知道底層自動調用了toString()方法了呢?讓我從字節碼角度進行剖析 ,對上述代碼進行反解析
0 new #4 <com/zsh/javase/base/Emp>
3 dup
4 invokespecial #5 <com/zsh/javase/base/Emp. : ()V>
7 astore_1
8 getstatic #6 <java/lang/System.out : Ljava/io/PrintStream;>
11 aload_1
12 invokedynamic #7 <makeConcatWithConstants, BootstrapMethods #0>
17 invokevirtual #8 <java/io/PrintStream.println : (Ljava/lang/String;)V>
20 return
下面這一行表明調用了toString()方法(toString底層就是new了一個String對象)
一般來說系統了解過JVM才會看得懂這些字節碼指令,如果你剛接觸Java,你只需要記住.java編譯成.class文件時,底層自動調用了toString()方法即可。
new //代表創建一個Emp對象,并將引用壓入棧頂
dup //復制棧頂的引用,并將其壓入棧頂(此時你可能會有疑問,new指令之后棧頂已經有了一個地址,為什么還需要復制一份;其實一份是虛擬機要自動調用< init>方法做初始化使用的,另一份是給程序員使用的(對對象中的變量做賦值等操作,彈棧就沒了)
1.3.為什么如果沒有重寫toString()方法就會打印類似的地址呢?
當然會調用它的父類Object中的toString方法,看下面父類Object中toString方法的源碼會發現,它的返回剛好是類的全限定名+@+對象的哈希碼
2、深入字節碼探究基類與導出類的構造初始化
基類:父類基類:父類基類:父類
導出類:子類導出類:子類導出類:子類
2.1.如何理解繼承中基類與導出類之間的關系
我們都知道在Java中“一切皆對象”,從現實生活中舉例:比如“車”和“跑車🚓”,它們之間有什么特征聯系呢?
1."車“是一個寬泛的概念,“跑車”則是一個相對狹小的概念
2."車"擁有的特征而“跑車”一般都會擁有
所以當你創建了一個導出類對象時,該對象隱含了一個基類的子對象,這個子對象與你使用基類創建的對象的一致的。(只不過后者來自外部,而前者包裝在導出類對象的內部)
2.2.如何理解基類與導出類的構造器初始化
觀看以下代碼分析結果
public class ExtendTest {public static void main(String[] args) {Son son = new Son();} } class Parent {public Parent() {System.out.println("Parent..");} } class Son extends Parent {public Son() {System.out.println("Son");} }結果:
從這我們可以發現,這個構建過程是從基類開始向下擴散的,所以基類在導出類訪問它之前就已經完成了初始化,即使你不為它創建構造器,編譯器會自動地為你生成一個默認地構造器。
如果你要想要調用基類中有參的構造器就需要使用“super”關鍵字,如下面這樣:
public class ExtendTest {public static void main(String[] args) {Son son = new Son(11);} } class Parent {public Parent(int i) {System.out.println("Parent有參構造..");} } class Son extends Parent {public Son(int i) {super(11);System.out.println("Son有參構造..");} }
2.3.從字節碼角度解讀基類與導出類的構造初始化
class Parent {public Parent() {} }對上述代碼進行反編譯之后得到字節碼指令如下
0 aload_0
1 invokespecial #1 <java/lang/Object.< init> : ()V>
4 return
可以看出加載時它調用了基類Object的< init>空參方法。
3、為什么有時要對對象進行代理(Delegation)?
其實主要原因有以下:
1.保證單一職責
2.擁有更多的控制力
3.1、解析第一種原因
需要使每個類的功能盡可能的單一(單一職責),這樣對某個類進行修改時才能保證幾乎不影響其他類;比如有一個User類對象,我想要對該對象進行權限判斷,如果直接在User類中進行添加方法判斷是不是顯得很混亂(一旦這樣的類關聯很多,對一個類進行修改就會出現牽一發而動全身)
public class DelegationExample {/*** 測試Java中的代理(Delegation)* 模擬如下場景:* <p>一個賬戶類,現要對該賬戶做權限判斷,增加一個代理類</p>* 為什么要使用代理? 原因如下* 需要使每個類的功能盡可能的單一(單一職責),這樣對某個類進行修改時才能保證幾乎不影響其他類* @param args*/public static void main(String[] args) {User user = new User("zsh");UserDelegation delegation = new UserDelegation();delegation.setUser(user);delegation.judge();} } class User {private String name;User(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;} } class UserDelegation {private User user;public void setUser(User user) {this.user = user;}public void judge() {if (user.getName().equals("zsh")) {System.out.println("擁有該權限!");} else {System.out.println("無相應權限!");}} }3.2、解析第二種原因
基類中的方法全部暴露給了子類,看以下方法
class SpaceControl {void up(int vel){};void down(int vel){};void left(int vel){};void right(int vel){};void forward(int vel){};void back(int vel){}; } class SpaceShip extends SpaceControl{private String spaceName;public SpaceShip(String spaceName) {this.spaceName = spaceName;}public static void main(String[] args) {SpaceShip spaceShip = new SpaceShip("飛船一號");spaceShip.up(100);spaceShip.down(100);spaceShip.left(100);//...} }所以我們可以使用代理類只暴露部分方法給外部,然后也可也擴展一些操作。
class SpaceDelegation {private String name;private SpaceControl control = new SpaceControl();public SpaceDelegation(String name) {this.name = name;}public void up(int vel) {control.up(vel);}public void down(int vel) {if (vel < 0) {System.out.println("輸入錯誤");return;}control.down(vel);}public static void main(String[] args) {SpaceDelegation delegation = new SpaceDelegation("飛船一號");delegation.up(100);} }當然也可也添加判斷,例如上述代碼在down方法中對vel值做了校驗處理
4、為什么我們要手動清理一些對象?
5、如何理解組合與繼承
組合與繼承都是實現了在新的類中嵌套一個對象,只不過組合是顯示地這么做,而繼承是隱式地這么做;你或許想知道我如何在二者之間做選擇
組合技術通常在新類中使用現有類的功能,這樣一來新的類就是一個新的接口;而繼承則是延申出來的一個接口,本質上是對基類的擴展(具體化)
說白了組合就是組裝,嵌套的對象只是它的零件,比如”車子“和”輪胎“,你能說”輪胎“繼承與”車子“嘛,那肯定不可以,所以肯定是使用組合技術。
再比如”車子“和”跑車“,你能說”車子“組裝了”跑車“嘛!那肯定瞎扯嘛!
6、淺析向上轉型
分析以下代碼
class Instrument {public void play() {}static void tune(Instrument i) {i.play();} } class Wind extends Instrument {public static void main(String[] args) {Wind wind = new Wind();Instrument.tune(wind);} }在此例中,tune()方法可以接受Instrument的引用,然而我們卻傳入了Wind的引用。是不是很奇怪,其實你如果能夠想到導出類至少包含基類中所含有的方法(導出類中隱含了一個基類對象),那么就會很好理解,這樣的動作稱之為向上轉型。
Tinking in Java:在向上轉型的過程中,唯一可能發生的事情就是丟失一些方法,而不是獲取一些方法。這也就是為什么編譯器在”未明確表示轉型“,仍然能夠向上轉型的原因(注意如果基類定義方法為private,那表示是基類私有的)
7、再臨final關鍵字
顧名思義,它的含義是”這是無法改變的“
7.1.使用final修飾數據什么含義、什么作用?
使用final修飾數據,相當于向編譯器告知”這一塊數據是恒定不變的“。
當使用final修飾基本數據類型時,它表示是一個永不改變的常量;編譯器會將它代入到任何使用到它的計算式中(也就是直接替換為該常量值,因為你不能改變)
分析如下代碼
class Test {private final int NUM = 1;private int a = 2;public static void main(String[] args) {Test test = new Test();int b = test.NUM;int c = test.a;} }進行反編譯查看字節碼指令
找到執行int b = test.NUM的指令
iconst_1 : 將常量壓入操作數棧中。
istore_2:彈出操作數棧棧頂元素,將其保存到局部變量表為2的位置。
找尋int c = test.a的指令
aload_1:將局部變量表1號位置上的引用test加載到操作數棧中
getfield #3 : 獲取對象的實例域a
istore_3:彈出操作數棧棧頂元素,將其保存到局部變量表為3的位置
可以看出編譯器對final修飾的數據做了優化,可以直接代入計算式執行,不用再去根據引用去尋找。
當final修飾引用類型時,表示該引用一旦被初始化指向一個對象,就無法再指向另外一個對象。需要注意的是對象的內容是可以修改的(Thinking in Java:Java并未提供任何使對象恒定不變的途徑)
同樣的也適用于數組,數組也是對象
7.2.使用final修飾數據初始化的時機
必須在域的定義處或者構造器中對final修飾的變量進行賦值,這也是final域在使用前總是被初始化的原因所在。
class FinalKey {private final int NUM = 1;private final String OB;public FinalKey() {OB = "ob";} }7.3.final參數
如果將參數定義為final類型,這樣意味著你無法修改基本類型的參數、無法將引用類型的參數指向另外一個對象。
7.4.使用final修飾方法的含義、擴展內容
被我們熟知的就是將方法鎖定,不能被重寫。
另外一個作用是效率,在早期Java中,如果使用final修飾了方法,那么編譯器將針對該方法的所有調用都轉為內嵌調用(將整段代碼插入調用處)。這將消除調用開銷,但是如果代碼膨脹,那么這樣不會有任何性能提高。
在如今的HoSpot虛擬機中可以探測到這些情況,因此不需要再使用final進行優化了。
總結:所以說只有明確禁止覆蓋時,才將方法設置為final(另外類中的所有private方法都隱式地指定為final的,所以對private方法添加final關鍵字,沒有任何意義;
看下面的奇怪例子:
class TypeParent {private void method() {} } class TypeSon extends TypeParent {private void method() {} }上述不是已經說明private隱式地指定為final,那為什么上面這個例子還能覆蓋呢?其實這不是覆蓋,只是在子類中新定義了一個方法,與基類中的方法沒有任何聯系。
7.5.使用final修飾類的隱含意義
這個使用final修飾類,表示“永遠不需要做任何變動或者處于安全考慮”,比如String類”
另外final類的域或者方法都會隱式地指定為final,所以在final類中為域或方法添加final沒有任何意義。
8、在繼承中靜態、非靜態、構造函數的初始化順序
以下會涉及JVM的相關知識,如果不了解,記住即可!
類的加載過程中,分為三個階段
- 🌹1.loading階段
生成大Class對象,將靜態存儲結構轉換為方法區運行時的數據結構
- 🌹2.linking階段
(1)驗證:校驗字節碼文件信息是否符合虛擬機要求(防止篡改)
(2)準備:為類變量分配內存空間并初始化為默認值[0或者null],這里不包含用final修飾的static,因為final在編譯的時候會分配了,準備階段會顯式初始化
(3)解析:符號引用抓換位直接引用
- 🌹3.initization階段
執行類構造器< Clinit>的過程,注意這是類構造器,不是對象構造器< init>
這個Clinit方法不需要進行定義,它是javac編譯器自動收集類變量的賦值動作和靜態代碼塊合并而來的(總結一句話:執行類變量和靜態代碼塊的賦值語句)
所以在類加載階段的初始化順序為:
父類的靜態屬性默認初始化->子類的靜態屬性默認初始化->父類的靜態屬性顯示初始化->子類的靜態屬性顯示初始化(注意static final一塊修飾的變量在準備階段會進行顯示初始化操作)
最后就是對象層面的變量的初始化
父類成員變量初始化->子類成員變量初始化->父類構造器->子類構造器
累了,不想寫了
9、你覺得類是在什么時候被加載的?
構造器也是static方法,盡管沒有顯示出來。所以類是在任何static成員被訪問時加載的。
總結
以上是生活随笔為你收集整理的深入浅出Java复用类【从字节码角度看toString调用机制、对象代理、组合与继承、转型、final、初始化】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 实验1 最小生成树问题【Kruskal+
- 下一篇: 深入浅出在NIO技术中,如何理解直接缓冲