第四條:通過私有構造器強化不可實例化的能力。 1.有時候,你可能需要編寫至包含靜態方法和靜態域的類。這些類的名聲很不好,因為有些人在面向對象的語言中濫用這樣的類來編寫過程化的程序。 2.盡管如此,他們也確實有它們特有的好處: 1.利用這種類,以java.lang.Math或者java.util.Arrays的方式,把基本類型的值或者數組類型上的相關方法組織起來. 2.我們也可以通過java.util.Collections的方式,把實現特定接口的對象上的靜態方法包括工廠方法組織起來。 3.利用這種類可以把final類上的方法組織起來,以取代擴展該類的做法。 3.這樣的工具類Unility class不希望被實例化,實例對它沒有任何意義。然而在缺少顯示構造器的情況下,編譯器會自動提供一個公有的,無參的缺省構造器default constructor.對于用戶而言,這個構造器與其他的構造器沒有任何區別。在已發行的API中常常可以看到一些被無意識地實例化的類。 4.企圖通過將類做成抽象類來強制該類不可被實例化,這是行不通的。該類可以被子類化,并且該子類也可被實例化。這樣做甚至會誤導用戶,以為這種類是專門為了繼承而設計的。然而有一些簡單的習慣用法可以確保類不可被實例化。 由于只有當類不包含顯示的的構造器時,編譯器才會生成缺省的構造器,因為我們只要讓這個類包含私有的構造器,它就不能被實例化了: public class UtilityClass { private UtilityClass() { throw new AssertionError(); } //...others } 由于顯示的構造器是私有的,所以不可以再該類的外部訪問它。AssertionError不是必須的。但是它可以避免不小心在類的內部調用構造器。它保證該類在任何情況下都不會被實例化。 注:1.但是這種用法有點違背直覺,好像構造器就是專門設計成不能調用一樣。因此明智的做法就是在代碼中增加一條注釋,如: //Supress default constructor for noninstantiability. 2.AssertionError:拋出該異常指示某個斷言失敗 5.這種慣用法的副作用:它使得一個類不能被子類化。所有的構造器都必須顯示或隱式的調用超類構造器。在這種情形下子類就沒有可訪問的超類構造器可調用了。 第5條:避免創建不必要的對象 1.一般說來,最好能重用對象而不是在每次需要的時候就創建一個相同功能的新對象。重用方式即快速,又流行。如果對象是不可變的immutable,它就始終可以被重用。 極端反面的例子: String s = new String("landon");//Don't do this! 該語句每次執行的時候都會創建一個新的String實例,但是這些創建對象的動作全都是不必要的。傳遞給String構造器的參數("landon")本身就是一個String實例,功能方面等同于構造器創建的所有對象。 如果上述用法是在一個循環中,或者是在一個被頻繁調用的方法中,就會創建愛你出成千上萬不必要的String實例。改進:String s = "landon".該版本只用了一個String實例,而不是每次執行的時候都創建一個新的實例。而且其可以保證,對于所有在同一虛擬機中運行的代碼,只要其包含相同的字符串字面常量,該對象就會被常用(注:String常量池,@link String#intern)。 對于同時提供了靜態工廠方法和構造器的不可變類,通常可以使用靜態工廠方法而不是構造器,以避免創建不必要的對象。如Boolean.valueOf(String)幾乎總是優先于構造器Boolean(String).構造器在每次調用的時候都會創建一個新的對象,而靜態工廠方法則從來不要求這樣做,實際上也不會這樣做. 2.除了重用不可變的對象外,也可以重用那些已知不會修改的可變對象。下面這個也是可以較常見的反面例子,其中涉及可變的Date對象,它們的值一旦計算出來之后就不再變化。 Person#isBabyBoomer(),檢查這個人是否是生育高峰期出生的小孩,即1946至1964年間。 public boolean isBabyBoomer() { Calendar gmtCalendar = Calendar .getInstance(TimeZone.getTimeZone("GMT")); // 1946 gmtCalendar.set(1946, Calendar.JANUARY, 1, 0, 0, 0); Date boomStart = gmtCalendar.getTime(); gmtCalendar.set(1965, Calendar.JANUARY, 1, 0, 0, 0); Date boomEnd = gmtCalendar.getTime(); return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) <= 0; } 該方法每次調用的時候都會創建一個Calendar,一個TimeZone和兩個Date實例,這都是不必要的; 改進: static { Calendar gmtCalendar = Calendar .getInstance(TimeZone.getTimeZone("GMT")); // 1946 gmtCalendar.set(1946, Calendar.JANUARY, 1, 0, 0, 0); BOOM_START = gmtCalendar.getTime(); gmtCalendar.set(1965, Calendar.JANUARY, 1, 0, 0, 0); BOOM_END = gmtCalendar.getTime(); } <p>改進后的方法只在初始化的時候創建Calender,TimeZone和Date實例一次。如果該方法頻繁調用,則會顯著的提高性能。 <p>除了提高性能之外,代碼的含義也更清晰了,BOOM_START和BOOM_END從局部變量改為final靜態域,顯然就應該作為常量對待。 從而使代碼更易于理解。但是這種優化帶來的效果不總是那么明顯,因為Calendar實例的創建代價特別昂貴。 3.如果改進的Person類被初始化了,它的isBabyBoomer方法卻永遠不會被調用,那就沒有必要初始化BOOM_START和BOOM_END域。通過延遲初始化lazy initializing,即把對這些域的初始化延遲到isBabyBoomer方法第一次被調用的時候進行,則有可能消除這些不必要的初始化工作。但是不建議這樣做。正如延遲初始化中常見的情況一樣,這樣做會使方法的實現更加復雜,從而無法將性能顯著提高到超過已經達到的水平。 4.前面的例子中,討論到的對象均是能夠被重用的,因為它們在被初始化之后不會再改變。考慮適配器adapter情形,有時也叫做視圖view。適配器是指這樣一個對象,它把功能委托一個后備對象backing object,從而為后備對象提供一個可以替代的接口。由于適配器除了后備對象之外,沒有其他的狀態信息,所以針對某個給定對象的特定適配器而言,它不需要創建多個適配器實例。 如:Map接口的keySet方法返回該Map對象的Set視圖,其中包含該Map中所有的key.粗看起來,好友每次調用keySet都應該創建一個新的Set實例。但是對于一個給定的Map對象,實際上每次調用keySet都返回同樣的Set實例。雖然被返回的Set實例一般是可改變的,但是所有返回的對象在功能上是等同的。當其中一個返回對象發生變化的時候,所有其他的返回對象也要發生變化。因為它們是由同一個Map實例支撐的。 雖然創建keySet視圖對象的多個實例并無害處,卻也是沒有必要的。 5.在Java1.5發行版本中,有一種創建多余對象的新方法,稱作自動裝箱autoboxing。它允許程序員將基本類型和裝箱類型Boxed primitive Type混用,暗需要自動裝箱和拆箱。自動裝箱使得基本類型和裝箱基本類型的差別變的模糊起來,但是并沒有完全消除。他們在語義上還有這微妙的區別,在性能上也有著比較明顯的差別。 下面這段程序,計算所有int正值的總和,用long,因為int不夠大: // 這里用的是Long,程序運行會非常慢,因為程序大約構造了2|31個多余的Long實例 //Long sum = 0L; long sum = 0L; for(long i = 0;i < Integer.MAX_VALUE;i++) { sum += i; } 將sum的聲明從Long變為long,程序運行時間會減慢很多。結論很明顯:要優先使用基本類型而不是裝箱基本類型,要當心無意識的自動裝箱。 6.不要錯誤的認為本條目所介紹的內容暗示著創建對象的代建非常昂貴,我們 應該盡可能避免的創建對象。相反,由于小對象的構造器只做少量的顯示工作,所以小對象的創建和回收動作是非常廉價的,特別是在現代的jvm實現更是如此。通過創建附加的對象,提升程序的清晰性,簡潔行和功能性,這通常是件好事。 7.反之通過維護自己的對象池object pool來避免創建對象并不是一種好的做法,除非池中的對象是非常重量級的。真正正確使用對象池的典型對象示例就是數據庫連接池。建立數據庫連接的代碼是非常昂貴的,因此重用這些對象非常有意義。而且數據庫的許可可能限制你只能使用一定數量的連接。但是一般而言,維護自己的對象池必定會把代碼弄的很亂,同時增加內存使用footprint,并且還會損害性能。現代的JVM實現具有高度優化的垃圾回收器,其性能很容易就會超出輕量級對象池的性能。 8.與本條目對應的是保護性拷貝defensive copying的內容。本題目提及:當你應該重用現有對象的時候,請不要創建新對象。而39條則說,你應該創建新對象的時候,請不要重用現有對象。注意,在提倡保護性拷貝的時候,因重用對象而付出的代碼要遠遠大于因創建重復對象而付出的代價。必要時如果沒能實施保護性考慮,將會導致潛在的錯誤和安全漏洞,而不必要地創建對象則只會影響程序的風格和性能。
部分源碼:
package com.book.chap2.privateConstructor;


/** *//**
*
*工具類
*<p>因為是工具類,所以不希望被實例化,而且實例對其沒有任何意義。</p>
*<p>采用私有構造器來防止實例化(副作用,不可被子類化)</p>
*<p>可在私有構造器中拋出異常避免不小心在類的內部調用構造器</p>
*
*@author landon
*@since 1.6.0_35
*@version 1.0.0 2013-1-10
*
*/

public class UtilityClass


{
//Supress default constructor for noninstantiability
private UtilityClass()

{
throw new AssertionError();
}
}

package com.book.chap2.avoidCreateUnnecessaryObject;


/** *//**
*
*自動裝箱問題
*
*@author landon
*@since 1.6.0_35
*@version 1.0.0 2013-1-24
*
*/

public class AutoBoxingProblem


{
public static void main(String
args)

{
// 這里用的是Long,程序運行會非常慢,因為程序大約構造了2|31個多余的Long實例
//Long sum = 0L;
long sum = 0L;
for(long i = 0;i < Integer.MAX_VALUE;i++)

{
sum += i;
}
System.out.println(sum);
}
}

package com.book.chap2.avoidCreateUnnecessaryObject;

import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;


/** *//**
*
* 檢查一個人是否出生于1946-1964的生育期高峰
*
* @author landon
* @since 1.6.0_35
* @version 1.0.0 2013-1-23
*
*/

public class Person


{
private final Date birthDate;

public Person(Date birth)

{
birthDate = birth;
}


/** *//**
*
* 是否出生在高峰期
* <p>
* 該方法每次調用的時候都會創建一個Calendar,一個TimeZone和兩個Date實例,這都是不必要的
*
* @return
*/
public boolean isBabyBoomer()

{
Calendar gmtCalendar = Calendar
.getInstance(TimeZone.getTimeZone("GMT"));

// 1946
gmtCalendar.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCalendar.getTime();

gmtCalendar.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCalendar.getTime();

return birthDate.compareTo(boomStart) >= 0
&& birthDate.compareTo(boomEnd) <= 0;
}

// 對于以上情況,采用靜態初始化器,避免上面這種效率低下的情況
private static final Date BOOM_START;
private static final Date BOOM_END;

// 靜態初始化器
static

{
Calendar gmtCalendar = Calendar
.getInstance(TimeZone.getTimeZone("GMT"));

// 1946
gmtCalendar.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCalendar.getTime();

gmtCalendar.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCalendar.getTime();
}


/** *//**
*
* 第二種方法判斷是否生在高峰期
* <p>改進后的方法只在初始化的時候創建Calender,TimeZone和Date實例一次。如果該方法頻繁調用,則會顯著的提高性能。
* <p>除了提高性能之外,代碼的含義也更清晰了,BOOM_START和BOOM_END從局部變量改為final靜態域,顯然就應該作為常 *
* 量對待。
* 從而使代碼更易于理解。
@return
*/
public boolean isBabyBoomer2()

{
return birthDate.compareTo(BOOM_START) >= 0
&& birthDate.compareTo(BOOM_END) <= 0;
}
}

posted on 2013-03-15 16:10
landon 閱讀(1826)
評論(0) 編輯 收藏 所屬分類:
Program 、
Book