鄙人為興趣愛好,0基礎入門學習Java,有些心得想法,記錄於此,與君分享。 然畢竟新手,學識尚淺,錯誤之處,希望多多指正批評,也是對我最大的幫助! 前言:本篇文章,主要討論在子類繼承父類之後,各屬性成員和方法的特征規律和差異,this和super的異同,以及一些繼承在記憶體中構建的過程。 文章內所有內 ...
鄙人為興趣愛好,0基礎入門學習Java,有些心得想法,記錄於此,與君分享。
然畢竟新手,學識尚淺,錯誤之處,希望多多指正批評,也是對我最大的幫助!
前言:本篇文章,主要討論在子類繼承父類之後,一些繼承在記憶體中構建的過程,以及this和super的特點和異同。
文章內所有內容均為個人猜測和想法,不代表任何學科結論。
一、我是孫子!
既然有孫子,那肯定是指三代傳承,所以,我準備了三個類,A是爺爺,B是爸爸,C是孫子(也就是我,一下均以孫子代替)。代碼如下:
類A(爺爺):
1 public class A { 2 //屬性部分 3 public int i_A; 4 public String str_A = "我是類A里str屬性的初始值"; 5 //一般方法部分 6 public void function (){ 7 System.out.println("我是類A里的“function”方法,我不接受參數"); 8 } 9 public void function_A(){ 10 System.out.println("我是類A里的“function_A”方法,我不接收參數"); 11 } 12 public void function_A(int n){ 13 System.out.println("我是類A里的“function_A”方法,我接收一個值為"+n+"的int類型參數"); 14 } 15 //構造方法部分 16 public A(){ 17 System.out.println("我是類A的無參數構造方法"); 18 } 19 public A(int n){ 20 System.out.println("我是類A的帶參數構造方法,我接收一個值為"+n+"的int類型參數"); 21 } 22 //代碼塊部分 23 { 24 System.out.println("我是類A的代碼塊,我受過嚴格的訓練"); 25 } 26 }Class A
類B(爸爸):
1 public class B extends A { 2 //屬性部分 3 public int i_B; 4 public String str_B = "我是類B里str屬性的初始值"; 5 //一般方法部分 6 public void function (){//父類A里一般方法的重寫 7 System.out.println("我是類B里的“function”方法,是對父類A方法的重寫,我不接受參數"); 8 } 9 public void function_B(){ 10 System.out.println("我是類B里的“function_B”方法,我不接收參數"); 11 } 12 public void function_B(int n){ 13 System.out.println("我是類B里的“function_B”方法,我接收一個值為"+n+"的int類型參數"); 14 } 15 //構造方法部分 16 public B(){ 17 System.out.println("我是類B的無參數構造方法"); 18 } 19 public B(int n){ 20 System.out.println("我是類B的帶參數構造方法,我接收一個值為"+n+"的int類型參數"); 21 } 22 //代碼塊部分 23 { 24 System.out.println("我是類B的代碼塊,無論多好笑,我都不會笑"); 25 } 26 }Class B
類C(孫子):
public class C extends B{ //屬性部分 public int i_C; public String str_C = "我是類C里str屬性的初始值"; //一般方法部分 public void function (){//父類A里一般方法的重寫 System.out.println("我是類C里的“function”方法,是對父類B方法的重寫,我不接受參數"); } public void function_C(){ System.out.println("我是類C里的“function_C”方法,我不接收參數"); } public void function_C(int n){ System.out.println("我是類C里的“function_C”方法,我接收一個值為"+n+"的int類型參數"); } //構造方法部分 public C(){ System.out.println("我是類C的無參數構造方法"); } public C(int n){ System.out.println("我是類C的帶參數構造方法,我接收一個值為"+n+"的int類型參數"); } //代碼塊部分 { System.out.println("我是類C的代碼塊,除非忍不住"); } }Class C
準備好了三個類,那我就要開始生成一個孫子。首先,我們不帶參數new一個C類對象c_new,在main中的代碼如下:
1 public class Relation {
2 public static void main(String[] args){
3 C new_c = new C();
4 }
5 }
main方法
執行結果如下圖:
分析一:
通過結果來看
第一、給我的直觀感受是,java在new一個孫子對象的過程中,最先是new了一個爺爺類,再在爺爺類的後面new了一個爸爸類,最後在爸爸類的後面,new了一個孫子類。又由於object是所有類的父類,object也是爺爺類的父類,所以爺爺類其實是new在object類之後。
第二、我認為,所有的“new”操作,一定都是在記憶體中開闢地址連續的空間(如果new出來的空間不連續,我估計整個java體系就會坍塌),而能100%保證正確申請到所需空間的步驟應該都是:先確定大小,再開闢空間,這個開闢的過程我覺得應該是JVM做的,而且開闢的空間必須等於或者大於所需要的空間。(我要是設計者,我會設計會大於所需的空間,多餘的空間可以存儲一些代碼信息,用於其他的用途,比如底層的一些保護或者回收機制)
所以,我產生了一個想法:
這個開闢空間的過程並非動態的(先開闢A大小空間,再接著記憶體地址開闢B大小空間,再……),而是在編譯的過程中,JVM會把A extends Object(隱式) 、B extends A、C extends B這個關係額外記錄成一條信息,這條信息告訴電腦,如果我需要生成一個C類對象,你需要給我對應大小空間的連續記憶體塊。於是,在得到指令需要生成一個C類對象後,記憶體會被開闢出一個能裝得下從object到C類所有大小的地址連續的記憶體空間。
但是又會存在一個問題,如果我把main中“ C new_c = new C(); ”這條語句註釋掉,再編譯執行,能通過,既然main裡面什麼都沒做也可以執行通過,那此時ABC和object這四個類到底在不在記憶體中呢?還是說只有new出現了,才會產生我上述的編譯過程?
為了證明這一點,我在main()里設計了一個while(true)的方法,可以無限選擇生成三種類中的任意一種類對象,我發現,在已經編譯後的程式執行過程中,我可以隨意選擇生成A、B或者C類的對象,這說明,類A,B,C甚至object肯定還是在記憶體中存在的,但是存在的空間是否連續,我無法確定,而且沒有一個具體的媒介可以用到他們,而且我猜想,很大可能性,這個記憶體狀態下,各個類所占用的空間也僅僅是類裡面代碼描述內容所需占用的記憶體空間,並不是像生成的具體對象那樣的記憶體空間。
這個確定存在的推論讓我又得到一條信息,new的過程肯定都是複製的某塊記憶體的數據,而這塊記憶體正好是所需要生成對象的類存在的記憶體塊,不然怎麼可能在不操作記憶體數據的情況下,把可能不連續的記憶體塊,變成一定連續的記憶體塊呢?(因為我覺得如果通過操作記憶體數據來使記憶體連續實在太費力了,不符合java的特性)。
綜上所述,以我的代碼為例,我覺得整個 “C new_c = new C();” 的過程應該是下麵這個步驟:
1)記憶體中已經有幾塊區域存放了類Object、類A、類B和類C的主體。假如這四個類所占空間分別是50位元組,100位元組,100位元組,100位元組。合計:350位元組。
2)在記憶體的另外一塊足夠大的地方,開闢出一個350位元組的連續地址空間,從object類到C類,依次將其記憶體的內容賦值到這塊新的記憶體中,這塊新記憶體是連續的,並產生一個代表這塊記憶體的地址值。
3)將這個地址值,存到另外一塊名為c_new的記憶體里。
至此,我覺得整個new的過程就完成了,以後對於c_new的操作,都是在c_new的值指向的新記憶體裡面進行的反覆讀寫與計算。
分析二:
通過執行順序來看:
眾所周知,構造一個類的具體對象是通過類裡面的構造方法實現的,那麼我們按照這個原則,開始從C();分析代碼的執行順序。
進入C();第一行,直接就是輸出字元串 "我是類C的無參數構造方法" ,這和結果完全不一樣。從教程中,我瞭解到,其實構造函數的第一行,再未顯式調用的情況下,永遠是隱式調用了一個方法,super(),即調用當前類的父類的構造方法。所以我寫的構造函數,實際上是這樣的:
public C(){ super(); System.out.println("我是類C的無參數構造方法"); }C();
public C(int n){ super(); System.out.println("我是類C的帶參數構造方法,我接收一個值為"+n+"的int類型參數"); }C(int n);
依次類推,B和A類的構造方法應該均是如此。所以,整個執行過程應該是C();→B();→A();→object();,這是幾個方法的嵌套使用(object有沒有構造函數我不確定,目前只討論ABC類,姑且這樣寫吧),在object();執行完畢之後,要執行的理應是A();里的下一條語句,System.out.plintln();,但是,結果顯示的卻是先執行了代碼塊的部分,而且,B、C類也是如此,我覺得這應該是java設計的一種類的規則,並不是某種機制產生的客觀結果(也可能是,只是我還不懂),而且代碼塊的調用是在super()和輸出之間發生,只有這種解釋,才能將輸出結果與構造過程一一對應。可以new一個帶參數的C類對象佐證。
public class Relation { public static void main(String[] args){ C new_c = new C(5);//帶參數 } }main()
結果如下:
完美!
二、我到底是誰!
是的,我又變成了this,我是一個指代關鍵字,在類的代碼裡面,指代當前類的具體對象,用來調用當前類的各種屬性和方法。比如,在類C裡面,我是代表類C的對象,在類B裡面,我代表B的對象,在A類里,我代表A的對象。。。但是,真的如此嗎?我到底是誰?
為了查明真想,首先,我們在C類的function_C方法裡加入一行代碼,
public void function_C(){ this.function(); System.out.println("我是類C里的“function_C”方法,我不接收參數"); }function_C()
通過main里生成的new_c調用function_C方法,執行後結果如下
function_C方法正確調用到了this所在C類的function方法。
但是,如果在B類的function_B方法裡加入調用方法,那this到底調用的哪一個function方法?讓我來測試一下。
1 public void function_B(){ 2 this.function(); 3 System.out.println("我是類B里的“function_B”方法,我不接收參數"); 4 }function_b()
結果如下
沒錯,function_B里的this.function()調用的卻是C類的function方法。如果在A類裡面測試,結果一樣。
看上去,this並不是嚴格指代當前類的對象,起碼在父類里通過this調用重寫的方法時不符合這個說法。那麼如果測試的重寫的方法,而是重寫的屬性(這個說法可能並不專業和準確)呢?在三個類裡面設置一個同名同類型的變數,經過同上步驟的測試,this指代的又變成了當前類的對象。
考慮到function方法實際上是子類對父類的重寫,情況可能有些特殊,所以關於this的指代性,可以做一個一般歸納:
在繼承中,this確實在一般情況下,指代的是一個當前類的對象,但是如果在父類或者父父類(父類的父類)中通過this調用重寫的方法(同名同參數),那麼實際調用的就是new出來的具體對象的重寫方法,不會調用到父類,或者父父類。(這個結論的佐證就是,在父類中輸入this.後,IDE不會提示到子類的各種屬性和方法,除了重寫的方法)
接下來,我們討論一下this();,通過this調用構造方法。
首先,我們在C類的C(int n)方法里,第一行加入this(),然後帶參數運行,結果如下:
通過追蹤代碼的執行順序,我們不難得出一個結論,this()把原來的隱藏的super()真正的屏蔽了起來,而如果顯式調用super(),編譯不會通過,這說明,this()和super()不能同時存在。其實這也很好理解,如果同時存在,那這個類的生成後就會非常混亂。所以這也應該是Java設定的特性。
三、我就是你爸爸!
super就是爸爸,他比this的理解簡單多了,在任何情況下,super都指代當前類的父類的對象,沒有其他特例。就算是在父類里寫super.function(),調用的也是父父類的function()方法,測試如下:
在B類的function_B方法的程式塊第一行寫super.function();,然後main中用生成的C類對象調用到function_B查看結果。
1 public void function_B(){ 2 super.function(); 3 System.out.println("我是類B里的“function_B”方法,我不接收參數"); 4 }function_B()
結果如下:
從結果不難看出,即使實際調用的是C類對象,但是super是寫在B類的function_B方法里的,所以會找到B類的父類里的對應的方法或者屬性。
而super()就更不用討論什麼了,因為在任何子類的構造函數裡面的第一行預設都是super(),不管顯式或者隱式,他都必須在那裡。
this()和super()的共同點就是:
①必須在構造函數里存在。在其他位置沒有實際意義,編譯也不會通過。
②都必須在構造函數的第一行。作為用來生成對象的方法,肯定是先有了對象你才能使用對象。
③由於第二條的存在,那麼this()和super()肯定不能同時存在。如果構造函數內是this(),那編譯器應該會預設註釋掉隱式的super()。
四、結尾
到這裡,已經寫完了我所理解的繼承時記憶體構建、this和super特性的大部分內容,其實這裡面應該有很多不正確的地方或者沒有考慮到的地方,但是作為心得體會,留下記錄,可便於日後重新審視,發現自己的不足,從而提升自己,更歡迎歡迎大家多多批評指正,讓我更快速的進步!