唯品會Java開發手冊》1.0.2版閱讀
阿新 • • 發佈:2020-10-12
# [《唯品會Java開發手冊》1.0.2版](https://vipshop.github.io/vjtools/#/standard/?id=《唯品會java開發手冊》102版)閱讀
## 1. 概述
[《阿里巴巴Java開發手冊》](https://github.com/alibaba/p3c),是首個對外公佈的企業級Java開發手冊,對整個業界都有重要的意義。
我們結合唯品會的內部經驗,參考《Clean Code》、《Effective Java》等重磅資料,增補了一些條目,也做了些精簡。
感謝阿里授權我們定製和再發布。
## 2. 規範正文
1. 命名規
[TOC]
注意: 如需全文pdf版,請下載原始碼,在docs/standard/目錄執行merge.sh生成。
## 3. 規範落地
規則落地主要依靠程式碼格式模版與[Sonar程式碼規則檢查](https://www.sonarqube.org/)。
其中Sonar規則不如人意的地方,我們進行了定製。
- [Eclipse/Intellij 格式模板](https://github.com/vipshop/vjtools/tree/master/standard/formatter)
- [Sonar 規則修改示例](https://github.com/vipshop/vjtools/tree/master/standard/sonar-vj)
## 4. 參考資料
- [《Clean Code》](https://book.douban.com/subject/4199741/)
- [《Effective Java 2nd》](https://book.douban.com/subject/3360807/)
- [《SEI CERT Oracle Coding Standard for Java》(線上版)](https://www.securecoding.cert.org/confluence/display/java/SEI+CERT+Oracle+Coding+Standard+for+Java)
- [Sonar Java Rules](https://rules.sonarsource.com/java/)
## 5. 定製記錄
- [《唯品會Java開發手冊》-與阿里手冊的比較文學I](http://calvin1978.blogcn.com/?p=1771)
- [阿里手冊的增補與刪減記錄](https://vipshop.github.io/vjtools/#/standard/ali)
# (一) 命名規約
**Rule 1. 【強制】禁止拼音縮寫,避免閱讀者費勁猜測;儘量不用拼音,除非中國式業務詞彙沒有通用易懂的英文對應。**
```text
禁止: DZ[打折] / getPFByName() [評分]
儘量避免:Dazhe / DaZhePrice
```
------
**Rule 2. 【強制】禁止使用非標準的英文縮寫**
```text
反例: 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. 【推薦】包名全部小寫。點分隔符之間儘量只有一個英語單詞,即使有多個單詞也不使用下劃線或大小寫分隔**
```text
正例: com.vip.javatool
反例: com.vip.java_tool, com.vip.javaTool
```
- [Sonar-120:Package names should comply with a naming convention](https://rules.sonarsource.com/java/RSPEC-120)
------
**Rule 6. 【強制】類名與介面名使用UpperCamelCase風格,遵從駝峰形式**
Tcp, Xml等縮寫也遵循駝峰形式,可約定例外如:DTO/ VO等。
```text
正例:UserId / XmlService / TcpUdpDeal / UserVO
反例:UserID / XMLService / TCPUDPDeal / UserVo
```
- [Sonar-101:Class names should comply with a naming convention](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-101)
- [Sonar-114:Interface names should comply with a naming convention](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-114)
------
**Rule 7. 【強制】方法名、引數名、成員變數、區域性變數使用lowerCamelCase風格,遵從駝峰形式**
```text
正例: localValue / getHttpMessage();
```
- [Sonar-100:Method names should comply with a naming convention](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-100)
- [Sonar-116:Field names should comply with a naming convention](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-116)
- [Sonar-117:Local variable and method parameter names should comply with a naming convention](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-117)
------
**Rule 8. 【強制】常量命名全大寫,單詞間用下劃線隔開。力求語義表達完整清楚,不要嫌名字長**
```text
正例: MAX_STOCK_COUNT
反例: MAX_COUNT
```
例外:當一個static final欄位不是一個真正常量,比如不是基本型別時,不需要使用大寫命名。
```java
private static final Logger logger = Logger.getLogger(MyClass.class);
```
例外:列舉常量推薦全大寫,但如果歷史原因未遵循也是允許的,所以我們修改了Sonar的規則。
- [Sonar-115:Constant names should comply with a naming convention](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-115)
- [Sonar-308:Static non-final field names should comply with a naming convention](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-308)
------
**Rule 9. 【推薦】如果使用到了通用的設計模式,在類名中體現,有利於閱讀者快速理解設計思想**
```text
正例:OrderFactory, LoginProxy ,ResourceObserver
```
------
**Rule 10. 【推薦】列舉類名以Enum結尾; 抽象類使用Abstract或Base開頭;異常類使用Exception結尾;測試類以它要測試的類名開始,以Test結尾**
```text
正例:DealStatusEnum, AbstractView,BaseView, TimeoutException,UserServiceTest
```
- [Sonar-2166:Classes named like "Exception" should extend "Exception" or a subclass](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-2166)
- [Sonar-3577:Test classes should comply with a naming convention](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-3577)
------
**Rule 11. 【推薦】實現類儘量用Impl的字尾與介面關聯,除了形容能力的介面**
```text
正例:CacheServiceImpl 實現 CacheService介面。
正例: Foo 實現 Translatable介面。
```
------
**Rule 12. 【強制】POJO類中布林型別的變數名,不要加is字首,否則部分框架解析會引起序列化錯誤**
```text
反例:Boolean isSuccess的成員變數,它的GET方法也是isSuccess(),部分框架在反射解析的時候,“以為”對應的成員變數名稱是success,導致出錯。
```
------
**Rule 13. 【強制】避免成員變數,方法引數,區域性變數的重名複寫,引起混淆**
- 類的私有成員變數名,不與父類的成員變數重名
- 方法的引數名/區域性變數名,不與類的成員變數重名 (getter/setter例外)
下面錯誤的地方,Java在編譯時很坑人的都是合法的,但給閱讀者帶來極大的障礙。
```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](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-2387)
- [Sonar: Local variables should not shadow class fields](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-1117)
# (二) 格式規約
**Rule 1. 【強制】使用專案組統一的程式碼格式模板,基於IDE自動的格式化**
1)IDE的預設程式碼格式模板,能簡化絕大部分關於格式規範(如空格,括號)的描述。
2)統一的模板,並在接手舊專案先進行一次全面格式化,可以避免, 不同開發者之間,因為格式不統一產生程式碼合併衝突,或者程式碼變更日誌中因為格式不同引起的變更,掩蓋了真正的邏輯變更。
3)設定專案組統一的行寬,建議120。
4)設定專案組統一的縮排方式(Tab或二空格,四空格均可),基於IDE自動轉換。
- [VIP程式碼格式化模板](https://github.com/vipshop/vjtools/tree/master/standard/formatter)
------
**Rule 2. 【強制】IDE的text file encoding設定為UTF-8; IDE中檔案的換行符使用Unix格式,不要使用Windows格式**
------
**Rule 3. 【推薦】 用小括號來限定運算優先順序**
我們沒有理由假設讀者能記住整個Java運算子優先順序表。除非作者和Reviewer都認為去掉小括號也不會使程式碼被誤解,甚至更易於閱讀。
```java
if ((a == b) && (c == d))
```
- [Sonar-1068:Limited dependence should be placed on operator precedence rules in expressions](https://www.sonarsource.com/products/codeanalyzers/sonarjava/rules.html#RSPEC-1068),我們修改了三目運算子 `foo!=null?foo:""` 不需要加括號。
------
**Rule 4. 【推薦】類內方法定義的順序,不要“總是在類的最後新增新方法”**
一個類就是一篇文章,想象一個閱讀者的存在,合理安排方法的佈局。
1)順序依次是:建構函式 > (公有方法>保護方法>私有方法) > getter/setter方法。
如果公有方法可以分成幾組,私有方法也緊跟公有方法的分組。
2)當一個類有多個構造方法,或者多個同名的過載方法,這些方法應該放置在一起。其中引數較多的方法在後面。
```java
public Foo(int a) {...}
public Foo(int a, String b) {...}
public void foo(int a) {...}
public void foo(int a, String b) {...}
```
3)作為呼叫者的方法,儘量放在被呼叫的方法前面。
```java
public void foo() {
bar();
}
public void bar() {...}
```
------
**Rule 5. 【推薦】通過空行進行邏輯分段**
一段程式碼也是一段文章,需要合理的分段而不是一口氣讀到尾。
不同組的變數之間,不同業務邏輯的程式碼行之間,插入一個空行,起邏輯分段的作用。
而聯絡緊密的變數之間、語句之間,則儘量不要插入空行。
```java
int width;
int height;
String name;
```
------
**Rule 6.【推薦】避免IDE格式化**
對於一些特殊場景(如使用大量的字串拼接成一段文字,或者想把大量的列舉值排成一列),為了避免IDE自動格式化,土辦法是把註釋符號//加在每一行的末尾,但這有視覺的干擾,可以使用@formatter:off和@formatter:on來包裝這段程式碼,讓IDE跳過它。
```java
// @formatter:off
...
// @formatter:on
```
------
# (三) 註釋規約
**Rule 1.【推薦】基本的註釋要求**
完全沒有註釋的大段程式碼對於閱讀者形同天書,註釋是給自己看的,即使隔很長時間,也能清晰理解當時的思路;註釋也是給繼任者看的,使其能夠快速接替自己的工作。
程式碼將被大量後續維護,註釋如果對閱讀者有幫助,不要吝嗇在註釋上花費的時間。(但也綜合參見規則2,3)
第一、能夠準確反應設計思想和程式碼邏輯;第二、能夠描述業務含義,使別的程式設計師能夠迅速瞭解到程式碼背後的資訊。
除了特別清晰的類,都儘量編寫類級別註釋,說明類的目的和使用方法。
除了特別清晰的方法,對外提供的公有方法,抽象類的方法,同樣儘量清晰的描述:期待的輸入,對應的輸出,錯誤的處理和返回碼,以及可能丟擲的異常。
------
**Rule 2. 【推薦】通過更清晰的程式碼來避免註釋**
在編寫註釋前,考慮是否可以通過更好的命名,更清晰的程式碼結構,更好的函式和變數的抽取,讓程式碼不言自明,此時不需要額外的註釋。
------
**Rule 3. 【推薦】刪除空註釋,無意義註釋**
《Clean Code》建議,如果沒有想說的,不要留著IDE自動生成的,空的@param,@return,@throws 標記,讓程式碼更簡潔。
反例:方法名為put,加上兩個有意義的變數名elephant和fridge,已經說明了這是在幹什麼,不需要任何額外的註釋。
```java
/**
* 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的標記掃描,經常清理此類標記,線上故障經常來源於這些標記但未處理的程式碼。
```java
正例:
//TODO:calvin use xxx to replace yyy.
反例:
//TODO: refactor it
```
- [Sonar: Track uses of "TODO" tags](https://rules.sonarsource.com/java/RSPEC-1135)
- [Sonar: Track uses of "FIXME" tags](https://rules.sonarsource.com/java/RSPEC-1134)
------
**Rule 10. 【推薦】合理處理註釋掉的程式碼**
如果後續會恢復此段程式碼,在目的碼上方用`///`說明註釋動機,而不是簡單的註釋掉程式碼。
如果很大概率不再使用,則直接刪除(版本管理工具儲存了歷史程式碼)。
- [Sonar: Sections of code should not be "commented out"](https://rules.sonarsource.com/java/RSPEC-125)
------
# (四) 方法設計
**Rule 1. 【推薦】方法的長度度量**
方法儘量不要超過100行,或其他團隊共同商定的行數。
另外,方法長度超過8000個位元組碼時,將不會被JIT編譯成二進位制碼。
- [Sonar-107: Methods should not have too many lines](https://rules.sonarsource.com/java/RSPEC-107),預設值改為100
- Facebook-Contrib:Performance - This method is too long to be compiled by the JIT
------
**Rule 2. 【推薦】方法的語句在同一個抽象層級上**
反例:一個方法裡,前20行程式碼在進行很複雜的基本價格計算,然後呼叫一個折扣計算函式,再呼叫一個贈品計算函式。
此時可將前20行也封裝成一個價格計算函式,使整個方法在同一抽象層級上。
------
**Rule 3. 【推薦】為了幫助閱讀及方法內聯,將小概率發生的異常處理及其他極小概率進入的程式碼路徑,封裝成獨立的方法**
```java
if(seldomHappenCase) {
hanldMethod();
}
try {
...
} catch(SeldomHappenException e) {
handleException();
}
```
------
**Rule 4. 【推薦】儘量減少重複的程式碼,抽取方法**
超過5行以上重複的程式碼,都可以考慮抽取公用的方法。
------
**Rule 5. 【推薦】方法引數最好不超過3個,最多不超過7個**
1)如果多個引數同屬於一個物件,直接傳遞物件。
例外: 你不希望依賴整個物件,傳播了類之間的依賴性。
2)將多個引數合併為一個新建立的邏輯物件。
例外: 多個引數之間毫無邏輯關聯。
3)將函式拆分成多個函式,讓每個函式所需的引數減少。
- [Sonar-107: Methods should not have too many parameters](https://rules.sonarsource.com/java/RSPEC-107)
------
**Rule 6.【推薦】下列情形,需要進行引數校驗**
1) 呼叫頻次低的方法。
2) 執行時間開銷很大的方法。此情形中,引數校驗時間幾乎可以忽略不計,但如果因為引數錯誤導致中間執行回退,或者錯誤,代價更大。
3) 需要極高穩定性和可用性的方法。
4) 對外提供的開放介面,不管是RPC/HTTP/公共類庫的API介面。
如果使用Apache Validate 或 Guava Precondition進行校驗,並附加錯誤提示資訊時,注意不要每次校驗都做一次字串拼接。
```java
//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.【推薦】不能使用有繼承關係的引數型別來過載方法**
因為方法過載的引數型別是根據編譯時表面型別匹配的,不根據執行時的實際型別匹配。
```java
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的包下的類,都是不對外暴露的,可隨時被改變的不穩定類。
- [Sonar-1191: Classes from "sun.*" packages should not be used](https://rules.sonarsource.com/java/RSPEC-1191)
------
# (五) 類設計
**Rule 1. 【推薦】類成員與方法的可見性最小化**
任何類、方法、引數、變數,嚴控訪問範圍。過於寬泛的訪問範圍,不利於模組解耦。思考:如果是一個private的方法,想刪除就刪除,可是一個public的service方法,或者一個public的成員變數,刪除一下,不得手心冒點汗嗎?
例外:為了單元測試,有時也可能將訪問範圍擴大,此時需要加上JavaDoc說明或vjkit中的`@VisibleForTesting`註解。
------
**Rule 2.【推薦】 減少類之間的依賴**
比如如果A類只依賴B類的某個屬性,在建構函式和方法引數中,只傳入該屬性。讓閱讀者知道,A類只依賴了B類的這個屬性,而不依賴其他屬性,也不會呼叫B類的任何方法。
```java
a.foo(b); //WRONG
a.foo(b.bar); //RIGHT
```
------
**Rule 3.【推薦】 定義變數與方法引數時,儘量使用介面而不是具體類**
使用介面可以保持一定的靈活性,也能向讀者更清晰的表達你的需求:變數和引數只是要求有一個Map,而不是特定要求一個HashMap。
例外:如果變數和引數要求某種特殊型別的特性,則需要清晰定義該引數型別,同樣是為了向讀者表達你的需求。
------
**Rule 4. 【推薦】類的長度度量**
類儘量不要超過300行,或其他團隊共同商定的行數。
對過大的類進行分拆時,可考慮其內聚性,即類的屬性與類的方法的關聯程度,如果有些屬性沒有被大部分的方法使用,其內聚性是低的。
------
**Rule 5.【推薦】 建構函式如果有很多引數,且有多種引數組合時,建議使用Builder模式**
```java
Executor executor = new ThreadPoolBuilder().coreThread(10).queueLenth(100).build();
```
即使仍然使用建構函式,也建議使用chain constructor模式,逐層加入預設值傳遞呼叫,僅在引數最多的建構函式裡實現構造邏輯。
```java
public A(){
A(DEFAULT_TIMEOUT);
}
public A(int timeout) {
...
}
```
------
**Rule 6.【推薦】建構函式要簡單,尤其是存在繼承關係的時候**
可以將複雜邏輯,尤其是業務邏輯,抽取到獨立函式,如init(),start(),讓使用者顯式呼叫。
```java
Foo foo = new Foo();
foo.init();
```
------
**Rule 7.【強制】所有的子類覆寫方法,必須加`@Override`註解**
比如有時候子類的覆寫方法的拼寫有誤,或方法簽名有誤,導致沒能真正覆寫,加`@Override`可以準確判斷是否覆寫成功。
而且,如果在父類中對方法簽名進行了修改,子類會馬上編譯報錯。
另外,也能提醒閱讀者這是個覆寫方法。
最後,建議在IDE的Save Action中配置自動新增`@Override`註解,如果無意間錯誤同名覆寫了父類方法也能被發現。
- [Sonar-1161: "@Override" should be used on overriding and implementing methods](https://rules.sonarsource.com/java/RSPEC-1161)
------
**Rule 8.【強制】靜態方法不能被子類覆寫。**
因為它只會根據表面型別來決定呼叫的方法。
```java
Base base = new Children();
// 下句實際呼叫的是父類的靜態方法,雖然物件例項是子類的。
base.staticMethod();
```
------
**Rule 9.靜態方法訪問的原則**
**9.1【推薦】避免通過一個類的物件引用訪問此類的靜態變數或靜態方法,直接用類名來訪問即可**
目的是向讀者更清晰傳達呼叫的是靜態方法。可在IDE的Save Action中配置自動轉換。
```java
int i = objectA.staticMethod(); // WRONG
int i = ClassA.staticMethod(); // RIGHT
```
- [Sonar-2209: "static" members should be accessed statically](https://rules.sonarsource.com/java/RSPEC-2209)
- [Sonar-2440: Classes with only "static" methods should not be instantiated](https://rules.sonarsource.com/java/RSPEC-2440)
**9.2 【推薦】除測試用例,不要static import 靜態方法**
靜態匯入後忽略掉的類名,給閱讀者造成障礙。
例外:測試環境中的assert語句,大家都太熟悉了。
- [Sonar-3030: Classes should not have too many "static" imports](https://rules.sonarsource.com/java/RSPEC-3030) 但IDEA經常自動轉換static import,所以暫不作為規則。
**9.3【推薦】儘量避免在非靜態方法中修改靜態成員變數的值**
```java
// WRONG
public void foo() {
ClassA.staticFiled = 1;
}
```
- [Sonar-2696: Instance methods should not write to "static" fields](https://rules.sonarsource.com/java/RSPEC-2696)
- [Sonar-3010: Static fields should not be updated in constructors](https://rules.sonarsource.com/java/RSPEC-3010)
------
**Rule 10.【推薦】 內部類的定義原則**
當一個類與另一個類關聯非常緊密,處於從屬的關係,特別是只有該類會訪問它時,可定義成私有內部類以提高封裝性。
另外,內部類也常用作回撥函式類,在JDK8下建議寫成Lambda。
內部類分匿名內部類,內部類,靜態內部類三種。
1. 匿名內部類 與 內部類,按需使用:
在效能上沒有區別;當內部類會被多個地方呼叫,或匿名內部類的長度太長,已影響對呼叫它的方法的閱讀時,定義有名字的內部類。
1. 靜態內部類 與 內部類,優先使用靜態內部類:
2. 非靜態內部類持有外部類的引用,能訪問外類的例項方法與屬性。構造時多傳入一個引用對效能沒有太大影響,更關鍵的是向閱讀者傳遞自己的意圖,內部類會否訪問外部類。
3. 非靜態內部類裡不能定義static的屬性與方法。
- [Sonar-2694: Inner classes which do not reference their owning classes should be "static"](https://rules.sonarsource.com/java/RSPEC-2694)
- [Sonar-1604: Anonymous inner classes containing only one method should become lambdas](https://rules.sonarsource.com/java/RSPEC-1604)
------
**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方法。**
- [Sonar-1206: "equals(Object obj)" and "hashCode()" should be overridden in pairs](https://rules.sonarsource.com/java/RSPEC-1206)
------
**Rule 14.【強制】使用IDE生成toString,hashCode和equals方法。**
使用IDE生成而不是手寫,能保證toString有統一的格式,equals和hashCode則避免不正確的Null值處理。
子類生成toString() 時,還需要勾選父類的屬性。
------
**Rule 15. 【強制】Object的equals方法容易拋空指標異常,應使用常量或確定非空的物件來呼叫equals**
推薦使用java.util.Objects#equals(JDK7引入的工具類)
```java
"test".equals(object); //RIGHT
Objects.equals(object, "test"); //RIGHT
```
- [Sonar-1132: Strings literals should be placed on the left side when checking for equality](https://rules.sonarsource.com/java/RSPEC-1132)
------
**Rule 16.【強制】除了保持相容性的情況,總是移除無用屬性、方法與引數**
特別是private的屬性、方法、內部類,private方法上的引數,一旦無用立刻移除。信任程式碼版本管理系統。
- [Sonar-3985: Unused "private" classes should be removed](https://rules.sonarsource.com/java/RSPEC-3985)
- [Sonar-1068: Unused "private" fields should be removed](https://rules.sonarsource.com/java/RSPEC-1068)
- [Sonar: Unused "private" methods should be removed](https://rules.sonarsource.com/java/RSPEC-1144)
- [Sonar-1481: Unused local variables should be removed](https://rules.sonarsource.com/java/RSPEC-1481)
- [Sonar-1172: Unused method parameters should be removed](https://rules.sonarsource.com/java/RSPEC-1172) Sonar-VJ版只對private方法的無用引數告警。
------
**Rule 17.【推薦】final關鍵字與效能無關,僅用於下列不可修改的場景**
1) 定義類及方法時,類不可繼承,方法不可覆寫;
2) 定義基本型別的函式引數和變數,不可重新賦值;
3) 定義物件型的函式引數和變數,僅表示變數所指向的物件不可修改,而物件自身的屬性是可以修改的。
------
**Rule 18.【推薦】得墨忒耳法則,不要和陌生人說話**
以下呼叫,一是導致了對A物件的內部結構(B,C)的緊耦合,二是連串的呼叫很容易產生NPE,因此鏈式調用盡量不要過長。
```java
obj.getA().getB().getC().hello();
```
# (六) 控制語句
**Rule 1. 【強制】if, else, for, do, while語句必須使用大括號,即使只有單條語句**
曾經試過合併程式碼時,因為沒加括號,單條語句合併成兩條語句後,仍然認為只有單條語句,另一條語句在迴圈外執行。
其他增加除錯語句等情況也經常引起同樣錯誤。
可在IDE的Save Action中配置自動新增。
```java
if (a == b) {
...
}
```
例外:一般由IDE生成的equals()函式
- [Sonar-121: Control structures should use curly braces](https://rules.sonarsource.com/java/RSPEC-121) Sonar-VJ版豁免了equals()函式
------
**Rule 2.【推薦】少用if-else方式,多用哨兵語句式以減少巢狀層次**
```java
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)來減少巢狀。
```java
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;
}
```
- [Sonar-134: Control flow statements "if", "for", "while", "switch" and "try" should not be nested too deeply](https://rules.sonarsource.com/java/RSPEC-134),增大為4
------
**Rule 4.【推薦】布林表示式中的布林運算子(&&,||)的個數不超過4個,將複雜邏輯判斷的結果賦值給一個有意義的布林變數名,以提高可讀性**
```java
//WRONG
if ((file.open(fileName, "w") != null) && (...) || (...)|| (...)) {
...
}
//RIGHT
boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed || (...)) {
...
}
```
- [Sonar-1067: Expressions should not be too complex](https://rules.sonarsource.com/java/RSPEC-1067),增大為4
------
**Rule 5.【推薦】簡單邏輯,善用三目運算子,減少if-else語句的編寫**
```java
s != null ? s : "";
```
------
**Rule 6.【推薦】減少使用取反的邏輯**
不使用取反的邏輯,有利於快速理解。且大部分情況,取反邏輯存在對應的正向邏輯寫法。
```java
//WRONG
if (!(x >= 268) { ... }
//RIGHT
if (x < 268) { ... }
```
- [Sonar-1940: Boolean checks should not be inverted](https://rules.sonarsource.com/java/RSPEC-1940)
------
**Rule 7.【推薦】表示式中,能造成短路概率較大的邏輯儘量放前面,使得後面的判斷可以免於執行**
```java
if (maybeTrue() || maybeFalse()) { ... }
if (maybeFalse() && maybeTrue()) { ... }
```
------
**Rule 8.【強制】switch的規則**
1)在一個switch塊內,每個case要麼通過break/return等來終止,要麼註釋說明程式將繼續執行到哪一個case為止;
2)在一個switch塊內,都必須包含一個default語句並且放在最後,即使它什麼程式碼也沒有。
```java
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;
}
```
- [Sonar: "switch" statements should end with "default" clauses](https://rules.sonarsource.com/java/RSPEC-131)
------
**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)有一定成本,呼叫者與被呼叫函式間儘量使用同一型別,減少預設轉換**
```java
//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);
```
- [Sonar-2153: Boxing and unboxing should not be immediately reversed](https://rules.sonarsource.com/java/RSPEC-2153)
**2.2 【推薦】自動拆箱有可能產生NPE,要注意處理**
```java
//如果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方法。
```java
//RIGHT
if (counter1.get() == counter2.get()){...}
```
- [Sonar-2204: ".equals()" should not be used to test the values of "Atomic" classes](https://rules.sonarsource.com/java/RSPEC-2204)
**3.4【強制】 double及float的比較,要特殊處理**
因為精度問題,浮點數間的equals非常不可靠,在vjkit的NumberUtil中有對應的封裝函式。
```java
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) 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的查詢。
- [Sonar-2864:"entrySet()" should be iterated when both the key and value are needed](https://rules.sonarsource.com/java/RSPEC-2864)
------
**Rule 5. 【強制】當物件用於集合時,下列情況需要重新實現hashCode()和 equals()**
1) 以物件做為Map的KEY時;
2) 將物件存入Set時。
上述兩種情況,都需要使用hashCode和equals比較物件,預設的實現會比較是否同一個物件(物件的引用相等)。
另外,物件放入集合後,會影響hashCode(),equals()結果的屬性,將不允許修改。
- [Sonar-2141:Classes that don't define "hashCode()" should not be used in hashes](https://rules.sonarsource.com/java/RSPEC-2141)
------
**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方法,仍然可能會多執行緒間互相覆蓋。
```java
//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. 如果集合要被讀取,定義成``
```java
Class Stack{
public void pushAll(Iterable extends E> src){
for (E e: src)
push(e);
}
}
Stack stack = new Stack();
Iterable integers = ...;
stack.pushAll(integers);
```
1. 如果集合要被寫入,定義成``
```java
Class Stack{
public void popAll(Collection super E> dist){
while(!isEmpty())
dist.add(pop);
}
}
Stack stack = new Stack();
Collection