【原創】《Effective Java》讀書筆記

《Effective Java》這本書介紹了Java編程中78條極具使用價值的經驗規劃,包括創建和銷毀對象,類和接口,泛型枚舉,異常,并發,序列化等等內容,可以針對性地有選擇閱讀。本書主要針對Java,具體到android中要自己考慮實際應用。

第二章 創建和銷毀對象

第5條:避免創建不必要的對象

一般來說,最好能重用對象而不是在每次需要的時候就創建一個相同功能的新對象

反例一:

String string =newString("stringette”);

傳遞給String構造器的參數"stringent”本身就是一個String實例

String string ="stringent";

只用了一個String實例,而不是每次都創建一個新的實例

在同一個java虛擬機中的代碼,只要它們包含相同的字符串常量,該對象就會被重用

除了重用不可變的對象外,也可以重用那些已知不會被修改的可變對象

反例二:

現在有一個類是Person,并有一個isBabyBoomer方法,用來檢驗這個類的每個對象是否為一個“baby boomer”(生育高峰期出生的小孩),換句話說,就是檢驗這個人是否出生于1946年至1964年期間。

public classPerson {

private finalDatebirthDate;

private booleanisBabyBoomer() {

Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));

gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);

Date boomStart = gmtCal.getTime();

gmtCal.set(1965, Calendar.JANUARY,1,0,0,0);

Date boomEnd = gmtCal.getTime();

returnbirthDate.compareTo(boomStart) >=0&&

birthDate.compareTo(boomEnd) <0;

}

}

這里涉及可變的Date對象,它的值一旦計算出來之后就不再變化。isBabyBoomer每次被調用的時候,都會新建一個Calendar,一個TimeZone和和兩個Date實例,這是不必要的。

private finalDatebirthDate;

private static finalDateBOOM_START;

private static finalDateBOOM_END;

static{

Calendar gmtCal = Calendar.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();

}

private booleanisBabyBoomer() {

returnbirthDate.compareTo(BOOM_START) >=0&&

birthDate.compareTo(BOOM_END) <0;

}

改進后只在初始化的時候創建Calendar,TimeZone和Date實例各一次,而不是在每次調用isBabyBoomer時候都創建這樣的實例。如果isBabyBoomer被頻繁調用,這種方法將顯著提升性能。

反例三:

自動裝箱方法

Long sum =0L;

for(longi =0; i

sum += i;

}

System.out.println(sum);

這段程序答案是正確的,但是比實際情況更慢一點,因為打錯了一個字符。

應該優先使用基本類型而不是裝箱基本類型,要當心無意識的自動裝箱。

當你應該重用現有對象的時候,請不要創建新的對象

第三章 對于所有對象都通用的方法

第8條:覆蓋equals時請遵守通用約定

第9條:覆蓋equals時總要覆蓋hashCode

第10條:始終要覆蓋toString

第11條:謹慎地覆蓋clone

盡管Object是一個具體類,但是設計它主要是為了擴展。它所有的非final方法(equals, hashCode, toString, clone和finalize)都有明確的通用約定,因為它們被設計成是要被覆蓋的。任何一個類,它在被覆蓋的時候,都有責任遵守這些通用約定;如果不能做到這一點,其它依賴這些約定的類(HashMap和HashSet)就無法一起正常工作。

public static classContactInfo {

publicStringname="";

publicStringnumber="";

publicStringavatarUriStr="";

publicObjectcustomInfo= Boolean.FALSE;

private volatile inthash code;

}

實現高質量equals方法的訣竅:

1.使用==操作符檢查參數是否為這個對象的引用

2.使用instanceof操作符檢查參數是否為正確的類型

3.把參數轉換成正確的類型

4.對于該類中的每個關鍵域,檢查參數中的域是否與該對象中對應的域相匹配

@Override

public booleanequals(Object obj) {

if(obj ==this) {

return true;

}

if(!(objinstanceofContactInfo)) {

return false;

}

ContactInfo contactInfoObj = (ContactInfo)obj;

returnTextUtils.equals(name, contactInfoObj.name)

&& TextUtils.equals(number, contactInfoObj.number);

}

原則:

自反性:對象必須等于自身,x.equals(x) = true

對稱性:任何兩個對象對于”它們是否相等“必須保持一致 ?y.equals(x) = x.equals(y)

傳遞性:A對象等于B對象,B對象等于C對象,那么A對象一定等于C對象 x.equals(y) = true ?y.equals(z) = true, so x.equals(z) = true

一致性:任何兩個相等的對象在沒有被修改的情況下必須始終相等

非空性:所有的對象必須不等于null

每個覆蓋了equals方法的類中,也必須覆蓋hashCode方法,不然違反Object.hashCode通用約定,HashMap, HashSet等出bug

@Overridepublic inthashCode() {

intresult =hashcode;

if(result == 0) {

result = 17;

result = 31 * result +name.hashCode();

result = 31 * result +number.hashCode();

hashcode= result;

}

returnresult;

}

高質量散列函數的解決方法:

1.把某個非0常數值,例如17(奇素數),保存在一個名為result的int類型的變量中

2.對于對象中每個equals方法涉及的關鍵域f:

a.為該域計算int類型的散列碼c:

boolean類型的域(如名叫f)????? 計算(f?1:0)

byte,char,short,int類型的域????? 計算 (int)f

long類型的域????????????????????????? 計算 (int)(f^(f>>>32))

float類型的域????????????????????????? 計算 Float.floatToIntBits(f)

double類型的域?????????????????????? 計算? Double.doubleToLongBits(f)

引用類型的域?????????????????????????? 使用 引用類型的hashCode方法得到的散列值

數組類型的域?????????????????????????? 把數組中的每個元素當成一個關鍵域,計算出散列值。

b.對于每一個關鍵域計算出來的散列值 (如名叫c)

result = result * 31 + c;

3.最后返回這個result整數

原則:為不相等的對象產生不相等的散列碼

@Override

publicString toString() {

returnname+number;

}

clone方法:

x.clone().getClass() == x.getClass()在父類子類的情況會出現歧義,子類的getClass和父類的getClass應該相等嗎?

不建議覆蓋,對于一個專門為繼承而設計的類,如果你未能提供行為良好的受保護的clone方法,它的子類就不可能實現Cloneable接口。

第四章 類和接口

第14條:在公有類中使用訪問方法而非公有域

如果類可以在它所在的包的外部進行訪問,就提供訪問方法;

如果公有類暴露了它的數據域,在將來想改變將會很困難,因為訪問它的代碼已經遍布各處了。

如果類是包級私有的,或者是私有的嵌套類,直接暴露它的 數據域并沒有本質的錯誤。

這些代碼被限定在類的內部,不必改變包之外的代碼就可以修改

反例:

Java類庫中的java.awt包中Point和Dimension類,特別是Dimension類,內部數據的暴露造成了嚴重的性能問題,而且這個問題至今仍然存在。

第16條:復合優先于繼承

不當地使用繼承會讓代碼變得很脆弱,因為繼承打破了封裝性。子類是依賴于父類的特定功能而實現的,但父類有可能隨著發行版本而變化,如果真的發生了變化,子類就有可能遭到破壞。

解決方案:復合,不用擴展現有類,而是把現有類作為新類的一個實例。

第18條:接口優于抽象類

接口和抽象類最明顯的區別:

抽象類允許包含某些方法的實現,接口不允許。

更加重要的區別:

為了實現抽象定義的類型,類必須成為抽象類的一個子類。

任何一個類,只要它定義了所有必要的方法,并且遵守通用約定,它就被允許實現一個接口,不管這個類是處于類層次中的哪一個位置。

因為Java只允許單繼承,所以,抽象類作為類型定義受到了極大的限制。而實現新接口則沒有限制。

但是公有接口的設計必須非常謹慎,接口一旦被公開發行并被廣泛實現,再想改變這個接口幾乎是不可能的。

第五章 泛型

第25條:列表優先于數組

數組是協變的(covariant),泛型是不可變的(invariant)

//編譯可通過,運行crash

Object[] objectArray =newLong[1];

objectArray[0] ="String”;

//編譯報錯

List list =newArrayList();

list.add("string”);

泛型是編譯時類型安全的,數組是運行時類型安全的,數組有缺陷

第六章 枚舉和注解

第七章 方法

第39條:必要時進行保護性拷貝

JAVA相比C和C++而言是一門安全的語言,它對于緩沖區溢出、數組越界、非法指針以及其他內部破壞的錯誤都自動免疫。在一門安全的語言中,在設計類的時候,可以確切地知道,無論系統的其他部分發生什么事情,這些類的約束都可以保持為真。對于那些把所有內存當成一個巨大數組來看待的語言來說,這是不可能的。但即使在安全的語言中,仍然需要適當的保護。

public? class?? Period{

private?? final?? Date?? startTime;

private?? finale? Date?? endTime;

public?? Period(Date? startTime , Date? endTime){

if(startTime.compareTo(endTime) > 0){

throw?? new? IllegalArgumentException(“startTime? after? endTime !”);

}

this.startTime = startTime;

this.endTime? = endTime;

}

pubilc?? Date?? start(){

return ?this.startTime ;

}

public?? Date? end(){

return? this.endTime? ;

}

}

這個類貌似是一個不可變類 ,因為startTime和endTime域都是final的,還加了保護開始時間必須在結束時間之前,但是它并不是一個嚴格的不可變類,因為Date類并不是一個不可變類。

這樣就會出問題:

攻擊一:

Date? startTime?? =? new Date();

Date? endTime?? = new? Date();

Period? per =? new? Period(startTime ,? endTime );

endTime.setYear(78);

為了保護內部信息避免受到攻擊,對于構造器內的每個可變參數進行保護性拷貝是必要的,并且使用備份對象作為Period實例的組件,而不使用原始的對象:

防御一:

public?? Period(Date?? startTime , Date? endTime ){

this.startTime? =new Date (startTime.getTime());

this.endTime?? =new? Date(endTime.getTime());

if(this.startTime.compareTo(this.endTime)? >? 0){

throw?? new??? IllegalArgumentException(“startTime? after?? endTime !”);

}

}

保護性拷貝是在檢查參數的有效性之前進行的,這是十分必要的

攻擊二:

Date? startTime?? =? new Date();

Date? endTime?? = new? Date();

Period? per =? new? Period(startTime ,? endTime );

per.end().setYear(78);

還需要修改對成員變量的訪問,增加保護性拷貝:

防御二:

public? Date?? start(){

return?? new? Date(startTime.getTime());

}

public? Date? end(){

return? new?? Date(endTime.getTime());

}

在采用了新的訪問器和新的訪問方法之后,Period真正不可變了。

第43條:返回零長度的數組或者集合,而不是null

private final List cheesesInStock =?…;

public Cheese[] getCheeses() {

if (cheesesInStock.size() == 0) {

return null;

}

}

把沒有奶酪可買的情況當做是一種特例,這是不合常理的。這樣做客戶端必須有專門的邏輯去處理null值。很容易出錯。

有觀點認為返回null比零長度數組更好,因為它避免了分配數組所需要的開銷,但是這個級別上的性能擔心是多余的。

第八章 通用程序設計

第45條:將局部變量的作用域最小化

將局部變量的作用域最小化可以增加代碼的可讀性和可維護性,并降低出錯的可能性

第48條:如果需要精確的答案,請避免使用float和double

float和double是二進制浮點運算,并沒有提供完全精確的結果,不應該被用于需要精確結果,尤其是貨幣運算的情況。

例如,假設你的口袋里有$1,看到貨架上有一排糖果,標價分別為$0.1, $0.2, $0.3等等,一直到$1, 你打算從標價為$0.1的糖果開始買起,每種買一顆,一直到不能支持貨架上下一鐘糖果的價格為止,那么你可以買多少可糖果呢?

public static void main(String[] args) {

double funds = 1.00;

int itemsBought = 0;

for(double price = .10; funds >= price; price += .10) {

funds -= price;

itemsBought++;

}

System.out.println(itemsBought + " items bought.");

System.out.println("Money left over: $" + funds);

}

這段程序的運行結果是只能買3顆糖果,并且還剩下$0.39999999999

解決問題的正確辦法是使用BigDecimal、int或者long進行貨幣計算。

第49條:基本類型優先于裝箱基本類型

Java有一個類型系統由兩部分組成,包含基本類型,int、double和boolean,和引用類型,如String和List。每個基本類型都有一個對應的引用類型,稱作裝箱基本類型,例如Interger、Double和Boolean。

基本類型和裝箱類型的區別:

1.基本類型只有值,裝箱類型有與值不同的同一性(==)。

兩個裝箱基本類型可以有相同的值和不同的同一性。

2.基本類型只有功能完備的值,裝箱類型還有null

3.基本類型比裝箱類型更節省時間和空間

問題1:

Comparator naturalOrder =newComparator() {

publicintcompare(Integer first, Integer second) {

returnfirst < second ? -1 : (first == second ? 0 : 1);

}

};

如果打印naturalOrder.compare(new Integer(42), new Integer(42))的值,結果不是期望的0,而是1,表明第一個Integer值大于第二個。

問題出現在執行first == second,它在兩個對象引用上執行同一性比較,如果first和second引用表示同一個int值的不同Integer實例,這個比較操作會返回false,比較器會錯誤地返回1,對裝箱基本類型運用==操作符幾乎總是錯誤的。

修正:

Comparator naturalOrder = new Comparator() {

public int compare(Integer first, Integer second) {

int f = first;

int s??= second;

return f < s ? -1 : (f == s ? 0 : 1);

}

};

問題2:

public class Unbelievable {

static Integer i;

public static void main(String[] args) {

if(42 == i)

System.out.println("Unbelievable");

}

}

它不是打印出Unbelievable,而是拋出NullPointException異常,問題在于i是個Integer,不是int,它的初始值是null而不是0,當計算(i == 42)時,將Integer和int進行比較,裝箱基本類型自動拆箱,如果null對象被自動拆箱,就只能得到Nu'llPointException了。

修正的方法把i聲明為int。

總結:

裝箱基本類型適用于集合中的鍵值對以及泛型中,在其他可以選擇的情況下,基本類型要優于裝箱基本類型。

第53條:接口優于反射機制

反射機制允許一個類使用另一個類,即使當前者被編譯的時候后者還根本不存在,然而,這種能力也要付出代價:

1.喪失了編譯時類型檢查的好處

2.執行反射訪問所需要的代碼非常笨拙和冗長

3.性能損失 ?2~50倍

如果只是以非常有限的形式使用反射機制,雖然也要付出少許代價,但是可以獲得許多好處。

最好的解決方式是以反射的方式創建實例,然后通過它們的接口或者超類,以正常的方式訪問這些實例。如果適當的構造器不帶參數,甚至根本不需要使用java.lang.reflect包;Class.newInstance方法就已經提供了所需的功能。

下面的程序創建一個Set實例,它的類是由第一個命令行參數指定的。該程序把其余的命令行參數插入到這個集合中,然后打印該集合。如果是HashSet以隨機的方式打印出來,如果是TreeSet按照字母順序打印出來的程序:

public static void main(String[] args) {

Class c = null;

try {

c = Class.forName(args[0]);

} catch(ClassNotFoundException e) {

System.out.println("Class not found");

System.exit(1);

}

Set s = null;

try {

s = (Set) c.newInstance();

} catch(IllegalAccessException e) {

System.out.println("Class not accessible");

System.exit(1);

} catch(InstantiationException e) {

System.out.println("Class not instantiable");

System.exit(1);

}

s.addAll(Arrays.asList(args).subList(1, args.length));

System.out.println(s);

}

缺點:

1.需要加三個cache保護

2.代碼冗長

第54條:謹慎地使用本地方法

Java Native Interface(JNI)允許java應用程序調用本地方法(native method),指用本地程序設計語言(C或C++)來編寫的特殊方法。

使用本地方法來提高性能的做法不值得提倡。現在JVM越來越快,對于大多數任務,即使不使用本地方法也可以獲得與之相當的性能。

使用本地方法有一些嚴重的缺點:

1.因為本地方法不是安全的,使用它不能避免受內存毀壞錯誤的影響。

2.本地方法與平臺相關,使用本地方法不再可以自由移植。

3.更難調試

4.在進入和退出本地代碼時會產生固定開銷

5.如果本地代碼只做少量工作,反而會降低性能

6.某些需要膠合代碼的本地方法編起來難以閱讀

總而言之,使用本地方法前務必三思。

第55條:謹慎地進行優化

優化的三條格言:

1.很多計算上的過失都歸咎于效率(沒有必要達到的效率),而不是任何其他的原因,包括盲目地做傻事.

2.不要去計較效率上的小小得失,在97%的情況下,不成熟的優化才是一切問題的根源

3.優化方面我們遵守兩條原則:

a.不要進行優化

b.針對于專家,還是不要進行優化,也就是說,在你還沒有絕對清晰的優化方案之前,請不要優化.

它們講述了關于優化的深刻真理:優化的弊大于利。

不要為了性能而犧牲合理的結構,要努力寫好的程序而不是快的程序。

如果好的程序不夠快,它的結構將使它可以得到優化。但是遍布全局并且限制性能的結構缺陷幾乎是不可能被改正的,除非重寫。

反例:

java.awt.Component類中的getSize方法,Dimension實例是直接暴露的,由于Dimension是可變的,迫使與它有關的任何實現都必須分配一個新的實例,過多的分配會導致性能問題。

解決方案:

1.Dimension設置為不可變

2.用兩個方法來替換getSize方法,分別返回Dimension對象的單個基本組件

但是之前設計失誤的性能影響一直存在

“不要進行優化”:

試圖優化通常對性能沒有明顯影響,有時甚至更差。主要原因是要猜出程序哪里花時間并不容易,要多多使用剖析工具。優化的第一個步驟是檢查所選擇的算法,再多的底層優化也無法彌補算法選擇的不當。必要時重復這個過程,每次改變完之后都要測試性能,直到滿意為止。

總而言之,不要費力去編寫快速的程序,應該努力編寫好的程序,速度自然會隨之而來。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容