🍊 Java學習:Java從入門到精通總結
🍊 Spring系列推薦:Spring原始碼決議
📆 最近更新:2022年1月5日
🍊 個人簡介:通信工程本碩💪、阿里新晉猿同學🌕,我的故事充滿機遇、挑戰與翻盤,歡迎關注作者來共飲一杯雞湯
🍊 點贊 👍 收藏 ?留言 📝 都是我最大的動力!
豆瓣評分9.8的圖書《Effective Java》,是當今世界頂尖高手Josh Bloch的著作,在我之前的文章里我也提到過,編程就像練武,既需要外在的武功招式(編程語言、工具、中間件等等),也需要修煉心法(設計模式、原始碼等等)學霸、學神OR開掛,
我也始終有一個觀點:看視頻跟著敲代碼永遠只是入門,從書籍里學到了多少東西才決定了你的上限,

我個人在Java領域也已經學習了近5年,在修煉“內功”的方面也通過各種途徑接觸到了一些編程規約,例如阿里巴巴的泰山版規約,在此基礎下讀這本書的時候仍是讓我受到了很大的沖激,學習到了很多約定背后的細節問題,還有一些讓我欣賞此書的點是,書中對于編程規約的解釋讓我感到十分受用,并愿意將他們應用在我的作業中,也提醒了我要把閱讀JDK原始碼的任務提上日程,
最后想分享一下我個人目前的看法,內功修煉不像學習一個新的工具那么簡單,其主旨在于踏實,深入探索底層原理的程序很緩慢并且是艱辛的,但一旦開悟,修為一定會突破瓶頸,達到更高的境界,這遠遠不是我通過一兩篇博客就能學到的東西,
接下來就針對此書列舉一下我的識訓與思考,
不過還是要吐槽一下的是翻譯版屬實讓人一言難盡,有些地方會有誤導的效果,你比如java語言里extends是繼承的關鍵字,書本中全部翻譯成了擴展 就完全不是原來的意思了,所以建議有問題的地方對照英文原版進行語意上的理解,
沒有時間讀原作的同學可以參考我這篇文章,
69 只針對例外的情況才使用例外
try {
int i = 0;
while ( true )
range[i++].climb();
} catch ( ArrayIndexOutOfBoundsException e ) {
}
當這個回圈企圖訪問陣列邊界之外的第一個陣列元素的時候,使用try-catch并且忽略ArrayIndexOutOfBoundsException例外的手段來達到終止回圈的目的,
對于大多數程式員來說,下面的標準模式可讀性就很高:
for ( Mountain m : range )
m.climb();
第一串代碼企圖使用Java的錯誤判斷機制來提高程式性能,因為VM對每次陣列訪問都要檢查越界情況,這種想法有三個錯誤:
- 因為例外設計的初衷適用于不正常的情形,所有幾乎沒有JVM實作試圖對他們進行優化
- 把代碼放在try-catch塊中反而阻止了現代JVM實作本可能執行的某些特定優化
- 對資料進行標準的for遍歷并不會導致冗余的檢查
實際上基于例外的模式比標準模式要慢得多
例外應該只用于例外的情況下;他們永遠不應該用于正常的程式控制流程,
設計良好的API不應該強迫它的客戶端為了正常的控制流程而使用例外,如果類中具有「狀態相關」(state-dependent)的方法,這個類也應該具有一個單獨的「狀態測驗」(state-testing)方法,即表明是否可以呼叫這個狀態相關的方法,比如Iterator介面含有狀態相關的next方法,以及相應的狀態測驗方法hasNext,
for ( Iterator<Foo> i = collection.iterator(); i.hasNext(); ){
Foo foo = i.next();
...
}
如果Iterator缺少hasNext方法,客戶端將被迫改用下面的做法:
try {
Iterator<Foo> i = collection.iterator();
while ( true ){
Foo foo = i.next();
...
}
} catch ( NoSuchElementException e ) {}
另外一種提供單獨狀態測驗的做法是,如果「狀態相關」方法無法執行想要的計算,就可以讓它回傳一個零?度的optional值,或者回傳一個可識別的null,
- 如果物件將在缺少外部同步的情況下被并發訪問,或者可被外界改變狀態,就必須使用「optional或者可識別的null」
- 如果單獨的「狀態測驗」方法必須重復「狀態相關」方法的作業,從性能的?度考慮,就必須使用可被識別的回傳值
- 其他情況,「狀態測驗」方法優于可被識別的
null
70 對可恢復的情況使用受檢例外,對編程錯誤使用運行時例外
Java 程式設計語言提供了三種 throwable:受檢例外(checked exceptions)、運行時例外(runtime exceptions)和錯誤(errors),對于什么情況適合使用哪種 throwable,是有一些一般性的原則提出了強有力的指導:
如果期望呼叫者能夠合理的恢復程式運行,對于這種情況就應該使用受檢例外,通過拋出受檢例外,強迫呼叫者在一個 catch 子句中處理該例外,或者把它傳播出去,因此,方法中宣告要拋出的每個受檢例外都是對 API 用戶的一個潛在提示:與例外相關聯的條件是呼叫這個方法一種可能結果,
運行時例外和錯誤,在行為上兩者是等同的:它們都是不需要也不應該被捕獲的 throwable,
用運行時例外來表明編程錯誤,大多數運行時例外都表示 API 的客戶沒有遵守 API 規范建立的約定,例如陣列越界拋ArrayIndexOutOfBoundsException,
考慮資源枯竭的情形,這可能是由程式錯誤引起的,比如分配了一塊不合理的過大陣列,也可能確實是由于資源不足而引起的,如果資源枯竭是由于臨時的短缺,或是臨時需求太大造成的,這種情況可能是可恢復的,API 設計者需要判斷這樣的資源枯竭是否允許恢復,如果你相信一種情況可能允許回復,就使用受檢例外;如果不是,則使用運行時例外,如果不清楚是否有可能恢復,最好使用非受檢例外,
好不要實作任何新的 Error 的子類,實作的所有非受檢的 throwable 都應該是RuntimeExceptiond子類,也不應該拋出AssertionError例外,
因為受檢例外往往指明了可恢復的條件,所以對于這樣的例外,提供一些輔助方法尤其重要,通過這種方法呼叫者可以獲得一些有助于程式恢復的資訊,例如,假設因為用戶資金不足,當他企圖購買一張禮品卡時導致失敗,于是拋出受檢例外,這個例外應該提供一個訪問方法,以便允許客戶查詢所缺的費用金額,
71 避免不必要的使用受檢例外
如果呼叫者無法恢復失敗,就應該拋出未受檢例外,如果可以恢復,并且想要迫使呼叫者處理例外的條件,首選應該回傳一個optional值,當且僅當萬一失敗時,這些無法提供足夠的資訊,才應該拋出受檢例外,
受檢例外強迫程式員處理例外的條件,大大增強了可靠性,過分使用受檢例外會使API使用起來非常不方便,如果方法拋出受檢例外,呼叫該方法代碼就必須在catch塊中處理,或者拋出例外,
這種負擔在Java 8中更重了,因為拋出受檢例外的方法不能直接在Stream中使用,
除非下面兩種情況同時成立,否則更適合使用未受檢例外:
-
正確地使用API并不能阻止這種例外條件的產生
-
一旦產生例外,程式員可以立即采取有效動作
作為一個石蕊測驗,你可以試著問自己:程式員將如何處理該例外,下面的做法是最好的嗎?
} catch ( TheCheckedException e ) {
throw new AssertionError(); /* Can't happen! */
}
下面這種做法又如何?
} catch ( TheCheckedException e ) {
e.printStackTrace(); /* Oh well, we lose. */
System.exit( 1 );
}
如果覺得上面兩種方式都不行的話,還是采用未受檢的例外可能更合適,
石蕊測驗:簡單而具有決定性的測驗
如果方法拋出的受檢例外是唯一的,它給程式員帶來的額外負擔就會非常高:該方法必須放置于一個try塊中,并且不能在Stream里用,
消除受檢例外最容易的方法是,回傳所要的結果型別的一個optional,但缺點是方法無法回傳任何額外的詳細資訊
「把受檢例外變成未受檢例外」的一種方法是,把這個拋出例外的方法分成兩個方法,其中第一個方法回傳一個boolean值,表明是否應該拋出例外,例如:
try {
obj.action( args );
} catch ( TheCheckedException e ) {
... /* Handle exceptional condition */
}
被重構為:
if ( obj.actionPermitted( args ) ) {
obj.action( args );
} else {
... /* Handle exceptional condition */
}
如果程式員知道呼叫將會成功,或者不介意由于呼叫失敗而導致的執行緒終止,這種重構還允許以下這個更為簡單的呼叫形式:
obj.action(args);
如果物件將在缺少外部同步的情況下被并發訪問,或者可被外界改變狀態,這種重構就是不恰當的,因為在actionPermitted和action這兩個呼叫的時間間隔之中,物件的狀態有可能會發生變化,
72 優先使用標準的例外
Java平臺類別庫提供了一組基本的未受檢例外,它們滿足了絕大多數API的例外拋出需求,
重用標準的例外有多個好處:
- 使API更易于學習和使用
- 對于用到這些API程式而言,它們的可讀性會更好
- 例外類越少,意味著記憶體占用(footprint)就越小,裝載這些類的時間開銷也越少
以下是四種常用的例外:
IllegalArgumentException:
- 當呼叫者傳遞的引數值不合適的時候,往往就會拋出這個例外,比如,假設某一個引數代表了“某個動作的重復次數”,如果程式員給這個引數傳遞了一個負數,就會拋出這個例外,
IllegalStateException:
- 如果因為接收物件的狀態而使呼叫非法,通常就會拋出這個例外,例如,如果在某個物件被正確地初始化之前,呼叫者就企圖使用這個物件,就會拋出這個例外,
ConcurrentModificationException:
- 如果檢測到一個專?設計用于單執行緒的物件,或者與外部同步機制配合使用的物件正在(或已經)被并發地修改,就應該拋出這個例外,
UnsupportedOperationException:
- 如果物件不支持所請求的操作,就會拋出這個例外,
| 例外 | 使用場合 |
|---|---|
| IllegalArgumentException | 非null的引數值不正確 |
| IllegalStateException | 不適合方法呼叫的物件狀態 |
| NullPointerException | 在禁止使用null的情況下引數值為null |
| IndexOutOfBoundsExecption | 下標引數值越界 |
| ConcurrentModificationException | 在禁止并發修改的情況下,檢測到物件的并發修改 |
| UnsupportedOperationException | 物件不支持用戶請求的方法 |
不要直接重用Exception、RuntimeException、Throwable或者Error,對待這些類要像對待抽象類
一樣,
如果希望稍微增加更多的失敗一捕獲(failure-capture)資訊,可以放心地子類化標準例外,但要記住例外是可序列化的
73 拋出與抽象對應的例外
更高層的實作應該捕獲低層的例外,同時拋出可以按照高層抽象進行解釋的例外,這種做法稱為例外轉譯(exception translation),如下代碼所示:
try {
... /* Use lower-level abstraction to do our bidding */
} catch (LowerLevelException e) {
throw new HigherLevelException(...);
}
下面的例外轉譯例子取自于AbstractSequentialList類,該類是List介面的一個?架實作(skeletal implementation)
/**
* Returns the element at the specified position in this list.
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= size()}).
*/
public E get(int index) {
ListIterator<E> i = listIterator(index);
try {
return(i.next() );
} catch (NoSuchElementException e) {
throw new IndexOutOfBoundsException("Index: " + index);
}
}
一種特殊的例外轉譯形式稱為例外鏈(exception chaining),如果低層的例外對于除錯導致高層例外的問題非常有幫助,使用例外鏈就很合適,低層的例外(原因)被傳到高層的例外,高層的例外提供訪問方法(Throwable的getCause方法)來獲得低層的例外:
// Exception Chaining
try {
... // Use lower-level abstraction to do our bidding
} catch (LowerLevelException cause) {
throw new HigherLevelException(cause);
}
高層例外的構造器將原因傳到支持鏈(chaining-aware)的超級構造器,因此它最終將被傳給Throwable的其中一個運行例外鏈的構造器,例如Throwable(Throwable t) :
/* Exception with chaining-aware constructor */
class HigherLevelException extends Exception {
HigherLevelException( Throwable cause ) {
super(cause);
}
}
對于沒有支持鏈的例外,可以利用Throwable的initCause方法設定原因,例外鏈不僅讓你可以通程序式(用getCause)訪問原因,還可以將原因的堆戰軌跡集成到更高層的例外中,
盡管例外轉譯與不加選擇地從低層傳遞例外的做法相比有所改進,但是也不能濫用它,
處理來自底層的例外通常有兩種方法:
-
(推薦)在呼叫低層方法之前確保它們會成功執行,可以在給低層傳遞引數之前,檢查更高層方法的引數的有效性
-
讓更高層來悄悄地處理這些例外,從而將高層方法的呼叫者與低層的問題隔離開來,可以用某種適當的記錄機制(如
java.util.logging)將例外記錄下來,這樣有助于管理員調查問題,同時又將客戶端代碼和最終用戶與問題隔離開來,
74 每個方法拋出的例外都需要創建檔案
始終要單獨地宣告受檢例外,并且利用Javadoc的@throws標簽,準確地記錄下拋出每個例外的條件,
如果一個公有方法可能拋出多個例外類,則不要宣告它會拋出這些例外類的某個超類,
這條有一個例外:main方法它可以被安全地宣告拋出Exception,因為它只通過虛擬機呼叫,
對于方法可能拋出的未受檢例外,如果將這些例外資訊很好地組織成串列檔案,就可以有效地描述出這個方法被成功執行的前提條件,
對于介面中的方法,在檔案中記錄下它可能拋出的未受檢例外顯得尤為重要,這份檔案構成了該介面的通用約定(general contract)的一部分,它指定了該介面的多個實作必須遵循的公共行為,
使用Javadoc的@throws標簽記錄下一個方法可能拋出的每個未受檢例外,但是不要使用throws關鍵字將未受檢的例外包含在方法的宣告中
如果一個類中的許多方法出于同樣的原因而拋出同一個例外,在該類的檔案注釋中對這個例外建立檔案,這是可以接受的,
一個常?的例子是NullPointerException,若類的檔案注釋中有這樣的描述:「All methods in this class throw a NullPointerException if a null object reference is passed in any parameter」
75 在細節訊息中包含失敗 - 捕獲資訊
當程式由于未被捕獲的例外而失敗的時候,系統會自動地列印出該例外的堆疊軌跡,在堆疊軌跡中包含該例外的字串表示法(string representation),即它的toString方法的呼叫結果,它通常包含該例外的類名,緊隨其后的是細節訊息(detail message),
為了捕獲失敗,例外的細節資訊應該包含“與該例外有關”的所有引數和欄位的值,例如,IndexOutOfBoundsException例外的細節訊息應該包含下界、上界以及沒有落在界內的下標值,
注意兩點:
-
在診斷和修正軟體問題的程序中,許多人都可以看?堆疊軌跡,所以千萬不要在細節訊息中包含密碼、密鑰以及類似的資訊,
-
關于失敗的冗?描述資訊通常是不必要的,這些資訊可以通過閱讀源代碼而獲得,
為了確保在例外的細節訊息中包含足夠的失敗-捕捉資訊,一種辦法是在例外的構造器中引入這些資訊,例如 IndexOutOfBoundsException 中使用如下構造器代替 String 構造器:
/**
* Constructs an IndexOutOfBoundsException.
*
* @param lowerBound the lowest legal index value
* @param upperBound the highest legal index value plus one
* @param index the actual index value
*/
public IndexOutOfBoundsException( int lowerBound, int upperBound, int index ) {
// Generate a detail message that captures the failure
super(String.format("Lower bound: %d, Upper bound: %d, Index: %d",lowerBound, upperBound, index ) );
// Save failure information for programmatic access
this.lowerBound = lowerBound;
this.upperBound = upperBound;
this.index = index;
}
76 保持失敗的原子性
一般而言,失敗的方法呼叫應該使物件保持在被呼叫之前的狀態,具有這種屬性的方法被稱為具有失敗原子性(failure atomic),
有幾種途徑可以實作這種效果:
1. 設計一個不可變的物件
因為當每個物件被創建之后它就處于一致的狀態之中,以后也不會再發生變化,
2. 對于可變物件,在執行操作之前檢查引數的有效性
public Object pop() {
if ( size == 0 )
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; /* Eliminate obsolete reference */
return(result);
}
3. 調整計算處理程序的順序,使得任何可能會失敗的計算部分都在物件狀態被修改之前發生
以 TreeMap 的情形為例,它的元素被按照某種特定的順序做了排序,為了向 TreeMap 中添加元素,該元素的型別就必須是可以利用 TreeMap 的排序準則與其他元素進行比較的,如果企圖增加型別不正確的元素,在 tree 以任何方式被修改之前,自然會導致 ClassCastException 例外,
4. 在物件的一份臨時拷貝上執行操作,當操作完成之后再用臨時拷貝中的結果代替物件的內容
5. 撰寫一段恢復代碼(recovery code)
由它來攔截操作程序中發生的失敗,以及便物件回滾到操作開始之前的狀態上,這種辦法主要用于永久性的(基于磁盤的)資料結構,
如果兩個執行緒企圖在沒有適當的同步機制的情況下,并發地修改同一個物件,這個物件就有可能處在在不一致的狀態中,因此,不能在捕獲了ConcurrentModificationException例外之后再假設物件仍然是可用的,錯誤通常是不可恢復的,所以,當方法拋出 AssertionError 時,不需要再去保持失敗原子性,
77 不要忽略例外
要忽略一個例外非常容易,只需將方法呼叫通過try陳述句包圍起來,并包含一個空的catch塊:
try {
...
} catch ( SomeException e ) {
}
空的catch塊會使例外達不到應有的目的,每當?到空的catch塊時,應該警惕,
有些情形可以忽略例外,比如,關閉FileinputStream 的時候,因為你還沒有改變檔案的狀態,因此不必執行任何恢復動作,即使在這種情況下,把例外記錄下來還是明智的做法,可以調查例外的原因,如果選擇忽略例外,catch塊中應該包含一條注釋,說明為什么可以這么做,并且變數應該命名為ignored:
Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors = 4; // Default: guaranteed sufficient for any map
try {
numColors = f.get( 1L, TimeUnit.SECONDS );
} catch ( TimeoutException | ExecutionException ignored ) {
// Use default: minimal coloring is desirable, not required
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/404339.html
標籤:java
