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