Java對象的創建,持有Java對象,區分對象和類。

首先弄清楚兩個關鍵字:
static靜態。可以修飾內部類,成員變量,方法,代碼塊。static和實例化是互斥的,因爲他表示獨一份。不管是內部類,變量,方法,還是代碼塊,都只存在一份於Java虛擬機中,不依附對象的實例化而存在。(代碼塊的意義是隻執行一次,不是說存在於虛擬機中)
static修飾的方法,不能夠被重寫(final方法也有這個效果)。同時其可以在不創建實例時直接使用。
final不可修改。final在修飾原始數據類型時即表示這個類型是個常量了,不會變。在修飾其它類型的變量時,實際上是表示這個變量的引用是final的。引用不能再指向別的地址,變量本身的內部數據還是可變的。final修飾的變量必須提供初始化,要麼在定義時就賦值要麼就在構造函數中。
final修飾方法時,該方法不能被重寫(static方法也有這個效果)。編譯器對待final方法的時候不一樣會提供運行效率,這是因爲final方法不需要去進行動態綁定,編譯器就可以爲其生成更高效的代碼。
final還可以用在方法頭中修飾參數f(final Map m),這時候在方法體內不能出現影響m的final性質的代碼。
final還可以修飾類和內部類,如果一個類是final的,那麼這個類不能被繼承,然後類裏的方法自然也默認都是final的。


接下來簡單瞭解Jvm存儲模式的基本知識點,引用Thinking in Java中的說法:

  1. 寄存器–Registers。又稱程序計數器。CPU的內部的存儲,這是最快的存儲位置,但是數量十分的有限,在java中,寄存器是編譯器根據需要來進行分配的,我們並沒有直接的控制權。
  2. 棧–The Stack。堆棧屬於常規RAM區域。處理器可以通過“堆棧指針”來直接直接支持。堆棧指針若向下移,會創建新的內存;若向上移,則會釋放那些內存。這是一種特別快、特別有效的數據保存方式,僅次於寄存器。 在java中,如果需要上移或者下移指針,就需要準確知道所有數據的長度和存在時間。這限制了靈活性,所以java【不會把對象】創建到棧中。而【對象的引用】是要放在棧中的,當然還有其他一些東西(各種基本數據類型)。
  3. 堆(內存堆)–The Heap。一種常規用途的內存池(也在RAM 區域),其中保存了【Java 對象】。和棧不同,Heap最吸引人的地方在於編譯器不必知道要從堆裏分配多少存儲空間,也不必知道存儲的數據要在堆裏停留多長的時間。因此,用堆保存數據時會得到更大的靈活性。當然,爲達到這種靈活性,必然會付出一定的代價:在堆裏分配存儲空間時會花掉更長的時間!
  4. 靜態存儲(java方法區). 也是在RAM區域,但是是“位於固定位置”。類class的字節碼加載的信息,包括類中的靜態成員數據放置在該區域。將隨時等候調用。這就是文章開頭所說的用static修飾的內容,隨時等候調用,所以是不依賴實例化對象存在的。Java 對象本身永遠都不會置入靜態存儲空間。
  5. 常數存儲–Constant storage.(也是方法區的一部分) 永遠不會改變,通常置於程序內部。PS:thinking in java裏的描述很費解。 更適當的說法應該是常量池:用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。運行時常量池除了常量池的內容外,還可以動態性的放入常量如String.intern()的使用。
  6. 非RAM存儲,即非內存形式,可能會成爲流式對象發送給別人,或者存儲在磁盤。

再說說class。java.lang.Class是一個特殊的對象。我們編譯後的程序都是.class。Class的目的是建立起一個對象的類型,用來決定一個類的外觀與行爲。任何一個類(包括內部類),都有一個Class對象對應於這個類。在這個Class對象中保存着實例化該類所需要的基本信息。


瞭解一個對象的創建過程,是進階Java技巧的必要知識,這裏只是最簡單的描述:只有在用到一個對象,一個方法時,纔會啓動和創建有關的一系列操作。第一步就是去加載。

  1. 加載一個類所做的,是把.class文件以二進制讀入內存,在Heap中創建一個java.lang.Class。創建的Class對象是單例的,無論是這個類有多少個對象,Class對象是唯一的。
  2. 第二步連接,執行類中的static部分,或爲靜態變量分配內存,併爲靜態變量分配默認值(注意不是代碼裏的賦值)。連接又可以細分爲驗證、準備、解析三步,不在此展開。
  3. 第三步初始化,執行靜態代碼塊,併爲靜態變量初始化成代碼裏賦的正確值。至此-可以認爲是完成了的初始化。
  4. 第四步,在這裏開始開始進行對象的初始化。首先初始化成員變量,隨後再調用構造函數。

    需要注意的問題:

    1.在繼承體系中,首先是執行父類的類初始化,接着是子類的類初始化,接着是父類的對象初始化,再是子類的對象初始化。
    2.如果父類的構造函數,是一個帶有參數的構造函數。那麼子類必須實現一個構造函數並使用super(參數)作爲第一句,這就是爲了保證正確的初始化過程。
    3.內部類不會隨外部類的加載而加載,而是在要用到內部類的時候才加載。見使用內部類實現延遲加載的單例模式。
    4.有一個費解的問題,如果一個類繼承一個靜態內部類,那麼會發生什麼?首先,靜態內部類,沒有說不能用new來創建自己的對象,也是可以的。只是說不需要。其次,若他被繼承了,也就自然的遵循繼承體系的初始化過程了。但是,實際中我們很少會寫這麼“高級”的代碼吧?
    5.如果爲一個類編寫了構造函數,那麼就不存在默認構造函數了。構造函數裏調用別的構造函數使用this(),而且,這必須是第一句。目的也是保證在正確初始化前你不能做別的事。
    6.如果有人問你爲什麼可以實現upcasting,上面的流程就保證了子類肯定可以作爲一個父類的引用來使用。


接下來再總結一下如何持有對象。
再來看看class,有一句很好的說法“通過class可以找到一個類的祖宗十八代”。通過一個class,能(和反射相關)拿到其實現的所有接口,其父類,其方法,其構造函數,其成員變量,所以我們通過class能夠創建並持有對象。產生和new一樣的效果。工廠模式中就有很多這樣的創建方式。
只要能獲得一個Class對象,就可以用newInstance()來創建一個對象。獲得class的方法包括Class.forName(),類名.Class,對象.getClass()。

  1. 類名.Class,只做第一步加載。
  2. Class.forName(),會執行類的加載和初始化。
  3. 對象.getClass(),自不必說,連對象都已經存在了。
  4. 類名.this又是啥意思呢?一般看到的ClassName.this是因爲在內部類中要使用外部類本身,就使用外部類名.this。顯然這個this不會出現在內部類的靜態方法中。

instanceof和isInstance(),都是看一個對象是否是一個class的實例,isAssignableFrom是兩個class之間的比較,比較一個Class是否是自己或自己的子類,而不是操作實例對象。
isInterface判斷一個class是否是接口。

創建對象的另一個方式是clone(),clone的前提是已經存在一個實例對象了。關於clone()方法要理解和注意:一個類要想clone,必須implements Cloneable,否則會拋出CloneNotSupportedException。 而因爲有Object.clone()他可以不用重寫。但是這樣是沒什麼意義的。因爲Object.clone()是protected,只能在繼承他的對象內部使用。另外一個原因就是淺層複製。所以在使用clone時:1.實現cloneable接口。2.重寫clone()方法包括內部成員的clone方法從而完成深層複製。3.幾乎肯定要使用super.clone()作爲重寫方法的第一句。

一定要注意super.clone()的意思只是調用父類的一個clone方法(總會調用成功,因爲最上層的父類Object肯定有這個方法),僅此而已,絕對沒有說這個方法就是隻clone父類那部分。事實上,object.clone所做的事情是按位複製完整的這個繼承對象,也就是在哪個類的clone方法內使用super.clone實際上clone的就是這個類的對象。
ps:序列化也可以實現複製,但是,效率比clone低很多。


關於ClassLoader,JVM加載類到java虛擬機,使用的都是ClassLoader。JVM自己已經實現了三種ClassLoader分別是:Bootstrap(底層native代碼實現),Extension,System。依次對應加載路徑爲:/lib,< Java_Runtime_Home >/lib/ext,CLASSPATH。ClassLoader使用的是雙親委託模式工作:
雙親委派機制描述 (ps:雙親不是指有兩個家長…)
某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
每一個被加載的類都有一個加載他的ClassLoader的引用通過getClassLoder獲取,也可以使用setClassLoader來設置自定義的類加載器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章