天天看點

4.Java中的類和對象【第三章節草案】

本文目錄:【藍色部分為本章的目錄】 1.基本概念 2.Java變量相關 1)Java變量分類 2)Java中變量的初始化 3)Java變量修飾符和通路域 4)Java類修飾符[不包含内部類] 3.Java涉及OO的關鍵知識點【主體】 1)繼承的基本概念 2)抽象類、接口、final類: 3)重載和重寫: 4)對象的拷貝[深拷貝和淺拷貝]: 5)關鍵字this、super 6)Java中的inlining[内聯] 7)帶繼承的構造函數以及構造順序 8)談談Object中的方法:equals、hashCode、toString 9)帶繼承的類型轉換以及轉換中關于成員變量和成員函數的調用 10)Java語言中的反射 11)按引用傳遞和值傳遞原理 12)Java中的包和導入 13)匿名類和内部類 4.Java程式設計OO設計技巧 1)對象建立以及周期 2)對象屬性設定 3)垃圾回收 4)繼承、接口、抽象類 5.總結 4.Java程式設計OO設計技巧:   以下這些内容牽涉到開發過程中的一些開發經驗,以及個人整理的一些OO設計心得和項目實踐内容,僅做參考,而且文字量比較大!我們在學習Java的時候,初學一般不會考慮到很多程式性能以及記憶體管理上的問題,但是我們在開發過程就會遇到很多偏向這個方面的問題,這些問題往往不是因為别的原因,就是因為我們本身寫的代碼品質的問題,這種情況在大型項目以及嵌入式系統開發的時候尤為突出。是以養成一個良好的開發習慣以及一個比較規範的代碼習慣對自己本身是一個不錯的學習語言的方式,如果能夠針對語言基礎掌握一些更加良好的程式設計技巧,就會使得開發的系統更加美麗。   1)對象建立以及周期   i.了解編譯期(compile-time)優化   ——◆程式設計心得[1]:不能依賴編譯期優化技術——   我們程式設計的時候已經習慣了[編譯器優化能力],通常都是在開發過程關閉代碼的優化選項,一旦等程式調試通過了,就打開編譯器優化,讓編譯器産生優化代碼。從優化的效率而言,現代編譯器優化技術本身還是蠻先進的,而編譯器優化過程會使得代碼執行更加高效。正因為如此,很多程式員在程式設計的時候就過于依賴編譯器的優化特性來清理自己寫得很差的代碼,但是從開發角度上講,這是個壞習慣。其實我們在編寫代碼過程中,應該從計算機的思維來書寫程式,因為這些代碼最終會經過計算機編譯器進行編譯以及優化操作。   Sun公司的javac編譯器和其他的公司的編譯器僅僅支援少量的優化技術,包括最簡單的一些優化技術,如常量合并和無用碼删除。   [1]常量合并是編譯器針對常量表達式的預處理過程,也可以稱為預先運算,先看下邊代碼段: static final intlength =12; static final intheight =2; intvalue = length * height;   上邊的代碼将會在編譯時運算,也就是當這段代碼被編譯成class的位元組碼過後,會轉換為以下形式: intvalue =24;   這種情況就是編譯器優化過程中的常量合并,這裡需要提的是:常量合并不僅僅是javac編譯器裡面會用到,在其他語言編譯器的優化技術中有時候也會碰到。   [2]無用碼删除:這種技術的主要目的,是為了讓編譯器不去為[絕對不會執行]的區段産生相關位元組碼(bytecode),這種技術不影響代碼的執行,但是可以減少生成的class檔案的體積,如下代碼段: public classTestNoExecuteCode { public static finalbooleantestCondition = false; publicvoid testMethod()

{

if(TestNoExecuteCode.testCondition )

{ //……

} }

}   以上代碼裡面if語句的語句塊就稱為[絕對不會執行],但是有一點,javac編譯器判斷這段代碼是否絕對不會執行的規則是:運作時才會判斷為false的表達式,還是會生成位元組碼(bytecode)的,不生成位元組碼(bytecode)的情況必須是——表達式在編譯期間就已經被判斷為false了。javac指令若打開優化代碼使用javac -o,但是Sun的Java 2 SDK中-o編譯器選項對生成的bytecode毫無作用,如今沒有太大的必要使用這個選項,若開發過程我們真的需要将代碼優化,有三個選擇:【以下參考E文的翻譯】

  ●優化Java源代碼最好的方式是手工優化,如果要獲得更好的性能,可以掌握一些手工優化方法;   ●使用第三方的編譯器,将源碼編譯為優化的位元組碼(bytecode)   ●依賴JIT或者Hotspot運作時優化政策   ii.了解運作時(runtime)代碼優化   ——◆程式設計心得[2]:善用運作時代碼優化技術——   雖然編譯器在編譯時不會産生大量優化過的位元組碼(bytecode),但是JITs卻可以做各種優化工作,JITs的概念,可以參考第三章第六節:Java中的inlining,這裡就不做重複介紹。JIT本身的運作目的就在于它會将位元組碼(bytecode)在運作時轉換為本機二進制碼(native binary code),某些JITs在轉化之前,會先分析編譯器生成的位元組碼,然後進行優化轉換。JIT本身的目的在于将位元組碼轉化稱為本機二進制碼,本機執行的方式通常比解釋執行的方式速度快很多,從這點思考,如果被編譯的位元組碼如果被作業系統執行的次數很多,這種轉化是相當合算的。   但是JIT的使用前提在于:JITs必須確定[收集資料和執行優化]的時間,不能超過優化節省的時間。   一般情況下,JIT執行我們編譯好的代碼都會使程式更加快捷,但是開發過程可能會忽略一點,JIT本身的運作也是需要時間的,JITs是針對相對較少的運作時間設計,它的存在是為了加速代碼而并非使代碼緩慢下來,為了收集[充分的、為執行優化技術而必要的]資料,必須花額外的開銷,而基于這些考慮,JITs又不得不去忽略一些優化技術。單純依賴運作時優化的一個問題就是程式的大小,這一點主要展現在嵌入式系統開發以及實時程式設計中,因為這些程式對大小是有一定的要求的,而且存在特殊性;很多嵌入式系統本身沒有記憶體去啟動JIT或者Hotspot執行層,對需要Java快速運轉的實時程式設計,JIT或者Hotspot會有不确定性。這點可以參考rtj技術,即Real Time Java。   上邊兩點可以知道,絕佳組合就是:優化過後的bytecode和JIT或Hotspot執行層結合!   [*: 編譯時和運作時的相關内容,在Java異常部分同樣會提及,如果有必要到時候我會寫一份關于這種小知識點的總結性内容]   iii.對象建立和使用   ——◆程式設計心得[3]:減小對象建立的成本——   我們寫一段Java代碼的時候,如果定義了一個類A,往往建立A的對象寫的語句就是: A a =newA();   當然上邊這種做法是我們都使用的标準做法,既然是如此,一個複雜對象在建立的時候有可能就會牽涉到對象的成本開銷了,而這份成本開銷是我們最容易忽略的。對象的構造過程往往不像我們想象中那樣簡單,我們印象中的建立對象就是:配置設定記憶體+初始化值域,這裡我們再談談對象的建立。因為我們将需要建立的對象的數量和體積最小化,就可以一定程度上提升程式的性能,這種做法将稱為任何系統本身的“福音”。是以我們必須徹底了解:一個對象建立過程。以下是對象建立過程(*:需要提及的是這裡隻是對象的建立,我們在學習Java的對象和類的時候一定要區分類加載和對象建立兩個過程針對源代碼定義的變量以及成員産生的影響。):   [1]從記憶體堆(heap)之中配置設定記憶體,用以存放全部的執行個體變量以及這個對象連同其父類以及整個繼承樹上的父類的實作專用資料,這種資料包括類和方法的資料區以及類和方法的指針。   [2]該對象的執行個體變量被初始化稱為對應的預設值   [3]調用最底層派生類的構造函數,構造函數做的第一件事情就是調用它的父類的構造函數,這個函數會遞歸調用直到Ojbect根類為止,這裡也說明了一點:Java裡面所有的Class的基類就是java.lang.Object。   【*:這裡糾正一個小小的筆誤,以防讀者誤解。前邊有個程式我直接寫了調用Base類的構造函數,實際上對JVM本身而言在對象初始化的時候,确實是最先調用了Object的構造函數,也就是說從父類往下遞歸調用,但是這個遞歸的過程不是JVM直接調用的父類的,它是先調用的子類的構造,因為子類的構造函數的第一句話總是super()或者super(param),是以它會一直遞歸到Object類,每個類的執行個體化過程都是如此,是以希望讀者能夠了解這個初始化過程,同樣了解在構造函數裡面什麼都不寫的時候,它使用super關鍵字的隐藏語句,同樣也可以解釋為什麼當父類定義了含參數的構造函數的時候,子類一定要顯示調用父類的構造函數并且使用super(param)的方式】   [4]在構造函數本體執行的時候,所有的執行個體變量都會設定初始值和初始化區段先執行,然後再執行構造函數本體。是以基類的構造函數先執行,然後才會執行子類的構造函數,這就使得任何構造函數都可以放心大膽使用父類的任何執行個體變量。   【*:這裡可以從概念上來了解這個設計過程,如果一個子類繼承了父類,那麼子類理所當然是可以使用父類的變量的,但是假設子類的構造在父類之前,那麼就會出現子類在調用構造函數的時候,父類的變量還未進行初始化,而子類本身又不可能對父類的變量進行初始化,是以這種構造函數的遞歸調用在所有OO程式設計語言裡面幾乎都是如此的設計法則。八卦一句:我們在學習一門語言的時候盡可能去參透語言設計的為什麼,這樣更加容易輔助我們了解語言的一些特性。】   既然一個對象的建立過程牽涉到了這麼多過程,那麼建立的時候建立一個輕量級對象比建立一個重量級對象的效率要快很多。而輕量級對象的含義可以了解為:不具有長繼承鍊、同樣不包含了太多其他對象的對象。這樣就牽涉到如何使用OO設計使得對象變得更加輕量級,這裡引入一個新概念POJO,POJO的概念在很多架構中都經常使用,其設計以及相關内容我可以推薦讀者一本書《POJOs in Action》,POJO屬于輕量級的對象,在很多架構諸如Hibernate、JDO中有時候都經常涉及到,而JPA規範裡面所設計的領域模型對象大部分也屬于POJO對象,而且将POJO規範化了,它的中文翻譯可以為:簡單Java對象。   這裡我們考慮一種情況,比如一個類A,裡面有兩個變量屬于對象類型的,一個為B類的執行個體、一個為C類的執行個體,然後由假設A、B、C三個類都是Object之下的第三代子類,那麼在初始化一個A的過程是如何做呢,這裡留給讀者自己去思考,這個時候A就屬于重量級對象。這種對象初始化的做法和一個單純的A類的初始化過程本質上一樣,但是開銷卻是大相徑庭,這裡提供一個輕量級對象A的初始化過程: classA{ private int val;

private booleanhasData =true ;

publicA(int a)

{

val = a; //這裡隐藏了一個this.隻要形參變量和執行個體變量不重名,這種省略寫法是合法的。

}

//……

}   如果寫入了這樣一句代碼來調用該對象:A a =newA(12);   它的建立步驟如下:   [1]從記憶體堆配置設定記憶體,用來存放A類的執行個體變量,以及一份[實作專屬資料]   【*:以防概念混淆,這裡舉個例子,實作專屬資料可以這樣了解,A的直接父類是Object,如果Object存在執行個體變量,那麼記憶體堆配置設定記憶體的時候,同樣包括A類以上整個繼承鍊裡面其他超類的資料區以及方法和類的指針】   [2]A類的執行個體變量val和hasData被初始化為對應的預設值,val的預設值為0,hasData指派為預設值false   [3]調用A類的構造函數,傳入值5   [4]A類調用父類的構造函數,這裡父類為(java.lang.Object)   [5]父類構造函數調用完成過後傳回,A類構造函數執行執行個體化的初始值設定過程,這個時候hasData指派為true   [6]然後指派将val設定為5,A的構造函數調用結束   [7]然後将引用a指向剛剛記憶體堆裡面完成的對象A   以上就是一個輕量級對象的整個初始化過程,結合前邊講的對象的構造順序來了解更加容易了解Java裡面對象的構造流程,如果分析一個重量級對象的構造流程,你就會發現合理設計系統裡面的對象是很重要的。一般情況下,如果我們在軟體性能評測的時候可以确定性能問題是由于中型對象建立而成,那麼我們可以采取一定的政策來回避:1]使用延遲加載;2]重新設計該類使之合理或者更加輕量級;當然本小節的書寫目的不是為了讓讀者在開發過程不使用重量級對象,合理設計對象的量級是OO裡面的一個難點,因為業務複雜度,我們在模組化過程會牽涉到很多對象的設計,是以如何設計一個對象的量級是我們開發過程要去仔細思考的内容。而class以下特征将會增加對象的建立成本:   ●構造函數中包含過量代碼   ●内含數量衆多或者龐大的對象   ●太深的繼承層次,而且有些類不是它的直接父類   ——◆程式設計心得[4]:盡可能複用對象——   上邊提及了對象建立的開銷,是以我們在設計過程需要盡可能減少建立的次數,這種情況下,建立的對象越少,意味着代碼執行效率會越高。但是我們在實際開發中,有可能會針對某個相同對象進行重複的建立工作,這種情況下,我們盡量采取對象複用技術,而不去重新建立該對象。這裡提供一個代碼的例子來說明: classUserInfo { privateStringuserName ;

privateStringpassword ;

privateStringemail ;

private intsalary; public void setUserName(String userName)

{

this.userName = userName;

}

public void setPassword(String password)

{

this.password = password;

}

public void setEmail(String email)

{

this.email = email;

}

public String getUserName()

{

return this.userName ;

}

public String getPassword()

{

returnthis.password ;

}

public String getEmail()

{

return this.email ;

}

public voidsetSalary(int salary)

{

this.salary = salary;

}

public int getSalary()

{

returndbprocess(this);//這個地方dbprocess(this)是僞代碼,也可以算作僞代碼,它所表示的業務含義就是

}

} public classUserService { public intcomputePayroll(String[] username,String[] password) {

//TODO:斷言注釋部分,為Debug用,位置:UserService.computePayroll

//這個是我自己寫代碼的常用寫法assert:username.length == password.length;

int size = username.length;

int totalPayroll = 0;

for(int i = 0; i < size; i++ )

{

UserInfo user =newUserInfo();//這裡是疊代建立對象

user.setUserName(username[i]);

user.setPassword(password[i]);

totalPayroll += user.getSalary();

}

return totalPayroll;

}

}   分析上邊的代碼,其缺陷在于,每一次疊代的時候都會去建立一次UserInfo對象,這種情況實際上是沒有必要的,沒有必要在循環疊代中每疊代一次都去建立該對象,這筆開銷會随着對象的複雜度有所提升,是以改成以下版本: public classUserService { public intcomputePayroll(String[] username,String[] password) {

//TODO:斷言注釋部分,為Debug用,位置:UserService.computePayroll

//這個是我自己寫代碼的常用寫法assert:username.length == password.length;

int size = username.length;

int totalPayroll = 0;

UserInfo user =newUserInfo();//在疊代外建立對象

for(int i = 0; i < size; i++ )

{ user.setUserName(username[i]);

user.setPassword(password[i]);

totalPayroll += user.getSalary();

}

return totalPayroll;

}

}   用了以上的代碼版本過後,你的代碼運作比起原始版本要快大概4到5倍左右,但是這樣有可能會引入一個誤區,典型的使用就是對于集合Vector或者ArrayList的時候,使用這樣技術是不現實的。在Java裡面,集合儲存的是對象引用而不是對象本身,而我們在使用以上高效代碼版本的過程裡并不會建立對象,這樣就使得對象的拷貝不存在,在對象添加到集合裡面稱為集合元素的時候需要的是一個對象拷貝,否則會使得集合裡面所有元素的引用指向一個對象。這種解決最好的辦法就是建立對象的拷貝或者直接對對象進行clone操作,這樣就可以複用該對象了,是以這種情況使用高效代碼版本反而是不合适的做法。   iv.對象的銷毀   ——◆程式設計心得[5]:消除過期的對象引用——   我們在C++語言中可以知道,如果要消除記憶體配置設定,比如消除某個指針配置設定的記憶體要使用關鍵字delete操作,也就是需要自己手工管理記憶體。而Java具有垃圾回收功能,它會自動回收過期的對象引用以及不使用的記憶體,當我們用完了對象過後,系統會自動回收該對象使用期間的配置設定的記憶體。但是有一點需要說明的是:我們一旦聽說系統能夠自動回收記憶體,是以自己在程式設計的時候往往覺得沒有必要再考慮記憶體管理的事情了,但實際上這是一個很不好的習慣。這裡提供一個完整的例子來說明這中情況: public classCustomStack { private Object[] elements;

private int size = 0;

publicCustomStack(int size)

{

this.elements =new Object[size];

}

//……

public Object pop()

{

if ( size == 0 )

throw new EmptyStackException();

return elements[--size];

} }   這段代碼本質上講沒有太大的錯誤,而且不論怎麼測試,它的運作都是正常的,但是卻存在一個潛在的“記憶體洩漏問題”,嚴格點講,随着垃圾回收活動的增加,不斷占用了使用的記憶體,程式的性能會逐漸展現出上邊這段代碼的問題。而且這個潛在的問題有可能還會引起OutOfMemoryError的錯誤,隻是這種問題是潛在的,有可能對于普通應用失敗的幾率很小很小。   【*:這裡提及一個額外的心得,我們開發程式的最初,都不可能擁有龐大的程式資料量,但是我們最好在最初設計系統的時候就考慮到系統在遇到大量資料以及複雜業務邏輯的時候的效率以及其靈活性,雖然開始感覺可能有點複雜或者繁瑣,但是這樣的設計會使得後期的很多開發變得異常輕松。】   而上邊這句話的錯誤在于:存在過期引用沒有進行消除工作,在pop()方法内部我們傳回的時候是直接傳回的這個堆棧之前的引用,是以就存在了原來的引用指向的對象實際上是沒有被清空的。可以看看修改過的版本來對比【僅提供pop函數代碼】: publicObject pop() { if ( size == 0 )

throw new EmptyStackException();

Object obj = elements[--size];

elements[size] =null ;

return obj;

}   上邊代碼有一句elements[size] = null,其實道理很簡單,該方法的調用目的是傳回Stack的移除對象,原始版本裡面,傳回是對的,但是并沒有從Stack中移除,而在傳回過後,有一個引用并沒有設定為null,使得該對象被保留下來了,這樣垃圾回收器在回收的時候不會注意到該對象,是以該對象不會被清除,反複多次調用過後,就使得Stack的操作保留了很多無用的對象下來,這樣程式執行時間長了就會OutOfMemoryError。但是有一點就是不能在開發過程過于極端,不能每次遇到這樣的問題的時候都去考慮設定為null的情況,本小節的目的是:消除過期的對象引用,這種做法也是盡可能消除,不是所有的内容都依靠程式手動消除,在一定情況下可以依賴Java本身的垃圾回收,也是比較标準的做法。   2)對象屬性設定   i.函數參數:   [1]了解Java裡面的形參和實參:我們在Java