摘要:也就是說,一個實例變量,在的對象初始化過程中,最多可以被初始化次。當所有必要的類都已經裝載結束,開始執行方法體,并用創建對象。對子類成員數據按照它們聲明的順序初始化,執行子類構造函數的其余部分。
類的拷貝和構造
C++是默認具有拷貝語義的,對于沒有拷貝運算符和拷貝構造函數的類,可以直接進行二進制拷貝,但是Java并不天生支持深拷貝,它的拷貝只是拷貝在堆上的地址,不同的變量引用的是堆上的同一個對象,那最初的對象是怎么被構建出來的呢?
Java對象的創建過程關于對象的創建過程一般是從new指令(我說的是JVM的層面)開始的(具體請看圖1),JVM首先對符號引用進行解析,如果找不到對應的符號引用,那么這個類還沒有被加載,因此JVM便會進行類加載過程(具體加載過程可參見我的另一篇博文)。符號引用解析完畢之后,JVM會為對象在堆中分配內存,HotSpot虛擬機實現的JAVA對象包括三個部分:對象頭、實例字段和對齊填充字段(對齊不一定),其中要注意的是,實例字段包括自身定義的和從父類繼承下來的(即使父類的實例字段被子類覆蓋或者被private修飾,都照樣為其分配內存)。相信很多人在剛接觸面向對象語言時,總把繼承看成簡單的“復制”,這其實是完全錯誤的。JAVA中的繼承僅僅是類之間的一種邏輯關系(具體如何保存記錄這種邏輯關系,則設計到Class文件格式的知識),唯有創建對象時的實例字段,可以簡單的看成“復制”。
為對象分配完堆內存之后,JVM會將該內存(除了對象頭區域)進行零值初始化,這也就解釋了為什么JAVA的屬性字段無需顯示初始化就可以被使用,而方法的局部變量卻必須要顯示初始化后才可以訪問。最后,JVM會調用對象的構造函數,當然,調用順序會一直上溯到Object類。
Java對象的初始化初始化的順序是父類的實例變量構造、初始化->父類構造函數->子類的實例變量構造、初始化->子類的構造函數。對于靜態變量、靜態初始化塊、變量、初始化塊、構造器,它們的初始化順序依次是(靜態變量、靜態初始化塊)>(變量、初始化塊)>構造器。
JVM在為一個對象分配完內存之后,會給每一個實例變量賦予默認值,這個時候實例變量被第一次賦值,這個賦值過程是沒有辦法避免的。如果我們在實例變量初始化器中對某個實例x變量做了初始化操作,那么這個時候,這個實例變量就被第二次賦值了。 如果我們在實例初始化器中,又對變量x做了初始化操作,那么這個時候,這個實例變量就被第三次賦值了。如果我們在類的構造函數中,也對變量x做了初始化操作,那么這個時候,變量x就被第四次賦值。也就是說,一個實例變量,在Java的對象初始化過程中,最多可以被初始化4次。
下面還是舉一個例子吧
class Parent { /* 靜態變量 */ public static String p_StaticField = "父類--靜態變量"; /* 變量 */ public String p_Field = "父類--變量"; protected int i = 9; protected int j = 0; /* 靜態初始化塊 */ static { System.out.println( p_StaticField ); System.out.println( "父類--靜態初始化塊" ); } /* 初始化塊 */ { System.out.println( p_Field ); System.out.println( "父類--初始化塊" ); } /* 構造器 */ public Parent() { System.out.println( "父類--構造器" ); System.out.println( "i=" + i + ", j=" + j ); j = 20; } } public class SubClass extends Parent { /* 靜態變量 */ public static String s_StaticField = "子類--靜態變量"; /* 變量 */ public String s_Field = "子類--變量"; /* 靜態初始化塊 */ static { System.out.println( s_StaticField ); System.out.println( "子類--靜態初始化塊" ); } /* 初始化塊 */ { System.out.println( s_Field ); System.out.println( "子類--初始化塊" ); } /* 構造器 */ public SubClass() { System.out.println( "子類--構造器" ); System.out.println( "i=" + i + ",j=" + j ); } /* 程序入口 */ public static void main( String[] args ) { System.out.println( "子類main方法" ); new SubClass(); } }
上面的初始化結果是:
父類--靜態變量
父類--靜態初始化塊
子類--靜態變量
子類--靜態初始化塊
子類main方法
父類--變量
父類--初始化塊
父類--構造器
i=9, j=0
子類--變量
子類--初始化塊
子類--構造器
i=9,j=20
子類的靜態變量和靜態初始化塊的初始化是在父類的變量、初始化塊和構造器初始化之前就完成了。靜態變量、靜態初始化塊,變量、初始化塊初始化了順序取決于它們在類中出現的先后順序。
分析:
訪問SubClass.main(),(這是一個static方法),于是裝載器就會為你尋找已經編譯的SubClass類的代碼(也就是SubClass.class文件)。在裝載的過程中,裝載器注意到它有一個基類(也就是extends所要表示的意思),于是它再裝載基類。不管你創不創建基類對象,這個過程總會發生。如果基類還有基類,那么第二個基類也會被裝載,依此類推。
執行根基類的static初始化,然后是下一個派生類的static初始化,依此類推。這個順序非常重要,因為派生類的“static初始化”有可能要依賴基類成員的正確初始化。
當所有必要的類都已經裝載結束,開始執行main()方法體,并用new SubClass()創建對象。
類SubClass存在父類,則調用父類的構造函數,你可以使用super來指定調用哪個構造函數。基類的構造過程以及構造順序,同派生類的相同。首先基類中各個變量按照字面順序進行初始化,然后執行基類的構造函數的其余部分。
對子類成員數據按照它們聲明的順序初始化,執行子類構造函數的其余部分。
靜態變量初始化器和靜態初始化器基本同實例變量初始化器和實例初始化器相同,也有相同的限制(按照編碼順序被執行,不能引用后定義和初始化的類變量)。靜態變量初始化器和靜態初始化器中的代碼會被編譯器放到一個名為static的方法中(static是Java語言的關鍵字,因此不能被用作方法名,但是JVM卻沒有這個限制),在類被第一次使用時,這個static方法就會被執行。
Java對象的引用方式接下來我們再問一個問題,Java是怎么通過引用找到對象的呢?
至此,一個對象就被創建完畢,此時,一般會有一個引用指向這個對象。在JAVA中,存在兩種數據類型,一種就是諸如int、double等基本類型,另一種就是引用類型,比如類、接口、內部類、枚舉類、數組類型的引用等。引用的實現方式一般有兩種,具體請看圖3。此處說一句題外話,經常用人拿C++中的引用和JAVA的引用作對比,其實他們兩個只是“名稱”一樣,本質并沒什么關系,C++中的引用只是給現存變量起了一個別名(引用變量只是一個符號引用而已,編譯器并不會給引用分配新的內存),而JAVA中的引用變量卻是真真正正的變量,具有自己的內存空間,只是不同的引用變量可以“指向”同一個對象而已。因此,如果要拿C++和JAVA引用對象的方式相對比,C++中的指針倒和JAVA中的引用如出一轍,畢竟,JAVA中的引用其實就是對指針的封裝。
關于對象引用更深層次的問題,我們將在JVM篇章中詳細解釋。
匿名類、內部類和靜態類這一部分的內容相當寬泛,詳細的可以查閱下面的參考文章,我在這里主要強調幾個問題:
內部類的訪問權限(它對外部類的訪問權限和外部對它的訪問權限)
成員內部類為什么不能有靜態變量和靜態函數(final修飾的除外)
內部類和靜態內部類(嵌套內部類)的區別
局部內部類使用的形參為什么必須是final的
匿名內部類無法具有構造函數,怎么做初始化操作
內部類的繼承問題(由于它必須和外部類實例相關聯)
在這里只回答一下最后一個問題,由于成員內部類的實現其實是其構造函數的參數添加了外部類實體,所以內部類的實例化必須有外部類,但就類定義來說,內部類的定義只和外部類定義有關,代碼如下
public class Out { private static int a; private int b; public class Inner { public void print() { System.out.println(a); System.out.println(b); } } } // 內部類實例化 Out out = new Out(); Out.Inner inner = out.new Inner(); public class InheritInner extends Out.Inner { InheritInner(Out out){ out.super(); } }
最后關于內部類的實現原理,請閱讀參考文章中的《內部類的簡單實現原理》,這非常重要
Java多態的實現原理Java的多態主要有以下幾種形式:
繼承
覆蓋
接口
方法調用的原理多態是面向對象編程語言的重要特性,它允許基類的指針或引用指向派生類的對象,而在具體訪問時實現方法的動態綁定。Java 對于方法調用動態綁定的實現主要依賴于方法表,但通過類引用調用(invokevitual)和接口引用調用(invokeinterface)的實現則有所不同。
類引用調用的大致過程為:Java編譯器將Java源代碼編譯成class文件,在編譯過程中,會根據靜態類型將調用的符號引用寫到class文件中。在執行時,JVM根據class文件找到調用方法的符號引用,然后在靜態類型的方法表中找到偏移量,然后根據this指針確定對象的實際類型,使用實際類型的方法表,偏移量跟靜態類型中方法表的偏移量一樣,如果在實際類型的方法表中找到該方法,則直接調用,否則,認為沒有重寫父類該方法。按照繼承關系從下往上搜索。
方法表是實現動態調用的核心。方法表存放在方法區中的類型信息中。為了優化對象調用方法的速度,方法區的類型信息會增加一個指針,該指針指向一個記錄該類方法的方法表,方法表中的每一個項都是對應方法的指針。這些方法中包括從父類繼承的所有方法以及自身重寫(override)的方法。
Java 的方法調用有兩類:
動態方法調用:動態方法調用需要有方法調用所作用的對象,是動態綁定的。
靜態方法調用:靜態方法調用是指對于類的靜態方法的調用方式,是靜態綁定的;
類調用 (invokestatic) 是在編譯時就已經確定好具體調用方法的情況。
實例調用 (invokevirtual)則是在調用的時候才確定具體的調用方法,這就是動態綁定,也是多態要解決的核心問題。
JVM 的方法調用指令有四個,分別是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前兩個是靜態綁定,后兩個是動態綁定的。
class Person { public String toString(){ return "I"m a person."; } public void eat(){} public void speak(){} } class Boy extends Person{ public String toString(){ return "I"m a boy"; } public void speak(){} public void fight(){} } class Girl extends Person{ public String toString(){ return "I"m a girl"; } public void speak(){} public void sing(){} }
如果子類改寫了父類的方法,那么子類和父類的那些同名的方法共享一個方法表項。因此,方法表的偏移量總是固定的。所有繼承父類的子類的方法表中,其父類所定義的方法的偏移量也總是一個定值。Person 或 Object中的任意一個方法,在它們的方法表和其子類 Girl 和 Boy 的方法表中的位置 (index) 是一樣的。這樣 JVM 在調用實例方法其實只需要指定調用方法表中的第幾個方法即可。
在常量池(這里有個錯誤,上圖為ClassReference常量池而非Party的常量池)中找到方法調用的符號引用 。
查看Person的方法表,得到speak方法在該方法表的偏移量(假設為15),這樣就得到該方法的直接引用。
根據this指針得到具體的對象(即 girl 所指向的位于堆中的對象)。
根據對象得到該對象對應的方法表,根據偏移量15查看有無重寫(override)該方法,如果重寫,則可以直接調用(Girl的方法表的speak項指向自身的方法而非父類);如果沒有重寫,則需要拿到按照繼承關系從下往上的基類(這里是Person類)的方法表,同樣按照這個偏移量15查看有無該方法。
接口方法調用的原理因為 Java 類是可以同時實現多個接口的,而當用接口引用調用某個方法的時候,情況就有所不同了。
Java 允許一個類實現多個接口,從某種意義上來說相當于多繼承,這樣同樣的方法在基類和派生類的方法表的位置就可能不一樣了。
interface IDance{ void dance(); } class Person { public String toString(){ return "I"m a person."; } public void eat(){} public void speak(){} } class Dancer extends Person implements IDance { public String toString(){ return "I"m a dancer."; } public void dance(){} } class Snake implements IDance{ public String toString(){ return "A snake."; } public void dance(){ //snake dance } }方法調用的補充
我們先來看一個示例
public class Test { public static class A { public void print() { System.out.println("A"); } public void invoke() { print(); sprint(); } static void sprint() { System.out.println("sA"); } } public static class B extends A { @Override public void print() { System.out.println("B"); } static void sprint() { System.out.println("sB"); } } public static void main(String[] args){ A a = new B(); a.invoke(); // B SA } }
由于靜態方法是靜態調用的,在編譯期就決定了跳轉的符號,所以進入父類的invoke方法調用的sprint在編譯期即是A的sprint,A的sprint符號和B的sprint在class中并不相同,這個符號在編譯期已經確定了。
但是當在invoke中調用print,Java是通過傳進來的this去找他的類型信息,再從類別信息里去找方法表,所以依然調用的是子類方法表中的print。
我們再看一個例子。
public class Test { public static class A { public int a = 3; public void print() { System.out.println(a); } } public static class B extends A { public int a = 4; } public static void main(String[] args){ B b = new B(); b.print(); // 3 } }
多態只適用于父子類同樣簽名的方法,而屬性是不參與多態的。在print里的符號a在編譯期就確定是A的a了。同樣的還有private的方法,私有方法不參與繼承, 也不會出現在方法表中,因為私有方法是由invokespecial指令調用的。
成員變量的訪問只根據靜態類型進行選擇,不參與多態
私有方法不會發生多態選擇,只根據靜態類型進選擇。
繼承的實現原理上面已經說明了類方法調用的問題,子類繼承父類在方法調用時依然是根據對象頭找型別信息,然后去自己的類信息里找到方法區調用方法指針,和C++通過在對象中增加虛函數表指針不一樣,Java需要通過自己的運行時型別信息找到自己的方法表,而且這張方法表不僅包含覆蓋的方法也包含不覆蓋的,不像C++,不同的虛函數表包含不同的方法。比如A->B->C,那么A對象部分包含的虛函數表只有A聲明的虛方法,假設B新聲明了虛方法X,在C類的B類部分的末尾的虛函數表指針指向的才包含X,但是A類部分的指向的虛函數表則不會包含X。Java實際上是先在編譯時期就得知方法的偏移,在調用的時候直接找到真正型別的方法表對應偏移的方法,如果一個父類引用調用了一個父類沒有的方法,在編譯期就會報錯。
和C++不同,C++的內存布局是非常緊湊的,這也是為了支持它天然的拷貝語義,c++父類對象的內存空間是直接被包含在子類對象的連續內存空間中的,其屬性的偏移都取決于聲明順序和對齊。而Java雖然父類的實例變量依然是和子類的放在同一個連續的內存空間,但并非是通過簡單的偏移來取成員的。不過在Java對象的內存布局中,依然是先安置父類的再安置子類的,所以講sizeof(Parent)大小的內容轉型成為父類指針,就可以實現super了。具體是在字節碼中子類會有個u2類型的父類索引,屬于CONSTANT_Class_info類型,通過CONSTANT_Class_info的描述可以找到CONSTANT_Utf8_info,然后可以找到指定的父類。
重載、覆蓋和隱藏重載:方法名相同,但參數不同的多個同名函數
參數不同的意思是參數類型、參數個數、參數順序至少有一個不同
返回值和異常以及訪問修飾符,不能作為重載的條件(因為對于匿名調用,會出現歧義,eg:void a ()和int a() ,如果調用a(),出現歧義)
main方法也是可以被重載的
覆蓋:子類重寫父類的方法,要求方法名和參數類型完全一樣(參數不能是子類),返回值和異常比父類小或者相同(即為父類的子類),訪問修飾符比父類大或者相同
子類實例方法不能覆蓋父類的靜態方法;子類的靜態方法也不能覆蓋父類的實例方法(編譯時報錯),總結為方法不能交叉覆蓋
隱藏:父類和子類擁有相同名字的屬性或者方法時,父類的同名的屬性或者方法形式上不見了,實際是還是存在的。
當發生隱藏的時候,聲明類型是什么類,就調用對應類的屬性或者方法,而不會發生動態綁定
方法隱藏只有一種形式,就是父類和子類存在相同的靜態方法
屬性只能被隱藏,不能被覆蓋
子類實例變量/靜態變量可以隱藏父類的實例/靜態變量,總結為變量可以交叉隱藏
隱藏和覆蓋的區別:
被隱藏的屬性,在子類被強制轉換成父類后,訪問的是父類中的屬性
被覆蓋的方法,在子類被強制轉換成父類后,調用的還是子類自身的方法
因為覆蓋是動態綁定,是受RTTI(run time type identification,運行時類型檢查)約束的,隱藏不受RTTI約束,總結為RTTI只針對覆蓋,不針對隱藏
java的對象模型Java中存在兩種類型,原始類型和對象(引用)類型。原始類型,即數據類型,內存布局符合其類型規范,并無其他負載。而對象類型,則由于自定義類型、垃圾回收,對象鎖等各種語義與JVM性能原因,需要使用額外空間。
Java對象的內存布局:對象頭(Header),實例數據(Instance Data),對齊填充(Padding)。
詳細的內容可以查閱參考文章
這里我們主要講講在繼承和組合兩種情形下會對內存布局造成什么變化。
類屬性按照如下優先級進行排列:長整型和雙精度類型;整型和浮點型;字符和短整型;字節類型和布爾類型,最后是引用類型。這些屬性都按照各自的單位對齊。
不同類繼承關系中的成員不能混合排列。首先按照規則2處理父類中的成員,接著才是子類的成員
當父類中最后一個成員和子類第一個成員的間隔如果不夠4個字節的話,就必須擴展到4個字節的基本單位。
如果子類第一個成員是一個雙精度或者長整型,并且父類并沒有用完8個字節,JVM會破壞規則1,按照整形(int),短整型(short),字節型(byte),引用類型(reference)的順序,向未填滿的空間填充。
數組有一個額外的頭部成員,用來存放“長度”變量。數組元素以及數組本身,跟其他常規對象同樣,都需要遵守8個字節的邊界規則。
下面給一個例子
public class Test { public static class A { public A() { System.out.println(this.hashCode()); } } public static class B extends A { public B(){ System.out.println(this.hashCode()); System.out.println(super.equals(this)); } } public static void main(String[] args){ B b = new B(); } } /* * 輸出如下: * 1627674070 * 1627674070 * true */參考文章
Java對象的創建
Java類初始化順序(看這篇文章就夠了)
詳解內部類
詳解匿名內部類
內部類的簡單實現原理(時間不夠,內部類只看這篇即可)
Java技術——多態的實現原理(時間緊多態只看這篇)
java方法調用之重載、重寫的調用原理(一)
java方法調用之單分派與多分派(二)
java方法調用之動態調用多態(重寫override)的實現原理——方法表
java方法調用之多態的補充示例
java的重載、覆蓋和隱藏的區別
Java 對象內存布局
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70101.html
摘要:眾多面向對象的編程思想雖不盡一致,但是無論哪種面向對象編程語言都具有以下的共通功能。原型編程以類為中心的傳統面向對象編程,是以類為基礎生成新對象。而原型模式的面向對象編程語言沒有類這樣一個概念。 什么是面向對象?這個問題往往會問到剛畢業的新手or實習生上,也是往往作為一個技術面試的開頭題。在這里我們不去談如何答(fu)好(yan)問(guo)題(qu),僅談談我所理解的面向對象。 從歷...
showImg(https://segmentfault.com/img/remote/1460000007103938?w=391&h=247); 文章最初發表于我的個人博客非典型性程序猿 對于剛接觸JAVA或者其他面向對象編程語言的朋友們來說,可能一開始都很難理解面向對象的概念以及類和對象的關系。筆者曾經帶過一個短期培訓班教授java入門基礎,在最后結束課程的時候,還有很多同學不太理解面向對象...
摘要:同時,創建的子類有幾個固定字段,分別是初始化函數原型初始化函數對象通過這個函數,把基類和子類的函數合并執行,這樣解決了基類構造函數無法執行的問題。二是構造函數可能不止會操作,還可能會修改全局的某些狀態比如計數器。 綜述 在 ES6 之前,ES5 實現面向對象是大家經常討論的問題,趁著 ES6 還沒進入瀏覽器,借我自己的一段腳本,跟大家討論一下 js 面向對象的一些細節問題,歡迎留言指教...
摘要:當你真正到公司里面從事了幾年開發之后,你就會同意我的說法利用找工作,需要的就是項目經驗,項目經驗就是理解項目開發的基本過程,理解項目的分析方法,理解項目的設計思 Java就是用來做項目的!Java的主要應用領域就是企業級的項目開發!要想從事企業級的項目開發,你必須掌握如下要點: 1、掌握項目開發的基本步驟 2、具備極強的面向對象的分析與設計技巧 3、掌握用例驅動、以架構為核心的主流開發...
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
閱讀 1135·2021-11-25 09:43
閱讀 1575·2021-10-25 09:47
閱讀 2471·2019-08-30 13:46
閱讀 758·2019-08-29 13:45
閱讀 1285·2019-08-26 13:29
閱讀 2995·2019-08-23 15:30
閱讀 1109·2019-08-23 14:17
閱讀 1331·2019-08-23 13:43