Java是一門強大的進階語言。在學習了其基礎知識後,我們仍需要理解其深刻的內涵。接下來,我們會以《Effective Java》一書做為Java進階學習的載體,對Java進行一個系統的、全新的認識。接下來,就讓我們來感受Java高深的內涵吧。 第一章:建立和銷毀對象 第1條:考慮用靜態Factory 方法代替構造器
對於類而言,為了讓用戶端擷取它自身的一個執行個體,我們最常用的方法就是提供一個公有的構造器了。但是,其實還有一種好方法(它應該在每個程式員的工具箱中佔有一席之地),即是類提供一個公有的靜態工場方法。(一個返回類的執行個體的靜態方法,並不同於設計模式中的Factory 方法模式)
提供靜態Factory 方法而不是公有構造器有以下幾大優勢:
1. 靜態Factory 方法有名稱而構造器沒有自己的名稱。
如果構造器的參數本身沒有確切地描述正被返回的對象,那麼就會出現客戶無法判斷所拿到的東西是什麼的尷尬局面了。舉個栗子:構造器BigInteger(int,int,Random)返回的BigInteger可能為素數,如果用名為BigInteger,probablePrime的靜態Factory 方法來表示則更為清楚。(1.4發行版最後就增加了這個方法。)
還有一種情況,就是當需要構建的內容一個構造器不夠使用時,我們往往會提供兩個構造器,這其實是很不好的方法,因為客戶根本就不知道該用哪個構造器,調用錯誤的構造器時,就會出現資料出錯。因此,可以使用靜態Factory 方法,並且為靜態Factory 方法選擇能夠突出其作用的名稱。
2.不必多次建立一個新對象
這樣不可變類就可以使用預先構建好的執行個體,或者將構建好的執行個體緩衝起來,進行重複利用,從而避免建立不必要的重複對象。舉個栗子:Boolean.valueOf(boolean)。它從來不建立對象。
另外,還有一個小優點。靜態Factory 方法能夠為重複的調用返回相同的對象,可嚴格控制執行個體的存在
3.可以返回原傳回型別的任何子類型的對象
使用靜態Factory 方法時,要求用戶端通過介面來引用被返回的對象,而不是通過它的實作類別來引用被返回的對象。用戶端永遠不需要關心他們從Factory 方法中得到的對象的類,只需要關心是某個類的某個子類即可。
4.建立參數化型別執行個體時,使代碼變得更加簡潔
在調用參數化類構造器時,即使型別參數很明顯,還是必須聲明。
以建立一個Map類為例:
Map<String,List<String>> m=new HashMap<String,List<String>>();
參數少的時候還好,隨著型別參數越變越長,這一說明也就變得異常痛苦。而靜態Factory 方法就可以解決這個問題(類型推導)
public static <K,V> HashMap<K,V> newInstance(){ return new HashMap<K,V>();}//調用Map<String,List<String>> m =HashMap.newInstance();//Java1.6還沒有這種Factory 方法,但你可以把這種方法放在你的工具類中。
靜態Factory 方法的缺點:
1.類如果不含公有的或者受保護的構造器,就不能被子類化
2.它與其他的靜態方法實際沒有任何差別
沒有任何差別意味著什麼呢。就是說,對於提供了靜態Factory 方法而不是構造器的類來說,沒有像構造器那樣在API文檔中明確標識出來,因此,想要查明如何執行個體化一個類是很困難的。那該怎麼辦呢。其實還是有方法的,你可以通過在類或者介面注釋中關注靜態工廠,並遵守標準的命名習慣即可。 第2條:遇到多個構造器參數時考慮使用構建器
上面講到了靜態工廠類,靜態工廠類與構造器相比好處還是蠻多的,但是,兩者有個共同的局限性:他們都不能很好地拓展到大量的選擇性參數。
解決方案:
1、我們一般想到的就是採用重疊構造器模式(可自行百度)
//示範調用代碼NutritionFacts cocaCola=new NutritionFacts(240,15,6,7,0,5,6);
當有許多參數時,重疊構造器模式會讓用戶端代碼很難編寫,且難以閱讀
2、JavaBean模式
//示範調用代碼NutritionFacts cocaCola=new NutritionFacts();cocaCola.setServingSize(240);cocaCola.setServings(8);cocaCola.setCalories(100);
JavaBean模式也有很嚴重的缺點。因為構造過程被分到了幾個調用中,所以構造過程中不能保證JavaBean的一致性。如果使用處於不一致狀態的對象,會導致失敗,且調試起來十分困難。另外,JavaBean模式阻止了把類做成不可變得可能,所以需要我們再費力氣去確保其安全執行緒。(手動“凍結”對象)但是手動的方法十分笨拙,在實踐中很少使用。
幸虧,還有第三種方法。
3、Builder模式
原理:不直接產生想要的對象,而是讓用戶端利用所有必要的參數調用構造器(或者靜態工廠),得到一個builder對象。然後用戶端在builder對象上調用類似於setter的方法,設定每個相關的選擇性參數。最後調用無參的build方法產生不可變對象。彌補了前兩種方法的不足。
代碼如下:
public class NutritionFacts{ private final int servingSize; private final int servings; private final int fat; public static class Builder{ //Required parameters private final int servingSize; private final int servings; private int fat=0; public Builder(int servingSize,int servings){ this.servingSize=servingSize; this.servings=servings; } public Builder fat(int val){ fat=val; return this; } public NutritionFacts builder(){ return new NutritionFacts(this); } } private NutritionFacts(Builder builder){ servingSize=builder.servingSize; servings=builder.servings; fat=builder.fat; }}//實驗調用代碼NutritionFacts cocaCola=new NutritionFacts.Builder(200,8).fat(60).builder();//建立成功
於構造器相比,builder的優勢在於可以有多個可變(varargs)參數。構造器則只能有一個可變參數。又因為builder利用單獨的方法來設定每個參數,要多少個就有多少個,要怎麼調整就怎麼調整。
總之,如果類的構造器或者靜態工廠中具有4個以上參數,設計這種類時就首選Builder模式,特別是當大多數參數都是可選的的時候。
第3條:用私人構造器或者枚舉類型強化Singleton屬性
Singleton指僅被執行個體化一次的類。通常代表如視窗管理器或檔案系統等本質上唯一的系統組件(使類成為Singleton會使它的用戶端測試變得很困難,因為無法給Singleton替換類比實現,除非它實現一個充當其類型的介面)
Java1.5發行前有兩種方法實現Singleton。
第一種:公有靜態成員為final域
public class Lin{ public static final Lin INSTANCE =new Lin();//使用final修飾,清楚地表明了這個類是單例 private Lin(){...} public void leaveTheBuilding(){...}}
由於缺少了公有的或者受保護的構造器,所以保證了Lin的全域唯一性:一旦Lin類被執行個體化,只會存在一個Lin執行個體,用戶端的任何行為都不會改變這一點。但要注意:享有特權的用戶端可以藉助AccessibleObject.setAccessible方法,通過反射機制調用私人構造器。(若要抵禦這種攻擊,則可以修改構造器,在要建立第二個執行個體時拋出異常)
第二種:公有成員是靜態方法
public class Lin{ private static final Lin INSTANCE = new Lin(); private Lin(){...} public static Lin getInstance(){return INSTANCE;} public void leaveTheBuilding(){...}}
對於所有調用都會返回同一個對象引用,所以保證了只有一個Lin執行個體。
以上兩種方法皆是把構造器保持為私人的,並匯出公有的靜態成員變數,以便允許用戶端訪問該類的唯一執行個體
但如今,以上的公有域方法在效能上不再有優勢,因為現在的JVM實現幾乎能將靜態Factory 方法的調用內聯化。(是不是很奔潰,講了那麼久博主你居然跟我說上面的方法不再有優勢,那有卵用。)別著急~凡是都要知道了它的內涵之後才能真正理解它高效啟動並執行原理嘛。接下來就來介紹實現Singleton的最佳方法了。
第三種:編寫一個包含單個元素的枚舉類型
public enum Lin{ INSTANCE; public void leaveTheBuilding(){...}}
無償地提供了序列化機制,即使是在面對複雜的序列化或者反射機制的時候也能絕對防止多次執行個體化。 第4條:通過私人構造器強化不可執行個體的能力
public class UtilityClass{ private UtilityClass(){ throw new AssertionError(); } ...//Remainder omitted
這種習慣用法有一點副作用,它使得一個類不能被子類化。子類就沒有可訪問的超類構造器可調用了 第5條:避免建立不必要對象
伴侶。最好是一直陪伴的同一個,而不是在寂寞時隨便約炮找對象。而對象,也最好是能夠重用的,而不是每次需要時就建立一個相同功能的新對象。
如果對象是不可變的,它就始終可以被重用。
Don’t do this!
String s=new String("new");
這樣如果迴圈的話,會建立無數個不必要的執行個體
正確開啟檔案
String s="reuse";
對於已知不會被修改的可變對象也可以進行重用
class Person{ private final Date birthDate;//出生就確定了,所以不可修改 private static final Date BOOM_START;//final證明已被當做常量對待 private static final Date BOOM_END;//同上 static{//初始化時建立,因此之後都是不可變類 Calender gmtCal=Calender .getInstance(TimeZone.getTimeZone("GMT")); gmtCal.set(1946,Calendar.JANUARY,1,0,0,0); BOOM_START=gmtCal.getTime(); gmtCal.set(1965,Calendar.JANUARY,1,0,0,0); BOOM_END=gmtCal.getTime(); } public boolean isBabyBoomer(){ return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0 ; }}
以上的這種方法,在多次調用時的速度會大大提高(因為已經是拿建立好的東西來重用)。
通過此點以上的判斷,我們知道了正確第使用對象的重要性。通過上面的重用方法,有的人可能會想到對象池。難道上述那些對象都適合放在對象池嗎。其實不然。
通過維護自己的對象池來避免建立對象並不是一種好的做法,除非池中的對象是非常重量級的。真正正確使用對象池的典型對象就是資料庫連接池。建立資料庫連接的代價非常昂貴,再加上資料庫許可可能限制你使用一定的數量串連,所以重用這些對象非常有意義。但一般而言,維護自己的對象池必定會把代碼弄得很亂,也增加記憶體佔用,損害效能。另外,現代的JVM實現具有高度最佳化的記憶體回收行程,所以效能很容易能超過輕量級對象池效能。所以,一般都用重量級對象池。
第6條:消除到期的對象引用
還記得我們這一章的標題叫什麼嗎。(前面講了太多靜態構造器的內容,估計大家都忘了這一章的標題了。)對,就是建立和銷毀對象。
今天,我們就來講講到期對象引用的消除。
由於Java有著強大的記憶體回收功能,我們可能會覺得,咦,我們現在就不再需要考慮記憶體管理的事了,真爽。但,其實不然。看下下面這個例子你就知道了
public class Stack{ private Object[] elements; private int Size = 0; private static final int DEFAULT_SIZE = 16; public Stack(){ elements = new Object[DEFAULT_SIZE]; } public void push(Object e){ ensureCapacity(); elements[Size++] = e; } public Object pop(){ if(size==0) throw new = EmptyStackException(); return elements[--Size]; } private void ensureCapacity(){ if(elements.length==Size) elements=Array.copyOf(elements,2*Size+1); }}
上面程式中並無明顯錯誤,運行也一切正常。但程式中隱藏著一個問題:記憶體流失。隨著記憶體回收行程活動的增加,記憶體佔用的增加,程式效能會不斷地降低。極端情況下還有可能導致磁碟交換或程式失敗。
我們來看看這個問題是怎麼出現的:
我們要知道,如果一個棧先是增長,然後再收縮。那麼,從棧中彈出的對象其實是不會被當做記憶體回收的,即使棧程式不再引用這些對象。為什麼呢。因為啊,棧內部維護著對這些對象的到期引用(永遠不會被解除的引用)。而且,如果一個對象引用被無意識地保留起來了,那麼記憶體回收機制不但不會處理這個對象,而且也不會處理被這個對象所引用的其他所有對象。對效能的影響得多大,想想就知道了……
如何解決這個問題呢。其實很簡單,我們只要在pop函數中做如下修改
Object result = elements[--Size]; elements[Size] = null; return result;
這叫做清空到期引用。原理解析:數組即時區域的元素是已經分配了的,而數組其餘部分的元素則是自由的。但是記憶體回收行程並不知道這一點;對於記憶體回收行程而言,elements中所有對象引用都同等有效。只有我們知道數組的非活動部分是不重要的,所以可以手動清空。
以上是記憶體流失的一種常見來源。另外兩個常見的泄漏來源是緩衝、監視器和其他回調。這兩種情況的解決方案則是使用WeakHashMap中的鍵來儲存。由於比較少用到,所以我們就不展開細講了。有遇到是再Google一下。 第7條:避免使用終結方法
首先需要強調:終結方法通常是不可預測的,也是很危險的,一般情況下是不必要的。使用終結方法會導致行為不穩定,降低效能,以及可移植性問題。
其次,再來說說終結方法的好處:兩種合法用途
1、當對象所有者忘記調用顯示終止方法時,可以充當“安全網”
以下是顯示終止方法代碼:
Foo foo = new foo(...);try{ ...}finally{ foo.terminate();//顯示終結方法。}
如果用戶端無法調用以上方法來正確結束操作時,“安全網”就起了作用。
2、終止非關鍵的本地資源
如果不是以上的兩種用途,則不要使用終結方法。原因:Java語言規範不僅不保證終結方法會被及時地執行,而且根本就不保證它們會被執行。當一個程式終止時,某些已經無法訪問對象上的終結方法依舊沒有被執行,這種情況是存在的。所以,慎用終結方法。 總結
在這一章中,我們講了對象的建立和銷毀習慣,在什麼時候應該使用哪種方式建立和銷毀對象,哪種方式是安全的,哪種方式應該盡量避免使用……讓我們對對象這一概念有了更清晰的認識,由於對象是Java的核心內容,所以,如何正確地建立和銷毀就顯得至關的重要了。
下一節,我們將會繼續學習Java編程的另一個核心——方法。