1. 程式人生 > >唯品會Java開發手冊》1.0.2版閱讀

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

# [《唯品會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 src){ for (E e: src) push(e); } } Stack stack = new Stack(); Iterable integers = ...; stack.pushAll(integers); ``` 1. 如果集合要被寫入,定義成`` ```java Class Stack{ public void popAll(Collection dist){ while(!isEmpty()) dist.add(pop); } } Stack stack = new Stack(); Collection 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的清晰結構與陣列的效能。 ```java public enum COLOR { RED, GREEN, BLUE, ORANGE; } EnumMap moodMap = new EnumMap (COLOR.class); ``` - [Sonar-1640: Maps with keys that are enum values should be replaced with EnumMap](https://rules.sonarsource.com/java/RSPEC-1640) ------ **Rule 12. 【推薦】Array 與 List互轉的正確寫法** ```java // 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)建立單條執行緒時直接指定執行緒名稱 ```java Thread t = new Thread(); t.setName("cleanup-thread"); ``` 2) 執行緒池則使用guava或自行封裝的ThreadFactory,指定命名規則。 ```java //guava 或自行封裝的ThreadFactory ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + "-%d").build(); ThreadPoolExecutor executor = new ThreadPoolExecutor(..., threadFactory, ...); ``` ------ **Rule 2. 【推薦】儘量使用執行緒池來建立執行緒** 除特殊情況,儘量不要自行建立執行緒,更好的保護執行緒資源。 ```java //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。 因此,如下的程式碼無法中斷執行緒: ```java 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();` 將中斷往外傳遞。 ```java //RIGHT public void myMethod() { try { ... } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } ``` - [Sonar-2142: "InterruptedException" should not be ignored](https://rules.sonarsource.com/java/RSPEC-2142) **5.2 主迴圈及進入阻塞狀態前要判斷執行緒狀態** ```java //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。 ```java executor.execute(ThreadPoolUtil.safeRunner(runner)); ``` ------ **Rule 7. 【強制】全域性的非執行緒安全的物件可考慮使用ThreadLocal存放** 全域性變數包括單例物件,static成員變數。 著名的非執行緒安全類包括SimpleDateFormat,MD5/SHA1的Digest。 對這些類,需要每次使用時建立。 但如果建立有一定成本,可以使用ThreadLocal存放並重用。 ThreadLocal變數需要定義成static,並在每次使用前重置。 ```java private static final ThreadLocal SHA1_DIGEST = new ThreadLocal() { @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](https://rules.sonarsource.com/java/RSPEC-2885) - Facebook-Contrib: Correctness - Field is an instance based ThreadLocal variable ------ **Rule 8. 【推薦】縮短鎖** 1) 能鎖區塊,就不要鎖整個方法體; ```java //鎖整個方法,等價於整個方法體內synchronized(this) public synchronized boolean foo(){}; //鎖區塊方法,僅對需要保護的原子操作的連續程式碼塊進行加鎖。 public boolean foo() { synchronized(this) { ... ... } //other stuff } ``` 2)能用物件鎖,就不要用類鎖。 ```java //物件鎖,隻影響使用同一個物件加鎖的執行緒 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*系列: ```java AtomicInteger count = new AtomicInteger(); count.addAndGet(2); ``` 但如果需要原子地同時對多個AtomicXXX的Counter進行操作,則仍然需要使用synchronized將改動程式碼塊加鎖。 ------ **Rule 14. 【推薦】延時初始化的正確寫法** 通過雙重檢查鎖(double-checked locking)實現延遲初始化存在隱患,需要將目標屬性宣告為volatile型,為了更高的效能,還要把volatile屬性賦予給臨時變數,寫法複雜。 所以如果只是想簡單的延遲初始化,可用下面的靜態類的做法,利用JDK本身的class載入機制保證唯一初始化。 ```java private static class LazyObjectHolder { static final LazyObject instance = new LazyObject(); } public void myMethod() { LazyObjectHolder.instance.doSomething(); } ``` - [Sonar-2168: Double-checked locking should not be used](https://rules.sonarsource.com/java/RSPEC-2168) ------ # (十) 異常處理 **Rule 1. 【強制】建立異常的消耗大,只用在真正異常的場景** 構造異常時,需要獲得整個呼叫棧,有一定消耗。 不要用來做流程控制,條件控制,因為異常的處理效率比條件判斷低。 發生概率較高的條件,應該先進行檢查規避,比如:IndexOutOfBoundsException,NullPointerException等,所以如果程式碼裡捕獲這些異常通常是個壞味道。 ```java //WRONG try { return obj.method(); } catch (NullPointerException e) { return false; } //RIGHT if (obj == null) { return false; } ``` - [Sonar-1696: "NullPointerException" should not be caught](https://rules.sonarsource.com/java/RSPEC-1696) ------ **Rule 2. 【推薦】在特定場景,避免每次構造異常** 如上,異常的建構函式需要獲得整個呼叫棧。 如果異常頻繁發生,且不需要列印完整的呼叫棧時,可以考慮繞過異常的建構函式。 1) 如果異常的message不變,將異常定義為靜態成員變數; 下例定義靜態異常,並簡單定義一層的StackTrace。`ExceptionUtil`見vjkit。 ```java private static RuntimeException TIMEOUT_EXCEPTION = ExceptionUtil.setStackTrace(new RuntimeException("Timeout"), MyClass.class, "mymethod"); ... throw TIMEOUT_EXCEPTION; ``` 2) 如果異常的message會變化,則對靜態的異常例項進行clone()再修改message。 Exception預設不是Cloneable的,`CloneableException`見vjkit。 ```java 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. 【推薦】異常日誌應包含排查問題的足夠資訊** 異常資訊應包含排查問題時足夠的上下文資訊。 捕獲異常並記錄異常日誌的地方,同樣需要記錄沒有包含在異常資訊中,而排查問題需要的資訊,比如捕獲處的上下文資訊。 ```java //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`是允許的** 是否定義獨立的異常類,關鍵是呼叫者會如何處理這個異常,如果沒有需要特別的處理,直接丟擲RuntimeExceptio