唯品會Java開發手冊》1.0.2版閱讀
《唯品會Java開發手冊》1.0.2版閱讀
1. 概述
《阿里巴巴Java開發手冊》,是首個對外公布的企業級Java開發手冊,對整個業界都有重要的意義。
我們結合唯品會的內部經驗,參考《Clean Code》、《Effective Java》等重磅資料,增補了一些條目,也做了些精簡。
感謝阿里授權我們定製和再發佈。
2. 規範正文
-
命名規
注意: 如需全文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) callerId
與 calleeId
, mydearfriendswitha
與 mydearfriendswithb
這種拼寫極度接近,考驗閱讀者眼力的。
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
- Sonar-101:Class names should comply with a naming convention
- Sonar-114:Interface names should comply with a naming convention
Rule 7. 【強制】方法名、參數名、成員變量、局部變量使用lowerCamelCase風格,遵從駝峰形式
正例: localValue / getHttpMessage();
- Sonar-100:Method names should comply with a naming convention
- Sonar-116:Field names should comply with a naming convention
- Sonar-117:Local variable and method parameter names should comply with a naming convention
Rule 8. 【強制】常量命名全大寫,單詞間用下劃線隔開。力求語義表達完整清楚,不要嫌名字長
正例: MAX_STOCK_COUNT
反例: MAX_COUNT
例外:當一個static final字段不是一個真正常量,比如不是基本類型時,不需要使用大寫命名。
private static final Logger logger = Logger.getLogger(MyClass.class);
例外:枚舉常量推薦全大寫,但如果歷史原因未遵循也是允許的,所以我們修改了Sonar的規則。
- Sonar-115:Constant names should comply with a naming convention
- Sonar-308:Static non-final field names should comply with a naming convention
Rule 9. 【推薦】如果使用到了通用的設計模式,在類名中體現,有利於閱讀者快速理解設計思想
正例:OrderFactory, LoginProxy ,ResourceObserver
Rule 10. 【推薦】枚舉類名以Enum結尾; 抽象類使用Abstract或Base開頭;異常類使用Exception結尾;測試類以它要測試的類名開始,以Test結尾
正例:DealStatusEnum, AbstractView,BaseView, TimeoutException,UserServiceTest
- Sonar-2166:Classes named like “Exception” should extend “Exception” or a subclass
- Sonar-3577:Test classes should comply with a naming convention
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;
}
}
- Sonar-2387: Child class fields should not shadow parent class fields
- Sonar: Local variables should not shadow class fields
(二) 格式規約
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))
- Sonar-1068:Limited dependence should be placed on operator precedence rules in expressions,我們修改了三目運算符
foo!=null?foo:""
不需要加括號。
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標籤,以及<
"
這樣的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編譯成二進制碼。
- Sonar-107: Methods should not have too many lines,默認值改為100
- Facebook-Contrib:Performance – This method is too long to be compiled by the 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
- Sonar-2209: “static” members should be accessed statically
- Sonar-2440: Classes with only “static” methods should not be instantiated
9.2 【推薦】除測試用例,不要static import 靜態方法
靜態導入後忽略掉的類名,給閱讀者造成障礙。
例外:測試環境中的assert語句,大家都太熟悉了。
- Sonar-3030: Classes should not have too many “static” imports 但IDEA經常自動轉換static import,所以暫不作為規則。
9.3【推薦】盡量避免在非靜態方法中修改靜態成員變量的值
// WRONG
public void foo() {
ClassA.staticFiled = 1;
}
- Sonar-2696: Instance methods should not write to “static” fields
- Sonar-3010: Static fields should not be updated in constructors
Rule 10.【推薦】 內部類的定義原則
當一個類與另一個類關聯非常緊密,處於從屬的關係,特別是只有該類會訪問它時,可定義成私有內部類以提高封裝性。
另外,內部類也常用作回調函數類,在JDK8下建議寫成Lambda。
內部類分匿名內部類,內部類,靜態內部類三種。
- 匿名內部類 與 內部類,按需使用:
在性能上沒有區別;當內部類會被多個地方調用,或匿名內部類的長度太長,已影響對調用它的方法的閱讀時,定義有名字的內部類。
- 靜態內部類 與 內部類,優先使用靜態內部類:
- 非靜態內部類持有外部類的引用,能訪問外類的實例方法與屬性。構造時多傳入一個引用對性能沒有太大影響,更關鍵的是向閱讀者傳遞自己的意圖,內部類會否訪問外部類。
- 非靜態內部類里不能定義static的屬性與方法。
- Sonar-2694: Inner classes which do not reference their owning classes should be “static”
- Sonar-1604: Anonymous inner classes containing only one method should become lambdas
Rule 11.【推薦】使用getter/setter方法,還是直接public成員變量的原則。
除非因為特殊原因方法內聯失敗,否則使用getter方法與直接訪問成員變量的性能是一樣的。
使用getter/setter,好處是可以進一步的處理:
- 通過隱藏setter方法使得成員變量只讀
- 增加簡單的校驗邏輯
- 增加簡單的值處理,值類型轉換等
建議通過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方法上的參數,一旦無用立刻移除。信任代碼版本管理系統。
- Sonar-3985: Unused “private” classes should be removed
- Sonar-1068: Unused “private” fields should be removed
- Sonar: Unused “private” methods should be removed
- Sonar-1481: Unused local variables should be removed
- Sonar-1172: Unused method parameters should be removed Sonar-VJ版只對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()函數
- Sonar-121: Control structures should use curly braces Sonar-VJ版豁免了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. 【強制】集合如果存在並發修改的場景,需要使用線程安全的版本
- 著名的反例,HashMap擴容時,遇到並發修改可能造成100%CPU佔用。
推薦使用java.util.concurrent(JUC)
工具包中的並發版集合,如ConcurrentHashMap等,優於使用Collections.synchronizedXXX()系列函數進行同步化封裝(等價於在每個方法都加上synchronized關鍵字)。
例外:ArrayList所對應的CopyOnWriteArrayList,每次更新時都會複製整個數組,只適合於讀多寫很少的場景。如果頻繁寫入,可能退化為使用Collections.synchronizedList(list)。
- 即使線程安全類仍然要注意函數的正確使用。
例如:即使用了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)
原則:
- 如果集合要被讀取,定義成“
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);
- 如果集合要被寫入,定義成“
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
, List
與 List
的選擇
定義成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而向外拋出,會發生下列情況:
- ScheduledExecutorService執行定時任務,任務會被中斷,該任務將不再定時調度,但線程池裡的線程還能用於其他任務。
- ExecutorService執行任務,當前線程會中斷,線程池需要創建新的線程來響應後續任務。
- 如果沒有在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);
}
- Sonar-2885: Non-thread-safe fields should not be static
- Facebook-Contrib: Correctness – Field is an instance based ThreadLocal variable
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異常如IllegalArgumentException
,IllegalStateException
,UnsupportedOperationException
,項目定義的Exception如ServiceException
。
5.2 【推薦】根據調用者的需要來定義異常類,直接使用RuntimeException
是允許的
是否定義獨立的異常類,關鍵是調用者會如何處理這個異常,如果沒有需要特別的處理,直接拋出RuntimeException也是允許的。
Rule 6. 異常捕獲的原則
6.1 【推薦】按需要捕獲異常,捕獲Exception
或Throwable
是允許的
如果無特殊處理邏輯,統一捕獲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);
- Sonar-1166: Exception handlers should preserve the original exceptions,其中默認包含了InterruptedException, NumberFormatException,NoSuchMethodException等若干例外
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();
例外: 應用啟動和關閉時,擔心日誌框架還未初始化或已關閉。
- Sonar-106: Standard outputs should not be used directly to log anything
- Sonar-1148: Throwable.printStackTrace(…) should not be called
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 不認為是魔法數
- Sonar-109: Magic numbers should not be used 但現實中所謂魔法數還是太多,該規則不能被真正執行。
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.【推薦】可降低優先級的常見代碼檢查規則
- 接口內容的定義中,去除所有modifier,如public等。 (多個public也沒啥,反正大家都看慣了)
- 工具類,定義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不強制 |
(四) 方法設計
- 規則 6,7,12,13 從阿里規範《控制語句》一章 移入
- 規則 9 從阿里規範《異常處理》一章 移入
- 規則1,2,3,4,5,8,10,11,14為新建規則
(五) 類設計
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, List 與 List 的選擇 |
增加規則 | |
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均為新增規則