唯品會Java開發手冊》1.0.2版閱讀

刺客伍六七發表於2020-10-12

《唯品會Java開發手冊》1.0.2版閱讀

1. 概述

《阿里巴巴Java開發手冊》,是首個對外公佈的企業級Java開發手冊,對整個業界都有重要的意義。

我們結合唯品會的內部經驗,參考《Clean Code》、《Effective Java》等重磅資料,增補了一些條目,也做了些精簡。

感謝阿里授權我們定製和再發布。

2. 規範正文

  1. 命名規

注意: 如需全文pdf版,請下載原始碼,在docs/standard/目錄執行merge.sh生成。

3. 規範落地

規則落地主要依靠程式碼格式模版與Sonar程式碼規則檢查

其中Sonar規則不如人意的地方,我們進行了定製。

4. 參考資料

5. 定製記錄

(一) 命名規約

Rule 1. 【強制】禁止拼音縮寫,避免閱讀者費勁猜測;儘量不用拼音,除非中國式業務詞彙沒有通用易懂的英文對應。

禁止: DZ[打折] / getPFByName() [評分]

儘量避免:Dazhe / DaZhePrice

Rule 2. 【強制】禁止使用非標準的英文縮寫

反例: AbstractClass 縮寫成 AbsClass;condition 縮寫成 condi。

Rule 3. 【強制】禁用其他程式語言風格的字首和字尾

在其它程式語言中使用的特殊字首或字尾,如_name, name_, mName, i_name,在Java中都不建議使用。


Rule 4. 【推薦】命名的好壞,在於其“模糊度”

1)如果上下文很清晰,區域性變數可以使用 list 這種簡略命名, 否則應該使用 userList 這種更清晰的命名。

2)禁止 a1, a2, a3 這種帶編號的沒誠意的命名方式。

3)方法的引數名叫 bookList ,方法裡的區域性變數名叫 theBookList 也是很沒誠意。

4)如果一個應用裡同時存在 Account、AccountInfo、AccountData 類,或者一個類裡同時有 getAccountInfo()、getAccountData(), save()、 store() 的函式,閱讀者將非常困惑。

5) callerIdcalleeIdmydearfriendswithamydearfriendswithb 這種拼寫極度接近,考驗閱讀者眼力的。


Rule 5. 【推薦】包名全部小寫。點分隔符之間儘量只有一個英語單詞,即使有多個單詞也不使用下劃線或大小寫分隔

正例: com.vip.javatool

反例: com.vip.java_tool, com.vip.javaTool

Rule 6. 【強制】類名與介面名使用UpperCamelCase風格,遵從駝峰形式

Tcp, Xml等縮寫也遵循駝峰形式,可約定例外如:DTO/ VO等。

正例:UserId / XmlService / TcpUdpDeal / UserVO

反例:UserID / XMLService / TCPUDPDeal / UserVo

Rule 7. 【強制】方法名、引數名、成員變數、區域性變數使用lowerCamelCase風格,遵從駝峰形式

正例: localValue / getHttpMessage();

Rule 8. 【強制】常量命名全大寫,單詞間用下劃線隔開。力求語義表達完整清楚,不要嫌名字長

正例: MAX_STOCK_COUNT 

反例: MAX_COUNT

例外:當一個static final欄位不是一個真正常量,比如不是基本型別時,不需要使用大寫命名。

private static final Logger logger = Logger.getLogger(MyClass.class);

例外:列舉常量推薦全大寫,但如果歷史原因未遵循也是允許的,所以我們修改了Sonar的規則。


Rule 9. 【推薦】如果使用到了通用的設計模式,在類名中體現,有利於閱讀者快速理解設計思想

正例:OrderFactory, LoginProxy ,ResourceObserver

Rule 10. 【推薦】列舉類名以Enum結尾; 抽象類使用Abstract或Base開頭;異常類使用Exception結尾;測試類以它要測試的類名開始,以Test結尾

正例:DealStatusEnum, AbstractView,BaseView, TimeoutException,UserServiceTest

Rule 11. 【推薦】實現類儘量用Impl的字尾與介面關聯,除了形容能力的介面

正例:CacheServiceImpl 實現 CacheService介面。

正例: Foo 實現 Translatable介面。

Rule 12. 【強制】POJO類中布林型別的變數名,不要加is字首,否則部分框架解析會引起序列化錯誤

反例:Boolean isSuccess的成員變數,它的GET方法也是isSuccess(),部分框架在反射解析的時候,“以為”對應的成員變數名稱是success,導致出錯。

Rule 13. 【強制】避免成員變數,方法引數,區域性變數的重名複寫,引起混淆

  • 類的私有成員變數名,不與父類的成員變數重名
  • 方法的引數名/區域性變數名,不與類的成員變數重名 (getter/setter例外)

下面錯誤的地方,Java在編譯時很坑人的都是合法的,但給閱讀者帶來極大的障礙。

public class A {
  int foo;
}

public class B extends A {
  int foo; //WRONG
  int bar;

  public void hello(int bar) { //WRONG
    int foo = 0; //WRONG
  }

  public void setBar(int bar) { //OK
    this.bar = bar;
  }
}

(二) 格式規約

Rule 1. 【強制】使用專案組統一的程式碼格式模板,基於IDE自動的格式化

1)IDE的預設程式碼格式模板,能簡化絕大部分關於格式規範(如空格,括號)的描述。

2)統一的模板,並在接手舊專案先進行一次全面格式化,可以避免, 不同開發者之間,因為格式不統一產生程式碼合併衝突,或者程式碼變更日誌中因為格式不同引起的變更,掩蓋了真正的邏輯變更。

3)設定專案組統一的行寬,建議120。

4)設定專案組統一的縮排方式(Tab或二空格,四空格均可),基於IDE自動轉換。


Rule 2. 【強制】IDE的text file encoding設定為UTF-8; IDE中檔案的換行符使用Unix格式,不要使用Windows格式


Rule 3. 【推薦】 用小括號來限定運算優先順序

我們沒有理由假設讀者能記住整個Java運算子優先順序表。除非作者和Reviewer都認為去掉小括號也不會使程式碼被誤解,甚至更易於閱讀。

if ((a == b) && (c == d))

Rule 4. 【推薦】類內方法定義的順序,不要“總是在類的最後新增新方法”

一個類就是一篇文章,想象一個閱讀者的存在,合理安排方法的佈局。

1)順序依次是:建構函式 > (公有方法>保護方法>私有方法) > getter/setter方法。

如果公有方法可以分成幾組,私有方法也緊跟公有方法的分組。

2)當一個類有多個構造方法,或者多個同名的過載方法,這些方法應該放置在一起。其中引數較多的方法在後面。

public Foo(int a) {...}
public Foo(int a, String b) {...}

public void foo(int a) {...}
public void foo(int a, String b) {...}

3)作為呼叫者的方法,儘量放在被呼叫的方法前面。

public void foo() {
    bar();
}

public void bar() {...}

Rule 5. 【推薦】通過空行進行邏輯分段

一段程式碼也是一段文章,需要合理的分段而不是一口氣讀到尾。

不同組的變數之間,不同業務邏輯的程式碼行之間,插入一個空行,起邏輯分段的作用。

而聯絡緊密的變數之間、語句之間,則儘量不要插入空行。

int width; 
int height; 

String name;

Rule 6.【推薦】避免IDE格式化

對於一些特殊場景(如使用大量的字串拼接成一段文字,或者想把大量的列舉值排成一列),為了避免IDE自動格式化,土辦法是把註釋符號//加在每一行的末尾,但這有視覺的干擾,可以使用@formatter:off和@formatter:on來包裝這段程式碼,讓IDE跳過它。

// @formatter:off
...
// @formatter:on

(三) 註釋規約

Rule 1.【推薦】基本的註釋要求

完全沒有註釋的大段程式碼對於閱讀者形同天書,註釋是給自己看的,即使隔很長時間,也能清晰理解當時的思路;註釋也是給繼任者看的,使其能夠快速接替自己的工作。

程式碼將被大量後續維護,註釋如果對閱讀者有幫助,不要吝嗇在註釋上花費的時間。(但也綜合參見規則2,3)

第一、能夠準確反應設計思想和程式碼邏輯;第二、能夠描述業務含義,使別的程式設計師能夠迅速瞭解到程式碼背後的資訊。

除了特別清晰的類,都儘量編寫類級別註釋,說明類的目的和使用方法。

除了特別清晰的方法,對外提供的公有方法,抽象類的方法,同樣儘量清晰的描述:期待的輸入,對應的輸出,錯誤的處理和返回碼,以及可能丟擲的異常。


Rule 2. 【推薦】通過更清晰的程式碼來避免註釋

在編寫註釋前,考慮是否可以通過更好的命名,更清晰的程式碼結構,更好的函式和變數的抽取,讓程式碼不言自明,此時不需要額外的註釋。


Rule 3. 【推薦】刪除空註釋,無意義註釋

《Clean Code》建議,如果沒有想說的,不要留著IDE自動生成的,空的@param,@return,@throws 標記,讓程式碼更簡潔。

反例:方法名為put,加上兩個有意義的變數名elephant和fridge,已經說明了這是在幹什麼,不需要任何額外的註釋。

/**
 * put elephant into fridge.
 * 
 * @param elephant
 * @param fridge
 * @return 
 */
public void put(Elephant elephant, Fridge fridge);

Rule 4.【推薦】避免建立人,建立日期,及更新日誌的註釋

程式碼後續還會有多人多次維護,而建立人可能會離職,讓我們相信原始碼版本控制系統對更新記錄能做得更好。


Rule 5. 【強制】程式碼修改的同時,註釋也要進行相應的修改。尤其是引數、返回值、異常、核心邏輯等的修改


Rule 6. 【強制】類、類的公有成員、方法的註釋必須使用Javadoc規範,使用/* xxx */格式,不得使用//xxx方式*

正確的JavaDoc格式可以在IDE中,檢視呼叫方法時,不進入方法即可懸浮提示方法、引數、返回值的意義,提高閱讀效率。


Rule 7. 【推薦】JavaDoc中不要為了HTML格式化而大量使用HTML標籤和轉義字元

如果為了Html版JavaDoc的顯示,大量使用 這樣的html標籤,以及&lt &quot 這樣的html轉義字元,嚴重影響了直接閱讀程式碼時的直觀性,而直接閱讀程式碼的機率其實比看Html版的JavaDoc大得多。

另外IDE對JavaDoc的格式化也要求``之類的標籤來換行,可以配置讓IDE不對JavaDoc的格式化。


Rule 8. 【推薦】註釋不要為了英文而英文

如果沒有國際化要求,中文能表達得更清晰時還是用中文。


Rule 9. 【推薦】TODO標記,清晰說明代辦事項和處理人

清晰描述待修改的事項,保證過幾個月後仍然能夠清楚要做什麼修改。

如果近期會處理的事項,寫明處理人。如果遠期的,寫明提出人。

通過IDE和Sonar的標記掃描,經常清理此類標記,線上故障經常來源於這些標記但未處理的程式碼。

正例:
//TODO:calvin use xxx to replace yyy.

反例:
//TODO: refactor it

Rule 10. 【推薦】合理處理註釋掉的程式碼

如果後續會恢復此段程式碼,在目的碼上方用///說明註釋動機,而不是簡單的註釋掉程式碼。

如果很大概率不再使用,則直接刪除(版本管理工具儲存了歷史程式碼)。


(四) 方法設計

Rule 1. 【推薦】方法的長度度量

方法儘量不要超過100行,或其他團隊共同商定的行數。

另外,方法長度超過8000個位元組碼時,將不會被JIT編譯成二進位制碼。


Rule 2. 【推薦】方法的語句在同一個抽象層級上

反例:一個方法裡,前20行程式碼在進行很複雜的基本價格計算,然後呼叫一個折扣計算函式,再呼叫一個贈品計算函式。

此時可將前20行也封裝成一個價格計算函式,使整個方法在同一抽象層級上。


Rule 3. 【推薦】為了幫助閱讀及方法內聯,將小概率發生的異常處理及其他極小概率進入的程式碼路徑,封裝成獨立的方法

if(seldomHappenCase) {
  hanldMethod();
}

try {
  ...
} catch(SeldomHappenException e) {
  handleException();
}

Rule 4. 【推薦】儘量減少重複的程式碼,抽取方法

超過5行以上重複的程式碼,都可以考慮抽取公用的方法。


Rule 5. 【推薦】方法引數最好不超過3個,最多不超過7個

1)如果多個引數同屬於一個物件,直接傳遞物件。

例外: 你不希望依賴整個物件,傳播了類之間的依賴性。

2)將多個引數合併為一個新建立的邏輯物件。

例外: 多個引數之間毫無邏輯關聯。

3)將函式拆分成多個函式,讓每個函式所需的引數減少。


Rule 6.【推薦】下列情形,需要進行引數校驗

1) 呼叫頻次低的方法。

2) 執行時間開銷很大的方法。此情形中,引數校驗時間幾乎可以忽略不計,但如果因為引數錯誤導致中間執行回退,或者錯誤,代價更大。

3) 需要極高穩定性和可用性的方法。

4) 對外提供的開放介面,不管是RPC/HTTP/公共類庫的API介面。

如果使用Apache Validate 或 Guava Precondition進行校驗,並附加錯誤提示資訊時,注意不要每次校驗都做一次字串拼接。

//WRONG
Validate.isTrue(length > 2, "length is "+keys.length+", less than 2", length);
//RIGHT
Validate.isTrue(length > 2, "length is %d, less than 2", length);

Rule 7.【推薦】下列情形,不需要進行引數校驗

1) 極有可能被迴圈呼叫的方法。

2) 底層呼叫頻度比較高的方法。畢竟是像純淨水過濾的最後一道,引數錯誤不太可能到底層才會暴露問題。

比如,一般DAO層與Service層都在同一個應用中,所以DAO層的引數校驗,可以省略。

3) 被宣告成private,或其他只會被自己程式碼所呼叫的方法,如果能夠確定在呼叫方已經做過檢查,或者肯定不會有問題則可省略。

即使忽略檢查,也儘量在方法說明裡註明引數的要求,比如vjkit中的@NotNull,@Nullable標識。


Rule 8.【推薦】禁用assert做引數校驗

assert斷言僅用於測試環境除錯,無需在生產環境時進行的校驗。因為它需要增加-ea啟動引數才會被執行。而且校驗失敗會丟擲一個AssertionError(屬於Error,需要捕獲Throwable)

因此在生產環境進行的校驗,需要使用Apache Commons Lang的Validate或Guava的Precondition。


Rule 9.【推薦】返回值可以為Null,可以考慮使用JDK8的Optional類

不強制返回空集合,或者空物件。但需要新增註釋充分說明什麼情況下會返回null值。

本手冊明確防止NPE是呼叫者的責任。即使被呼叫方法返回空集合或者空物件,對呼叫者來說,也並非高枕無憂,必須考慮到遠端呼叫失敗、序列化失敗、執行時異常等場景返回null的情況。

JDK8的Optional類的使用這裡不展開。


Rule 10.【推薦】返回值可以為內部陣列和集合

如果覺得被外部修改的可能性不大,或沒有影響時,不強制在返回前包裹成Immutable集合,或進行陣列克隆。


Rule 11.【推薦】不能使用有繼承關係的引數型別來過載方法

因為方法過載的引數型別是根據編譯時表面型別匹配的,不根據執行時的實際型別匹配。

class A {
  void hello(List list);
  void hello(ArrayList arrayList);
}

List arrayList = new ArrayList();

// 下句呼叫的是hello(List list),因為arrayList的定義型別是List
a.hello(arrayList);  

Rule 12.【強制】正被外部呼叫的介面,不允許修改方法簽名,避免對介面的呼叫方產生影響

只能新增新介面,並對已過時介面加@Deprecated註解,並清晰地說明新介面是什麼。


Rule 13.【推薦】不使用@Deprecated的類或方法

介面提供方既然明確是過時介面並提供新介面,那麼作為呼叫方來說,有義務去考證過時方法的新實現是什麼。

比如java.net.URLDecoder 中的方法decode(String encodeStr) 這個方法已經過時,應該使用雙引數decode(String source, String encode)。


Rule 14.【推薦】不使用不穩定方法,如com.sun.*包下的類,底層類庫中internal包下的類

com.sun.*sun.*包下的類,或者底層類庫中名稱為internal的包下的類,都是不對外暴露的,可隨時被改變的不穩定類。


(五) 類設計

Rule 1. 【推薦】類成員與方法的可見性最小化

任何類、方法、引數、變數,嚴控訪問範圍。過於寬泛的訪問範圍,不利於模組解耦。思考:如果是一個private的方法,想刪除就刪除,可是一個public的service方法,或者一個public的成員變數,刪除一下,不得手心冒點汗嗎?

例外:為了單元測試,有時也可能將訪問範圍擴大,此時需要加上JavaDoc說明或vjkit中的@VisibleForTesting註解。


Rule 2.【推薦】 減少類之間的依賴

比如如果A類只依賴B類的某個屬性,在建構函式和方法引數中,只傳入該屬性。讓閱讀者知道,A類只依賴了B類的這個屬性,而不依賴其他屬性,也不會呼叫B類的任何方法。

a.foo(b);     //WRONG

a.foo(b.bar); //RIGHT

Rule 3.【推薦】 定義變數與方法引數時,儘量使用介面而不是具體類

使用介面可以保持一定的靈活性,也能向讀者更清晰的表達你的需求:變數和引數只是要求有一個Map,而不是特定要求一個HashMap。

例外:如果變數和引數要求某種特殊型別的特性,則需要清晰定義該引數型別,同樣是為了向讀者表達你的需求。


Rule 4. 【推薦】類的長度度量

類儘量不要超過300行,或其他團隊共同商定的行數。

對過大的類進行分拆時,可考慮其內聚性,即類的屬性與類的方法的關聯程度,如果有些屬性沒有被大部分的方法使用,其內聚性是低的。


Rule 5.【推薦】 建構函式如果有很多引數,且有多種引數組合時,建議使用Builder模式

Executor executor = new ThreadPoolBuilder().coreThread(10).queueLenth(100).build();

即使仍然使用建構函式,也建議使用chain constructor模式,逐層加入預設值傳遞呼叫,僅在引數最多的建構函式裡實現構造邏輯。

public A(){
  A(DEFAULT_TIMEOUT);
}

public A(int timeout) {
  ...
}

Rule 6.【推薦】建構函式要簡單,尤其是存在繼承關係的時候

可以將複雜邏輯,尤其是業務邏輯,抽取到獨立函式,如init(),start(),讓使用者顯式呼叫。

Foo foo = new Foo();
foo.init();

Rule 7.【強制】所有的子類覆寫方法,必須加@Override註解

比如有時候子類的覆寫方法的拼寫有誤,或方法簽名有誤,導致沒能真正覆寫,加@Override可以準確判斷是否覆寫成功。

而且,如果在父類中對方法簽名進行了修改,子類會馬上編譯報錯。

另外,也能提醒閱讀者這是個覆寫方法。

最後,建議在IDE的Save Action中配置自動新增@Override註解,如果無意間錯誤同名覆寫了父類方法也能被發現。


Rule 8.【強制】靜態方法不能被子類覆寫。

因為它只會根據表面型別來決定呼叫的方法。

Base base = new Children();

// 下句實際呼叫的是父類的靜態方法,雖然物件例項是子類的。
base.staticMethod();

Rule 9.靜態方法訪問的原則

9.1【推薦】避免通過一個類的物件引用訪問此類的靜態變數或靜態方法,直接用類名來訪問即可

目的是向讀者更清晰傳達呼叫的是靜態方法。可在IDE的Save Action中配置自動轉換。

int i = objectA.staticMethod(); // WRONG

int i = ClassA.staticMethod(); // RIGHT

9.2 【推薦】除測試用例,不要static import 靜態方法

靜態匯入後忽略掉的類名,給閱讀者造成障礙。

例外:測試環境中的assert語句,大家都太熟悉了。

9.3【推薦】儘量避免在非靜態方法中修改靜態成員變數的值

// WRONG
public void foo() {
  ClassA.staticFiled = 1;
}

Rule 10.【推薦】 內部類的定義原則

當一個類與另一個類關聯非常緊密,處於從屬的關係,特別是只有該類會訪問它時,可定義成私有內部類以提高封裝性。

另外,內部類也常用作回撥函式類,在JDK8下建議寫成Lambda。

內部類分匿名內部類,內部類,靜態內部類三種。

  1. 匿名內部類 與 內部類,按需使用:

在效能上沒有區別;當內部類會被多個地方呼叫,或匿名內部類的長度太長,已影響對呼叫它的方法的閱讀時,定義有名字的內部類。

  1. 靜態內部類 與 內部類,優先使用靜態內部類:
  2. 非靜態內部類持有外部類的引用,能訪問外類的例項方法與屬性。構造時多傳入一個引用對效能沒有太大影響,更關鍵的是向閱讀者傳遞自己的意圖,內部類會否訪問外部類。
  3. 非靜態內部類裡不能定義static的屬性與方法。

Rule 11.【推薦】使用getter/setter方法,還是直接public成員變數的原則。

除非因為特殊原因方法內聯失敗,否則使用getter方法與直接訪問成員變數的效能是一樣的。

使用getter/setter,好處是可以進一步的處理:

  1. 通過隱藏setter方法使得成員變數只讀
  2. 增加簡單的校驗邏輯
  3. 增加簡單的值處理,值型別轉換等

建議通過IDE生成getter/setter。

但getter/seter中不應有複雜的業務處理,建議另外封裝函式,並且不要以getXX/setXX命名。

如果是內部類,以及無邏輯的POJO/VO類,使用getter/setter除了讓一些純OO論者感覺舒服,沒有任何的好處,建議直接使用public成員變數。

例外:有些序列化框架只能從getter/setter反射,不能直接反射public成員變數。


Rule 12.【強制】POJO類必須覆寫toString方法。

便於記錄日誌,排查問題時呼叫POJO的toString方法列印其屬性值。否則預設的Object.toString()只列印類名@數字的無效資訊。


Rule 13. hashCode和equals方法的處理,遵循如下規則:

13.1【強制】只要重寫equals,就必須重寫hashCode。 而且選取相同的屬性進行運算。

13.2【推薦】只選取真正能決定物件是否一致的屬性,而不是所有屬性,可以改善效能。

13.3【推薦】對不可變物件,可以快取hashCode值改善效能(比如String就是例子)。

13.4【強制】類的屬性增加時,及時重新生成toString,hashCode和equals方法。


Rule 14.【強制】使用IDE生成toString,hashCode和equals方法。

使用IDE生成而不是手寫,能保證toString有統一的格式,equals和hashCode則避免不正確的Null值處理。

子類生成toString() 時,還需要勾選父類的屬性。


Rule 15. 【強制】Object的equals方法容易拋空指標異常,應使用常量或確定非空的物件來呼叫equals

推薦使用java.util.Objects#equals(JDK7引入的工具類)

"test".equals(object);  //RIGHT

Objects.equals(object, "test"); //RIGHT

Rule 16.【強制】除了保持相容性的情況,總是移除無用屬性、方法與引數

特別是private的屬性、方法、內部類,private方法上的引數,一旦無用立刻移除。信任程式碼版本管理系統。


Rule 17.【推薦】final關鍵字與效能無關,僅用於下列不可修改的場景

1) 定義類及方法時,類不可繼承,方法不可覆寫;

2) 定義基本型別的函式引數和變數,不可重新賦值;

3) 定義物件型的函式引數和變數,僅表示變數所指向的物件不可修改,而物件自身的屬性是可以修改的。


Rule 18.【推薦】得墨忒耳法則,不要和陌生人說話

以下呼叫,一是導致了對A物件的內部結構(B,C)的緊耦合,二是連串的呼叫很容易產生NPE,因此鏈式呼叫盡量不要過長。

obj.getA().getB().getC().hello();

(六) 控制語句

Rule 1. 【強制】if, else, for, do, while語句必須使用大括號,即使只有單條語句

曾經試過合併程式碼時,因為沒加括號,單條語句合併成兩條語句後,仍然認為只有單條語句,另一條語句在迴圈外執行。

其他增加除錯語句等情況也經常引起同樣錯誤。

可在IDE的Save Action中配置自動新增。

if (a == b) {
  ...
}

例外:一般由IDE生成的equals()函式


Rule 2.【推薦】少用if-else方式,多用哨兵語句式以減少巢狀層次

if (condition) {
  ...
  return obj;
}

// 接著寫else的業務邏輯程式碼;
  • Facebook-Contrib: Style - Method buries logic to the right (indented) more than it needs to be

Rule 3.【推薦】限定方法的巢狀層次

所有if/else/for/while/try的巢狀,當層次過多時,將引起巨大的閱讀障礙,因此一般推薦巢狀層次不超過4。

通過抽取方法,或哨兵語句(見Rule 2)來減少巢狀。

public void applyDriverLicense() {
  if (isTooYoung()) {
    System.out.println("You are too young to apply driver license.");
    return;
  }

  if (isTooOld()) {
    System.out.println("You are too old to apply driver license.");
    return;
  }

  System.out.println("You've applied the driver license successfully.");
  return;
}

Rule 4.【推薦】布林表示式中的布林運算子(&&,||)的個數不超過4個,將複雜邏輯判斷的結果賦值給一個有意義的布林變數名,以提高可讀性

//WRONG
if ((file.open(fileName, "w") != null) && (...) || (...)|| (...)) {
  ...
}

//RIGHT
boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed || (...)) {
  ...
}

Rule 5.【推薦】簡單邏輯,善用三目運算子,減少if-else語句的編寫

s != null ? s : "";

Rule 6.【推薦】減少使用取反的邏輯

不使用取反的邏輯,有利於快速理解。且大部分情況,取反邏輯存在對應的正向邏輯寫法。

//WRONG
if (!(x >= 268) { ... }

//RIGHT
if (x < 268) { ... }

Rule 7.【推薦】表示式中,能造成短路概率較大的邏輯儘量放前面,使得後面的判斷可以免於執行

if (maybeTrue() || maybeFalse()) { ... }

if (maybeFalse() && maybeTrue()) { ... }

Rule 8.【強制】switch的規則

1)在一個switch塊內,每個case要麼通過break/return等來終止,要麼註釋說明程式將繼續執行到哪一個case為止;

2)在一個switch塊內,都必須包含一個default語句並且放在最後,即使它什麼程式碼也沒有。

String animal = "tomcat";

switch (animal) {
case "cat":
  System.out.println("It's a cat.");
  break;
case "lion": // 執行到tiger
case "tiger":
  System.out.println("It's a beast.");
  break;
default: 
  // 什麼都不做,也要有default
  break;
}

Rule 9.【推薦】迴圈體中的語句要考量效能,操作儘量移至迴圈體外處理

1)不必要的耗時較大的物件構造;

2)不必要的try-catch(除非出錯時需要迴圈下去)。


Rule 10.【推薦】能用while迴圈實現的程式碼,就不用do-while迴圈

while語句能在迴圈開始的時候就看到迴圈條件,便於幫助理解迴圈內的程式碼;

do-while語句要在迴圈最後才看到迴圈條件,不利於程式碼維護,程式碼邏輯容易出錯。


(七) 基本型別與字串

Rule 1. 原子資料型別(int等)與包裝型別(Integer等)的使用原則

1.1 【推薦】需要序列化的POJO類屬性使用包裝資料型別

1.2 【推薦】RPC方法的返回值和引數使用包裝資料型別

1.3 【推薦】區域性變數儘量使用基本資料型別

包裝型別的壞處:

1)Integer 24位元組,而原子型別 int 4位元組。

2)包裝型別每次賦值還需要額外建立物件,如Integer var = 200, 除非數值在快取區間內(見Integer.IntegerCache與Long.LongCache)才會複用已快取物件。預設快取區間為-128到127,其中Integer的快取區間還受啟動引數的影響,如-XX:AutoBoxCacheMax=20000。

3)包裝型別還有==比較的陷阱(見規則3)

包裝型別的好處:

1)包裝型別能表達Null的語義。

比如資料庫的查詢結果可能是null,如果用基本資料型別有NPE風險。又比如顯示成交總額漲跌情況,如果呼叫的RPC服務不成功時,應該返回null,顯示成-%,而不是0%。

2)集合需要包裝型別,除非使用陣列,或者特殊的原子型別集合。

3)泛型需要包裝型別,如Result


Rule 2.原子資料型別與包裝型別的轉換原則

2.1【推薦】自動轉換(AutoBoxing)有一定成本,呼叫者與被呼叫函式間儘量使用同一型別,減少預設轉換

//WRONG, sum 型別為Long, i型別為long,每次相加都需要AutoBoxing。
Long sum=0L;

for( long i = 0; i < 10000; i++) {
  sum+=i;
}

//RIGHT, 準確使用API返回正確的型別
Integer i = Integer.valueOf(str);
int i = Integer.parseInt(str);

2.2 【推薦】自動拆箱有可能產生NPE,要注意處理

//如果intObject為null,產生NPE
int i = intObject; 

Rule 3. 數值equals比較的原則

3.1【強制】 所有包裝類物件之間值的比較,全部使用equals方法比較

判斷物件是否同一個。Integer var = ?在快取區間的賦值(見規則1),會複用已有物件,因此這個區間內的Integer使用進行判斷可通過,但是區間之外的所有資料,則會在堆上新產生,不會通過。因此如果用== 來比較數值,很可能在小的測試資料中通過,而到了生產環境才出問題。

3.2【強制】 BigDecimal需要使用compareTo()

因為BigDecimal的equals()還會比對精度,2.0與2.00不一致。

  • Facebook-Contrib: Correctness - Method calls BigDecimal.equals()

3.3【強制】 Atomic 系列,不能使用equals方法*

因為 Atomic* 系列沒有覆寫equals方法。

//RIGHT
if (counter1.get() == counter2.get()){...}

3.4【強制】 double及float的比較,要特殊處理

因為精度問題,浮點數間的equals非常不可靠,在vjkit的NumberUtil中有對應的封裝函式。

float f1 = 0.15f;
float f2 = 0.45f/3; //實際等於0.14999999

//WRONG
if (f1 == f2) {...}
if (Double.compare(f1,f2)==0) 

//RIGHT
static final float EPSILON = 0.00001f;
if (Math.abs(f1-f2)<EPSILON) {...}

Rule 4. 數字型別的計算原則

4.1【強制】數字運算表示式,因為先進行等式右邊的運算,再賦值給等式左邊的變數,所以等式兩邊的型別要一致

例子1: int與int相除後,哪怕被賦值給float或double,結果仍然是四捨五入取整的int。

需要強制將除數或被除數轉換為float或double。

double d = 24/7;  //結果是3.0
double d =  (double)24/7; //結果是正確的3.42857

例子2: int與int相乘,哪怕被賦值給long,仍然會溢位。

需要強制將乘數的一方轉換為long。

long l = Integer.MAX_VALUE * 2; // 結果是溢位的-2
long l = Integer.MAX_VALUE * 2L; //結果是正確的4294967294

另外,int的最大值約21億,留意可能溢位的情況。

4.2【強制】數字取模的結果不一定是正數,負數取模的結果仍然負數

取模做陣列下標時,如果不處理負數的情況,很容易ArrayIndexOutOfBoundException。

另外,Integer.MIN_VALUE取絕對值也仍然是負數。因此,vjkit的MathUtil對上述情況做了安全的封裝。

-4 % 3  = -1;
Math.abs(Integer.MIN_VALUE) = -2147483648;
  • Findbugs: Style - Remainder of hashCode could be negative

4.3【推薦】 double 或 float 計算時有不可避免的精度問題

float f = 0.45f/3;    //結果是0.14999999

double d1 = 0.45d/3;  //結果是正確的0.15

double d2 = 1.03d - 0.42d; //結果是0.6100000000000001

儘量用double而不用float,但如果是金融貨幣的計算,則必須使用如下選擇:

選項1, 使用效能較差的BigDecimal。BigDecimal還能精確控制四捨五入或是其他取捨的方式。

選項2, 在預知小數精度的情況下,將浮點運算放大為整數計數,比如貨幣以"分"而不是以"元"計算。


Rule 5. 【推薦】如果變數值僅有有限的可選值,用列舉類來定義常量

尤其是變數還希望帶有名稱之外的延伸屬性時,如下例:

//WRONG
public String MONDAY = "MONDAY";
public int MONDAY_SEQ = 1;

//RIGHT
public enum SeasonEnum { 
    SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4); 
    int seq; 
    SeasonEnum(int seq) { this.seq = seq; }
}

業務程式碼中不要依賴ordinary()函式進行業務運算,而是自定義數字屬性,以免列舉值的增減調序造成影響。 例外:永遠不會有變化的列舉,比如上例的一年四季。


Rule 6. 字串拼接的原則

6.1 【推薦】 當字串拼接不在一個命令列內寫完,而是存在多次拼接時(比如迴圈),使用StringBuilder的append()

String s  = "hello" + str1 +  str2;  //Almost OK,除非初始長度有問題,見第3點.

String s  = "hello";  //WRONG
if (condition) {
  s += str1;
}

String str = "start";       //WRONG
for (int i = 0; i < 100; i++) {
  str = str + "hello";
}

反編譯出的位元組碼檔案顯示,其實每條用+進行字元拼接的語句,都會new出一個StringBuilder物件,然後進行append操作,最後通過toString方法返回String物件。所以上面兩個錯誤例子,會重複構造StringBuilder,重複toString()造成資源浪費。

6.2 【強制】 字串拼接物件時,不要顯式呼叫物件的toString()

如上,+實際是StringBuilder,本身會呼叫物件的toString(),且能很好的處理null的情況。

//WRONG
str = "result:" + myObject.toString();  // myObject為Null時,拋NPE

//RIGHT
str = "result:" + myObject;  // myObject為Null時,輸出 result:null

6.3【強制】使用StringBuilder,而不是有所有方法都有同步修飾符的StringBuffer

因為內聯不成功,逃逸分析並不能抹除StringBuffer上的同步修飾符

6.4 【推薦】當拼接後字串的長度遠大於16時,指定StringBuilder的大概長度,避免容量不足時的成倍擴充套件

6.5 【推薦】如果字串長度很大且頻繁拼接,可考慮ThreadLocal重用StringBuilder物件

參考BigDecimal的toString()實現,及vjkit中的StringBuilderHolder。


Rule 7. 【推薦】字元操作時,優先使用字元引數,而不是字串,能提升效能

//WRONG
str.indexOf("e");

//RIGHT
stringBuilder.append('a'); 
str.indexOf('e');
str.replace('m','z');

其他包括split等方法,在JDK String中未提供針對字元引數的方法,可考慮使用Apache Commons StringUtils 或Guava的Splitter。


Rule 8. 【推薦】利用好正規表示式的預編譯功能,可以有效加快正則匹配速度

反例:

//直接使用String的matches()方法
result = "abc".matches("[a-zA-z]");

//每次重新構造Pattern
Pattern pattern = Pattern.compile("[a-zA-z]");
result = pattern.matcher("abc").matches();

正例:

//在某個地方預先編譯Pattern,比如類的靜態變數
private static Pattern pattern = Pattern.compile("[a-zA-z]");
...
//真正使用Pattern的地方
result = pattern.matcher("abc").matches();

(八) 集合處理

Rule 1. 【推薦】底層資料結構是陣列的集合,指定集合初始大小

底層資料結構為陣列的集合包括 ArrayList,HashMap,HashSet,ArrayDequeue等。

陣列有大小限制,當超過容量時,需要進行復制式擴容,新申請一個是原來容量150% or 200%的陣列,將原來的內容複製過去,同時浪費了記憶體與效能。HashMap/HashSet的擴容,還需要所有鍵值對重新落位,消耗更大。

預設建構函式使用預設的陣列大小,比如ArrayList預設大小為10,HashMap為16。因此建議使用ArrayList(int initialCapacity)等建構函式,明確初始化大小。

HashMap/HashSet的初始值還要考慮載入因子:

為了降低雜湊衝突的概率(Key的雜湊值按陣列大小取模後,如果落在同一個陣列下標上,將組成一條需要遍歷的Entry鏈),預設當HashMap中的鍵值對達到陣列大小的75%時,即會觸發擴容。因此,如果預估容量是100,即需要設定100/0.75 +1=135的陣列大小。vjkit的MapUtil的Map建立函式封裝了該計算。

如果希望加快Key查詢的時間,還可以進一步降低載入因子,加大初始大小,以降低雜湊衝突的概率。


Rule 2. 【推薦】儘量使用新式的foreach語法遍歷Collection與陣列

foreach是語法糖,遍歷集合的實際位元組碼等價於基於Iterator的迴圈。

foreach程式碼一來程式碼簡潔,二來有效避免了有多個迴圈或巢狀迴圈時,因為不小心的複製貼上,用錯了iterator或迴圈計數器(i,j)的情況。


Rule 3. 【強制】不要在foreach迴圈裡進行元素的remove/add操作,remove元素可使用Iterator方式

//WRONG
for (String str : list) {
  if (condition) {
    list.remove(str);
  }
}

//RIGHT
Iterator<String> it = list.iterator();
while (it.hasNext()) {
  String str = it.next();
  if (condition) {
    it.remove();
  }
} 
  • Facebook-Contrib: Correctness - Method modifies collection element while iterating
  • Facebook-Contrib: Correctness - Method deletes collection element while iterating

Rule 4. 【強制】使用entrySet遍歷Map類集合Key/Value,而不是keySet 方式進行遍歷

keySet遍歷的方式,增加了N次用key獲取value的查詢。


Rule 5. 【強制】當物件用於集合時,下列情況需要重新實現hashCode()和 equals()

1) 以物件做為Map的KEY時;

2) 將物件存入Set時。

上述兩種情況,都需要使用hashCode和equals比較物件,預設的實現會比較是否同一個物件(物件的引用相等)。

另外,物件放入集合後,會影響hashCode(),equals()結果的屬性,將不允許修改。


Rule 6. 【強制】高度注意各種Map類集合Key/Value能不能儲存null值的情況

Map Key Value
HashMap Nullable Nullable
ConcurrentHashMap NotNull NotNull
TreeMap NotNull Nullable

由於HashMap的干擾,很多人認為ConcurrentHashMap是可以置入null值。同理,Set中的value實際是Map中的key。


Rule 7. 【強制】長生命週期的集合,裡面內容需要及時清理,避免記憶體洩漏

長生命週期集合包括下面情況,都要小心處理。

1) 靜態屬性定義;

2) 長生命週期物件的屬性;

3) 儲存在ThreadLocal中的集合。

如無法保證集合的大小是有限的,使用合適的快取方案代替直接使用HashMap。

另外,如果使用WeakHashMap儲存物件,當物件本身失效時,就不會因為它在集合中存在引用而阻止回收。但JDK的WeakHashMap並不支援併發版本,如果需要併發可使用Guava Cache的實現。


Rule 8. 【強制】集合如果存在併發修改的場景,需要使用執行緒安全的版本

  1. 著名的反例,HashMap擴容時,遇到併發修改可能造成100%CPU佔用。

推薦使用java.util.concurrent(JUC)工具包中的併發版集合,如ConcurrentHashMap等,優於使用Collections.synchronizedXXX()系列函式進行同步化封裝(等價於在每個方法都加上synchronized關鍵字)。

例外:ArrayList所對應的CopyOnWriteArrayList,每次更新時都會複製整個陣列,只適合於讀多寫很少的場景。如果頻繁寫入,可能退化為使用Collections.synchronizedList(list)。

  1. 即使執行緒安全類仍然要注意函式的正確使用。

例如:即使用了ConcurrentHashMap,但直接是用get/put方法,仍然可能會多執行緒間互相覆蓋。

//WRONG
E e = map.get(key);
if (e == null) {
  e = new E();
  map.put(key, e); //仍然能兩條執行緒併發執行put,互相覆蓋
}
return e;

//RIGHT 
E e = map.get(key);
if (e == null) {
  e = new E();
  E previous = map.putIfAbsent(key, e);
  if(previous != null) {
    return previous;
  }
}
return e;

Rule 9. 【推薦】正確使用集合泛型的萬用字元

List並不是List的子類,如果希望泛型的集合能向上向下相容轉型,而不僅僅適配唯一類,則需定義萬用字元,可以按需要extends 和 super的字面意義,也可以遵循PECS(Producer Extends Consumer Super)原則:

  1. 如果集合要被讀取,定義成``
Class Stack<E>{
  public void pushAll(Iterable<? extends E> src){
    for (E e: src)
      push(e);
  }
}

Stack<Number> stack = new Stack<Number>();
Iterable<Integer> integers = ...;
stack.pushAll(integers);
  1. 如果集合要被寫入,定義成``
Class Stack<E>{
  public void popAll(Collection<? super E> dist){
     while(!isEmpty())
          dist.add(pop);   
  }
}

Stack<Number> stack = new Stack<Number>();
Collection<Object> objects = ...;
stack.popAll(objects);

Rule 10. 【推薦】List, ListList的選擇

定義成List,會被IDE提示需要定義泛型。 如果實在無法確定泛型,就倉促定義成List來矇混過關的話,該list只能讀,不能增改。定義成List呢,如規則9所述,List 並不是List的子類,除非函式定義使用了萬用字元。

因此實在無法明確其泛型時,使用List也是可以的。


Rule 11. 【推薦】如果Key只有有限的可選值,先將Key封裝成Enum,並使用EnumMap

EnumMap,以Enum為Key的Map,內部儲存結構為Object[enum.size],訪問時以value = Object[enum.ordinal()]獲取值,同時具備HashMap的清晰結構與陣列的效能。

public enum COLOR {
  RED, GREEN, BLUE, ORANGE;
}

EnumMap<COLOR, String> moodMap = new EnumMap<COLOR, String> (COLOR.class);

Rule 12. 【推薦】Array 與 List互轉的正確寫法

// list -> array,構造陣列時不需要設定大小
String[] array = (String[])list.toArray(); //WRONG;
String[] array = list.toArray(new String[0]); //RIGHT
String[] array = list.toArray(new String[list.size()]); //RIGHT,但list.size()可用0代替。


// array -> list
//非原始型別陣列,且List不能再擴充套件
List list = Arrays.asList(array); 

//非原始型別陣列, 但希望List能再擴充套件
List list = new ArrayList(array.length);
Collections.addAll(list, array);

//原始型別陣列,JDK8
List myList = Arrays.stream(intArray).boxed().collect(Collectors.toList());

//原始型別陣列,JDK7則要自己寫個迴圈來加入了

Arrays.asList(array),如果array是原始型別陣列如int[],會把整個array當作List的一個元素,String[] 或 Foo[]則無此問題。 Collections.addAll()實際是迴圈加入元素,效能相對較低,同樣會把int[]認作一個元素。

  • Facebook-Contrib: Correctness - Impossible downcast of toArray() result
  • Facebook-Contrib: Correctness - Method calls Array.asList on an array of primitive values

(九) 併發處理

Rule 1. 【強制】建立執行緒或執行緒池時請指定有意義的執行緒名稱,方便出錯時回溯

1)建立單條執行緒時直接指定執行緒名稱

Thread t = new Thread();
t.setName("cleanup-thread");

2) 執行緒池則使用guava或自行封裝的ThreadFactory,指定命名規則。

//guava 或自行封裝的ThreadFactory
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + "-%d").build();

ThreadPoolExecutor executor = new ThreadPoolExecutor(..., threadFactory, ...);

Rule 2. 【推薦】儘量使用執行緒池來建立執行緒

除特殊情況,儘量不要自行建立執行緒,更好的保護執行緒資源。

//WRONG
Thread thread = new Thread(...);
thread.start();

同理,定時器也不要使用Timer,而應該使用ScheduledExecutorService。

因為Timer只有單執行緒,不能併發的執行多個在其中定義的任務,而且如果其中一個任務丟擲異常,整個Timer也會掛掉,而ScheduledExecutorService只有那個沒捕獲到異常的任務不再定時執行,其他任務不受影響。


Rule 3. 【強制】執行緒池不允許使用 Executors去建立,避資源耗盡風險

Executors返回的執行緒池物件的弊端 :

1)FixedThreadPool 和 SingleThreadPool:

允許的請求佇列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。

2)CachedThreadPool 和 ScheduledThreadPool:

允許的建立執行緒數量為 Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致 OOM。

應通過 new ThreadPoolExecutor(xxx,xxx,xxx,xxx)這樣的方式,更加明確執行緒池的執行規則,合理設定Queue及執行緒池的core size和max size,建議使用vjkit封裝的ThreadPoolBuilder。


Rule 4. 【強制】正確停止執行緒

Thread.stop()不推薦使用,強行的退出太不安全,會導致邏輯不完整,操作不原子,已被定義成Deprecate方法。

停止單條執行緒,執行Thread.interrupt()。

停止執行緒池:

  • ExecutorService.shutdown(): 不允許提交新任務,等待當前任務及佇列中的任務全部執行完畢後退出;
  • ExecutorService.shutdownNow(): 通過Thread.interrupt()試圖停止所有正在執行的執行緒,並不再處理還在佇列中等待的任務。

最優雅的退出方式是先執行shutdown(),再執行shutdownNow(),vjkit的ThreadPoolUtil進行了封裝。

注意,Thread.interrupt()並不保證能中斷正在執行的執行緒,需編寫可中斷退出的Runnable,見規則5。


Rule 5. 【強制】編寫可停止的Runnable

執行Thread.interrupt()時,如果執行緒處於sleep(), wait(), join(), lock.lockInterruptibly()等blocking狀態,會丟擲InterruptedException,如果執行緒未處於上述狀態,則將執行緒狀態設為interrupted。

因此,如下的程式碼無法中斷執行緒:

public void run() {

  while (true) { //WRONG,無判斷執行緒狀態。
    sleep();
  }

  public void sleep() {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      logger.warn("Interrupted!", e); //WRONG,吃掉了異常,interrupt狀態未再傳遞
    }
  }
}

5.1 正確處理InterruptException

因為InterruptException異常是個必須處理的Checked Exception,所以run()所呼叫的子函式很容易吃掉異常並簡單的處理成列印日誌,但這等於停止了中斷的傳遞,外層函式將收不到中斷請求,繼續原有迴圈或進入下一個堵塞。

正確處理是呼叫Thread.currentThread().interrupt(); 將中斷往外傳遞。

//RIGHT
public void myMethod() {
  try {
    ...
  } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
  }
}

5.2 主迴圈及進入阻塞狀態前要判斷執行緒狀態

//RIGHT
public void run() {
  try {
    while (!Thread.isInterrupted()) {
      // do stuff
    }
  } catch (InterruptedException e) {
    logger.warn("Interrupted!", e);
  }
}

其他如Thread.sleep()的程式碼,在正式sleep前也會判斷執行緒狀態。


Rule 6. 【強制】Runnable中必須捕獲一切異常

如果Runnable中沒有捕獲RuntimeException而向外丟擲,會發生下列情況:

  1. ScheduledExecutorService執行定時任務,任務會被中斷,該任務將不再定時排程,但執行緒池裡的執行緒還能用於其他任務。
  2. ExecutorService執行任務,當前執行緒會中斷,執行緒池需要建立新的執行緒來響應後續任務。
  3. 如果沒有在ThreadFactory設定自定義的UncaughtExceptionHanlder,則異常最終只列印在System.err,而不會列印在專案的日誌中。

因此建議自寫的Runnable都要保證捕獲異常; 如果是第三方的Runnable,可以將其再包裹一層vjkit中的SafeRunnable。

executor.execute(ThreadPoolUtil.safeRunner(runner));

Rule 7. 【強制】全域性的非執行緒安全的物件可考慮使用ThreadLocal存放

全域性變數包括單例物件,static成員變數。

著名的非執行緒安全類包括SimpleDateFormat,MD5/SHA1的Digest。

對這些類,需要每次使用時建立。

但如果建立有一定成本,可以使用ThreadLocal存放並重用。

ThreadLocal變數需要定義成static,並在每次使用前重置。

private static final ThreadLocal<MessageDigest> SHA1_DIGEST = new ThreadLocal<MessageDigest>() {
  @Override
  protected MessageDigest initialValue() {
    try {
      return MessageDigest.getInstance("SHA");
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException("...", e);
    }
  }
};

public void digest(byte[] input) {
  MessageDigest digest = SHA1_DIGEST.get();
  digest.reset();
  return digest.digest(input);
}

Rule 8. 【推薦】縮短鎖

1) 能鎖區塊,就不要鎖整個方法體;

//鎖整個方法,等價於整個方法體內synchronized(this)
public synchronized boolean foo(){};

//鎖區塊方法,僅對需要保護的原子操作的連續程式碼塊進行加鎖。
public boolean foo() {
    synchronized(this) {
        ...
        ...
    }
    //other stuff
}

2)能用物件鎖,就不要用類鎖。

//物件鎖,隻影響使用同一個物件加鎖的執行緒
synchronized(this) {
    ...
}

//類鎖,使用類物件作為鎖物件,影響所有執行緒。
synchronized(A.class) {
    ...
}

Rule 10. 【推薦】選擇分離鎖,分散鎖甚至無鎖的資料結構

  • 分離鎖:

1) 讀寫分離鎖ReentrantReadWriteLock,讀讀之間不加鎖,僅在寫讀和寫寫之間加鎖;

2) Array Base的queue一般是全域性一把鎖,而Linked Base的queue一般是隊頭隊尾兩把鎖。

  • 分散鎖(又稱分段鎖):

1)如JDK7的ConcurrentHashMap,分散成16把鎖;

2)對於經常寫,少量讀的計數器,推薦使用JDK8或vjkit封裝的LongAdder物件效能更好(內部分散成多個counter,減少樂觀鎖的使用,取值時再相加所有counter)

  • 無鎖的資料結構:

1)完全無鎖無等待的結構,如JDK8的ConcurrentHashMap;

2)基於CAS的無鎖有等待的資料結構,如AtomicXXX系列。


Rule 11. 【推薦】基於ThreadLocal來避免鎖

比如Random例項雖然是執行緒安全的,但其實它的seed的訪問是有鎖保護的。因此建議使用JDK7的ThreadLocalRandom,通過在每個執行緒裡放一個seed來避免了加鎖。


Rule 12. 【推薦】規避死鎖風險

對多個資源多個物件的加鎖順序要一致。

如果無法確定完全避免死鎖,可以使用帶超時控制的tryLock語句加鎖。


Rule 13. 【推薦】volatile修飾符,AtomicXX系列的正確使用

多執行緒共享的物件,在單一執行緒內的修改並不保證對所有執行緒可見。使用volatile定義變數可以解決(解決了可見性)。

但是如果多條執行緒併發進行基於當前值的修改,如併發的counter++,volatile則無能為力(解決不了原子性)。

此時可使用Atomic*系列:

AtomicInteger count = new AtomicInteger(); 
count.addAndGet(2); 

但如果需要原子地同時對多個AtomicXXX的Counter進行操作,則仍然需要使用synchronized將改動程式碼塊加鎖。


Rule 14. 【推薦】延時初始化的正確寫法

通過雙重檢查鎖(double-checked locking)實現延遲初始化存在隱患,需要將目標屬性宣告為volatile型,為了更高的效能,還要把volatile屬性賦予給臨時變數,寫法複雜。

所以如果只是想簡單的延遲初始化,可用下面的靜態類的做法,利用JDK本身的class載入機制保證唯一初始化。

private static class LazyObjectHolder {
  static final LazyObject instance = new LazyObject();
}

public void myMethod() {
  LazyObjectHolder.instance.doSomething();
}

(十) 異常處理

Rule 1. 【強制】建立異常的消耗大,只用在真正異常的場景

構造異常時,需要獲得整個呼叫棧,有一定消耗。

不要用來做流程控制,條件控制,因為異常的處理效率比條件判斷低。

發生概率較高的條件,應該先進行檢查規避,比如:IndexOutOfBoundsException,NullPointerException等,所以如果程式碼裡捕獲這些異常通常是個壞味道。

//WRONG
try { 
  return obj.method();
} catch (NullPointerException e) {
  return false;
}

//RIGHT
if (obj == null) {
  return false;
}     

Rule 2. 【推薦】在特定場景,避免每次構造異常

如上,異常的建構函式需要獲得整個呼叫棧。

如果異常頻繁發生,且不需要列印完整的呼叫棧時,可以考慮繞過異常的建構函式。

1) 如果異常的message不變,將異常定義為靜態成員變數;

下例定義靜態異常,並簡單定義一層的StackTrace。ExceptionUtil見vjkit。

private static RuntimeException TIMEOUT_EXCEPTION = ExceptionUtil.setStackTrace(new RuntimeException("Timeout"),
MyClass.class, "mymethod");

...

throw TIMEOUT_EXCEPTION;

2) 如果異常的message會變化,則對靜態的異常例項進行clone()再修改message。

Exception預設不是Cloneable的,CloneableException見vjkit。

private static CloneableException TIMEOUT_EXCEPTION = new CloneableException("Timeout") .setStackTrace(My.class,
 "hello"); 

...

throw TIMEOUT_EXCEPTION.clone("Timeout for 40ms");

3)自定義異常,也可以考慮過載fillStackTrace()為空函式,但相對沒那麼靈活,比如無法按場景指定一層的StackTrace。


Rule 3. 【推薦】自定義異常,建議繼承RuntimeException

詳見《Clean Code》,爭論已經結束,不再推薦原本初衷很好的CheckedException。

因為CheckedException需要在丟擲異常的地方,與捕獲處理異常的地方之間,層層定義throws XXX來傳遞Exception,如果底層程式碼改動,將影響所有上層函式的簽名,導致編譯出錯,對封裝的破壞嚴重。對CheckedException的處理也給上層程式設計師帶來了額外的負擔。因此其他語言都沒有CheckedException的設計。


Rule 4. 【推薦】異常日誌應包含排查問題的足夠資訊

異常資訊應包含排查問題時足夠的上下文資訊。

捕獲異常並記錄異常日誌的地方,同樣需要記錄沒有包含在異常資訊中,而排查問題需要的資訊,比如捕獲處的上下文資訊。

//WRONG
new TimeoutException("timeout");
logger.error(e.getMessage(), e);


//RIGHT
new TimeoutException("timeout:" + eclapsedTime + ", configuration:" + configTime);
logger.error("user[" + userId + "] expired:" + e.getMessage(), e);
  • Facebook-Contrib: Style - Method throws exception with static message string

Rule 5. 異常丟擲的原則

5.1 【推薦】儘量使用JDK標準異常,專案標準異常

儘量使用JDK標準的Runtime異常如IllegalArgumentExceptionIllegalStateExceptionUnsupportedOperationException,專案定義的Exception如ServiceException

5.2 【推薦】根據呼叫者的需要來定義異常類,直接使用RuntimeException是允許的

是否定義獨立的異常類,關鍵是呼叫者會如何處理這個異常,如果沒有需要特別的處理,直接丟擲RuntimeException也是允許的。


Rule 6. 異常捕獲的原則

6.1 【推薦】按需要捕獲異常,捕獲ExceptionThrowable是允許的

如果無特殊處理邏輯,統一捕獲Exception統一處理是允許的。

捕獲Throwable是為了捕獲Error類異常,包括其實無法處理的OOM StackOverflow ThreadDeath,以及類載入,反射時可能丟擲的NoSuchMethodError NoClassDefFoundError等。

6.2【推薦】多個異常的處理邏輯一致時,使用JDK7的語法避免重複程式碼

try {
  ...
} catch (AException | BException | CException ex) {
  handleException(ex);
}

Rule 7.異常處理的原則

7.1 【強制】捕獲異常一定要處理;如果故意捕獲並忽略異常,須要註釋寫明原因

方便後面的閱讀者知道,此處不是漏了處理。

//WRONG
try {
} catch(Exception e) {
}

//RIGHT
try {
} catch(Exception ignoredExcetpion) {
    //continue the loop
}

7.2 【強制】異常處理不能吞掉原異常,要麼在日誌列印,要麼在重新丟擲的異常裡包含原異常

 //WRONG
throw new MyException("message");

//RIGHT 記錄日誌後丟擲新異常,向上次呼叫者遮蔽底層異常
logger.error("message", ex); 
throw new MyException("message"); 

//RIGHT 傳遞底層異常
throw new MyException("message", ex); 

7.3 【強制】如果不想處理異常,可以不進行捕獲。但最外層的業務使用者,必須處理異常,將其轉化為使用者可以理解的內容


Rule 8. finally塊的處理原則

8.1 【強制】必須對資源物件、流物件進行關閉,或使用語法try-with-resource

關閉動作必需放在finally塊,不能放在try塊 或 catch塊,這是經典的錯誤。

更加推薦直接使用JDK7的try-with-resource語法自動關閉Closeable的資源,無需在finally塊處理,避免潛在問題。

try (Writer writer = ...) {
  writer.append(content);
}

8.2 【強制】如果處理過程中有丟擲異常的可能,也要做try-catch,否則finally塊中丟擲的異常,將代替try塊中丟擲的異常

//WRONG
try {
  ...
  throw new TimeoutException();
} finally {
  file.close();//如果file.close()丟擲IOException, 將代替TimeoutException
}

//RIGHT, 在finally塊中try-catch
try {
  ...
  throw new TimeoutException();
} finally {
  IOUtil.closeQuietly(file); //該方法中對所有異常進行了捕獲
}

8.3 【強制】不能在finally塊中使用return,finally塊中的return將代替try塊中的return及throw Exception

//WRONG
try {
  ...
  return 1;
} finally {
  return 2; //實際return 2 而不是1
}

try {
  ...
  throw TimeoutException();
} finally {
  return 2; //實際return 2 而不是TimeoutException
}

(十一) 日誌規約

Rule 1. 【強制】應用中不可直接使用日誌庫(Log4j、Logback)中的API,而應使用日誌框架SLF4J中的API

使用門面模式的日誌框架,有利於維護各個類的日誌處理方式統一。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static Logger logger = LoggerFactory.getLogger(Foo.class);

Rule 2. 【推薦】對不確定會否輸出的日誌,採用佔位符或條件判斷

//WRONG
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);

如果日誌級別是info,上述日誌不會列印,但是會執行1)字串拼接操作,2)如果symbol是物件,還會執行toString()方法,浪費了系統資源,最終日誌卻沒有列印。

//RIGHT
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);

但如果symbol.getMessage()本身是個消耗較大的動作,佔位符在此時並沒有幫助,須要改為條件判斷方式來完全避免它的執行。

//WRONG
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol.getMessage());

//RIGHT
if (logger.isDebugEnabled()) {
  logger.debug("Processing trade with id: " + id + " symbol: " + symbol.getMessage());
}

Rule 3. 【推薦】對確定輸出,而且頻繁輸出的日誌,採用直接拼裝字串的方式

如果這是一條WARN,ERROR級別的日誌,或者確定輸出的INFO級別的業務日誌,直接字串拼接,比使用佔位符替換,更加高效。

Slf4j的佔位符並沒有魔術,每次輸出日誌都要進行佔位符的查詢,字串的切割與重新拼接。

//RIGHT
logger.info("I am a business log with id: " + id + " symbol: " + symbol);

//RIGHT
logger.warn("Processing trade with id: " + id + " symbol: " + symbol);

Rule 4. 【推薦】儘量使用非同步日誌

低延時的應用,使用非同步輸出的形式(以AsyncAppender串接真正的Appender),可減少IO造成的停頓。

需要正確配置非同步佇列長度及佇列滿的行為,是丟棄還是等待可用,業務上允許丟棄的儘量選丟棄。


Rule 5. 【強制】禁止使用效能很低的System.out()列印日誌資訊

同理也禁止e.printStackTrace();

例外: 應用啟動和關閉時,擔心日誌框架還未初始化或已關閉。


Rule 6. 【強制】禁止配置日誌框架輸出日誌列印處的類名,方法名及行號的資訊

日誌框架在每次列印時,通過主動獲得當前執行緒的StackTrace來獲取上述資訊的消耗非常大,儘量通過Logger名本身給出足夠資訊。


Rule 7. 【推薦】謹慎地記錄日誌,避免大量輸出無效日誌,資訊不全的日誌

大量地輸出無效日誌,不利於系統效能,也不利於快速定位錯誤點。

記錄日誌時請思考:這些日誌真的有人看嗎?看到這條日誌你能做什麼?能不能給問題排查帶來好處?


Rule 8. 【推薦】使用warn級別而不是error級別,記錄外部輸入引數錯誤的情況

如非必要,請不在此場景列印error級別日誌,避免頻繁報警。

error級別只記錄系統邏輯出錯、異常或重要的錯誤資訊。


(十二) 其他規約

Rule 1. 【參考】儘量不要讓魔法值(即未經定義的數字或字串常量)直接出現在程式碼中

 //WRONG
 String key = "Id#taobao_"+tradeId;
 cache.put(key, value);

例外:-1,0,1,2,3 不認為是魔法數


Rule 2. 【推薦】時間獲取的原則

1)獲取當前毫秒數System.currentTimeMillis() 而不是new Date().getTime(),後者的消耗要大得多。

2)如果要獲得更精確的,且不受NTP時間調整影響的流逝時間,使用System.nanoTime()獲得機器從啟動到現在流逝的納秒數。

3)如果希望在測試用例中控制當前時間的值,則使用vjkit的Clock類封裝,在測試和生產環境中使用不同的實現。


Rule 3. 【推薦】變數宣告儘量靠近使用的分支

不要在一個程式碼塊的開頭把區域性變數一次性都宣告瞭(這是c語言的做法),而是在第一次需要使用它時才宣告。

否則如果方法已經退出或進入其他分支,就白白初始化了變數。

//WRONG
Foo foo = new Foo();

if(ok){
    return;
}

foo.bar();

Rule 4. 【推薦】不要像C那樣一行裡做多件事情

 //WRONG
fooBar.fChar = barFoo.lchar = 'c'; 
argv++; argc--;       
int level, size;

Rule 5. 【推薦】不要為了效能而使用JNI本地方法

Java在JIT後並不比C程式碼慢,JNI方法因為要反覆跨越JNI與Java的邊界反而有額外的效能損耗。

因此JNI方法僅建議用於呼叫"JDK所沒有包括的, 對特定作業系統的系統呼叫"


Rule 6. 【推薦】正確使用反射,減少效能損耗

獲取Method/Field物件的效能消耗較大, 而如果對Method與Field物件進行快取再反覆呼叫,則並不會比直接呼叫類的方法與成員變數慢(前15次使用NativeAccessor,第15次後會生成GeneratedAccessorXXX,bytecode為直接呼叫實際方法)

//用於對同一個方法多次呼叫
private Method method = ....

public void foo(){
  method.invoke(obj, args);
}

//用於僅會對同一個方法單次呼叫
ReflectionUtils.invoke(obj, methodName, args);

Rule 7.【推薦】可降低優先順序的常見程式碼檢查規則

  1. 介面內容的定義中,去除所有modifier,如public等。 (多個public也沒啥,反正大家都看慣了)
  2. 工具類,定義private建構函式使其不能被例項化。(命名清晰的工具類,也沒人會去例項化它,對靜態方法通過類來訪問也能避免例項化)

《阿里Java開發手冊》定製紀錄

只記錄較大的改動,對更多條目內容的重新組織與擴寫,則未一一冗述。

(一) 命名規約

對應 阿里規範《命名風格》一章

VIP 規範 阿里規範 修改
13. 變數、引數重名覆蓋 新增規則
1. 禁止拼音縮寫 2. 嚴禁使用拼音與英文混合的方式 改寫規則
3. 禁用其他程式語言風格的字首和字尾 1. 程式碼中的命名均不能以下劃線或美元符號開始 擴寫規則,把其他語言的囉嗦都禁止掉
4. 命名的好壞,在於其“模糊度” 11. 為了達到程式碼自解釋的目標 擴寫規則,參考《Clean Code》的更多例子
6. 常量命名全部大寫 5.常量名大寫 擴寫規則
7. 型別與中括號緊挨相連來定義陣列 刪除規則,非命名風格,也不重要
13. 介面類中的方法和成員變數不要加任何修飾符號 移動規則,非命名風格,移到類設計
16. 各層命名規約 刪除規則,各公司有自己的習慣

(二) 格式規約

對應 阿里規範《程式碼格式》一章

VIP 規範 阿里規範 修改
1. 專案組統一的程式碼格式模板 規則1-8 用IDE模版代替逐條描述 同時對Tab/空格不做硬性規定
3. 用小括號來限定運算優先順序 新增規則
4. 類內方法定義的順序 新增規則
5. 通過空行進行邏輯分段 11. 不同邏輯、不同語義 改寫規則
6. 避免IDE格式化 新增規則
10.單個方法行數不超過80行 刪除規則,非格式規約,移動方法設計
11.沒有必要增加若干空格來對齊 刪除規則,現在很少人這麼做

(三) 註釋規約

對應 阿里規範《註釋規約》一章

VIP 規範 阿里規範 修改
2. 刪除空註釋,無意義註釋 增加規則
7. JavaDoc中不要大量使用HTML標籤和轉義字元 增加規則
1. 註釋的基本要求 9. 對於註釋的要求 擴寫規則
4.避免建立人的註釋 3.所有的類都必須新增建立者 衝突規則
2.所有的抽象方法必須用Javadoc註釋 刪除規則,因為規則2不強制,併入規則1
4.方法內部單行註釋,使用//註釋 刪除規則,區別不大不強求
5. 所有的列舉型別欄位必須要有註釋 刪除規則,因為規則2不強制

(四) 方法設計


(五) 類設計

對應 阿里規範《OOP規範》一章

VIP 規範 阿里規範 修改
2.減少類之間的依賴 增加規則
3.定義變數與方法引數時,儘量使用介面 增加規則
4.類的長度度量 增加規則
5.Builder模式 增加規則
8.靜態方法不能被覆寫 增加規則
9.靜態方法的訪問原則 擴寫規則
10.內部類原則 增加規則
12-14.hashCode,equals,toString的規則 增加規則
16.總是移除無用屬性、方法與引數 增加規則
18.【推薦】得墨忒耳法則 增加規則
3. 提倡同學們儘量不用可變引數程式設計 刪除規則
9. 定義DO/DTO/VO等POJO類時,不要設定任何屬性預設值 刪除規則
10. 序列化類新增屬性時,請不要修改serialVersionUID欄位 刪除規則
13. 使用索引訪問用String的split方法時 刪除規則
19. 慎用Object的clone方法來拷貝物件 刪除規則
規則4,5 移到《方法規約》
規則6 移到《通用設計》
規則7,8,17 移到《基礎型別》
規則14,15 移到《格式規約》

(六) 控制語句

對應 阿里規範《控制語句》一章

VIP 規範 阿里規範 修改
4.布林表示式中的運算子個數不超過4個 擴寫規則
5.善用三目運算子 增加規則
6.能造成短路概率較大的邏輯放前面 增加規則
10.能用while迴圈實現的程式碼,就不用do-while迴圈 增加規則
3. 在高併發場景中,避免使用 ”等於”作為條件 刪除規則
8. 介面入參保護 刪除規則
9. 下列情形,需要進行引數校驗 移到《方法規約》
10. 下列情形,不需要進行引數校 移到《方法規約》

(七) 基本型別與字串


(八) 集合處理

對應 阿里規範《集合處理》一章

VIP 規範 阿里規範 修改
2. foreach語法遍歷 增加規則
7. 長生命週期的集合 增加規則
8. 併發集合 增加規則
9. 泛型的萬用字元 增加規則
10. List, ListList的選擇 增加規則
11. EnumMap 增加規則
2. ArrayList的subList結果 刪除規則
6. 泛型萬用字元 刪除規則
12.合理利用好集合的有序性 刪除規則
13.利用Set元素唯一的特性 刪除規則
12.Array 與 List互轉的 使用集合轉陣列的方法,必須使用集合的toArray 某位老大的測試,new String[0]也不錯

(九) 併發處理: 併發與多執行緒

對應 阿里規範《併發處理》一章

VIP 規範 阿里規範 修改
1. 指定執行緒名 擴寫規則
4. 正確停止執行緒 擴寫規則
5. 編寫可中斷的Runnable 增加規則
6. Runnable中必須捕獲一切異常 9.多執行緒並行處理定時任務 擴寫規則
7. 全域性變數的執行緒安全 擴寫規則
10. 選擇分離鎖, 分散鎖甚至無鎖的資料結構 增加規則
13. volatile修飾符,AtomicXX系列的正確使用 擴寫規則
8.併發修改同一記錄時,需要加鎖 刪除規則
10.使用CountDownLatch進行非同步轉同步操作 刪除規則
14.HashMap在容量不夠進行resize 移到《集合規約》一章
14. 延時初始化的正確寫法 12.雙重檢查鎖 衝突規則

(十) 異常處理

對應 阿里規範《異常處理》一章

VIP 規範 阿里規範 修改
2.在特定場合,避免每次構造異常 增加規則
5.異常丟擲的原則 增加規則
6.異常捕獲的原則 增加規則
7.異常處理的原則 增加規則
8.捕獲異常與拋異常,必須是完全匹配 刪除規則
12.對於公司外的開放介面必須使用“錯誤碼” 刪除規則
13.DRY原則 刪除規則,為什麼會出現在這章,太著名了
9.返回值可以為null 移到《方法設計》一章
10. 【推薦】防止NPE,是程式設計師的基本修養 拆開到各章
11.避免直接丟擲RuntimeException 規則衝突

(十一) 日誌規約

對應 阿里規範《日誌規約》一章

VIP 規範 阿里規範 修改
4.儘量使用非同步日誌 增加規則
5.禁止使用System.out() 增加規則
6.禁止配置日誌框架列印日誌列印時的類名,行號等資訊 增加規則
2.日誌檔案推薦至少儲存15天 刪除規則
3.應用中的擴充套件日誌命名方式 刪除規則
6.異常資訊應該包括兩類資訊 移到《異常處理》
2.合理使用使用佔位符 4.對trace/debug/info級別的日誌使用佔位符 還是要判斷日誌是否必然輸出, 並強調條件判斷與佔位符之間的差別

(十二) 其他規約

保留 阿里規範《常量定義》一章的規則1

VIP 規範 阿里規範 修改
規則2-4 刪除規則
規則5. 如果變數值僅在一個固定範圍內變化用enum型別來定義 移到《基本型別》

保留 阿里規範《其他》一章的規則7

VIP 規範 阿里規範 修改
規則2-4,6-8 刪除規則
規則1. 在使用正規表示式時,利用好其預編譯功 移到《基本型別》
  • 規則3,4,5,6,7均為新增規則

相關文章