熟读《阿里巴巴java开发手册》(一、编程规约)
目錄
(一) 命名風格
(二) 常量定義
(三) 代碼格式
(四) OOP 規約
(五) 集合處理
(六) 并發處理
(七) 控制語句
(八) 注釋規約
(九) 其它
(一) 命名風格
1. 【強制】 代碼中的命名均不能以下劃線或美元符號開始,也不能以下劃線或美元符號結束。
反例: _name / __name / $name / name_ / name$ / name__
2. 【強制】 代碼中的命名嚴禁使用拼音與英文混合的方式,更不允許直接使用中文的方式。
說明: 正確的英文拼寫和語法可以讓閱讀者易于理解,避免歧義。注意,純拼音命名方式更要避免采用。
正例: renminbi / alibaba / taobao / youku / hangzhou 等國際通用的名稱, 可視同英文。
反例: DaZhePromotion [打折] / getPingfenByName() [評分] / int 某變量 = 3
?
3. 【強制】 類名使用 UpperCamelCase 風格,但以下情形例外: DO / BO / DTO / VO / AO/ PO / UID 等。
正例: JavaServerlessPlatform / UserDO / XmlService / TcpUdpDeal / TaPromotion
反例: javaserverlessplatform / UserDo / XMLService / TCPUDPDeal / TAPromotion
?
4. 【強制】 方法名、參數名、成員變量、局部變量都統一使用 lowerCamelCase 風格,必須遵從駝峰形式。
正例: localValue / getHttpMessage() / inputUserId
?
5. 【強制】 常量命名全部大寫,單詞間用下劃線隔開,力求語義表達完整清楚,不要嫌名字長。
正例: MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例: MAX_COUNT / EXPIRED_TIME
?
6. 【強制】 抽象類命名使用 Abstract 或 Base 開頭;異常類命名使用 Exception 結尾; 測試類命名以它要測試的類的名稱開始,以 Test 結尾。
?
7. 【強制】 類型與中括號緊挨相連來表示數組。
正例: 定義整形數組 int[] arrayDemo;
反例: 在 main 參數中,使用 String args[]來定義。
?
8. 【強制】 POJO 類中布爾類型變量都不要加 is 前綴,否則部分框架解析會引起序列化錯誤。
說明: 在本文 MySQL 規約中的建表約定第一條,表達是與否的值采用 is_xxx 的命名方式,所以,需要在<resultMap>設置從 is_xxx 到 xxx 的映射關系。
反例: 定義為基本數據類型 Boolean isDeleted 的屬性,它的方法也是 isDeleted(), RPC 框架在反向解析的時候, “誤以為” 對應的屬性名稱是 deleted,導致屬性獲取不到,進而拋出異常。
?
9. 【強制】 包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用單數形式,但是類名如果有復數含義,類名可以使用復數形式。
正例: 應用工具類包名為 com.alibaba.ai.util、類名為 MessageUtils(此規則參考 spring 的框架結構)
?
10.【強制】 避免在子父類的成員變量之間、或者不同代碼塊的局部變量之間采用完全相同的命名,使可讀性降低。
說明: 子類、父類成員變量名相同,即使是 public 類型的變量也是能夠通過編譯,而局部變量在同一方法內的不同代碼塊中同名也是合法的,但是要避免使用。對于非 setter/getter 的參數名稱也要避免與成員變量名稱相同。
反例:
11.【強制】 杜絕完全不規范的縮寫, 避免望文不知義。
反例: AbstractClass“縮寫” 命名成 AbsClass; condition“縮寫” 命名成 condi,此類隨意縮寫嚴重降低了代碼的可閱讀性。
?
12.【推薦】 為了達到代碼自解釋的目標,任何自定義編程元素在命名時,使用盡量完整的單詞組合來表達其意。
正例: 在 JDK 中, 表達原子更新的類名為: AtomicReferenceFieldUpdater。
反例: int a 的隨意命名方式。
?
13.【推薦】 在常量與變量的命名時,表示類型的名詞放在詞尾,以提升辨識度。
正例: startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
反例: startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD
?
14.【推薦】 如果模塊、 接口、類、方法使用了設計模式,在命名時需體現出具體模式。
說明: 將設計模式體現在名字中,有利于閱讀者快速理解架構設計理念。
正例: public class OrderFactory;
? ? ? ? ? ? public class LoginProxy;
? ? ? ? ? ? public class ResourceObserver;
?
15.【推薦】 接口類中的方法和屬性不要加任何修飾符號( public 也不要加),保持代碼的簡潔性,并加上有效的 Javadoc 注釋。盡量不要在接口里定義變量,如果一定要定義變量,肯定是與接口方法相關,并且是整個應用的基礎常量。
正例: 接口方法簽名 void commit();
接口基礎常量 String COMPANY = "alibaba";
反例: 接口方法定義 public abstract void f();
說明: JDK8 中接口允許有默認實現,那么這個 default 方法,是對所有實現類都有價值的默認實現。
?
16.接口和實現類的命名有兩套規則:
1) 【強制】 對于 Service 和 DAO 類,基于 SOA 的理念,暴露出來的服務一定是接口,內部的實現類用Impl 的后綴與接口區別。
正例: CacheServiceImpl 實現 CacheService 接口。
2) 【推薦】 如果是形容能力的接口名稱,取對應的形容詞為接口名(通常是–able 的形容詞)。
正例: AbstractTranslator 實現 Translatable 接口。
?
17.【參考】 枚舉類名帶上 Enum 后綴,枚舉成員名稱需要全大寫,單詞間用下劃線隔開。
說明: 枚舉其實就是特殊的類, 域成員均為常量, 且構造方法被默認強制是私有。
正例: 枚舉名字為 ProcessStatusEnum 的成員名稱: SUCCESS / UNKNOWN_REASON。
?
18.【參考】 各層命名規約:
A) Service/DAO 層方法命名規約
1) 獲取單個對象的方法用 get 做前綴。
2) 獲取多個對象的方法用 list 做前綴,復數形式結尾如: listObjects。
3) 獲取統計值的方法用 count 做前綴。
4) 插入的方法用 save/insert 做前綴。
5) 刪除的方法用 remove/delete 做前綴。
6) 修改的方法用 update 做前綴。
B) 領域模型命名規約
1) 數據對象: xxxDO, xxx 即為數據表名。
2) 數據傳輸對象: xxxDTO, xxx 為業務領域相關的名稱。
3) 展示對象: xxxVO, xxx 一般為網頁名稱。
4) POJO 是 DO/DTO/BO/VO 的統稱,禁止命名成 xxxPOJO。
?
(二) 常量定義
1. 【強制】 不允許任何魔法值( 即未經預先定義的常量) 直接出現在代碼中。
反例: String key = "Id#taobao_" + tradeId;
? ? ? ? ? ? cache.put(key, value);
? ? ? ? ? ? // 緩存 get 時,由于在代碼復制時,漏掉下劃線,導致緩存擊穿而出現問題
?
2. 【強制】 在 long 或者 Long 賦值時, 數值后使用大寫的 L,不能是小寫的 l,小寫容易跟數字 1 混淆,造成誤解。
說明: Long a = 2l; 寫的是數字的 21,還是 Long 型的 2。
?
3. 【推薦】 不要使用一個常量類維護所有常量, 要按常量功能進行歸類,分開維護。
說明: 大而全的常量類, 雜亂無章, 使用查找功能才能定位到修改的常量,不利于理解和維護。
正例: 緩存相關常量放在類 CacheConsts 下;系統配置相關常量放在類 ConfigConsts 下。
?
4. 【推薦】 常量的復用層次有五層:跨應用共享常量、應用內共享常量、子工程內共享常量、包內共享常量、類內共享常量。
1) 跨應用共享常量:放置在二方庫中,通常是 client.jar 中的 constant 目錄下。
2) 應用內共享常量:放置在一方庫中, 通常是子模塊中的 constant 目錄下。
反例: 易懂變量也要統一定義成應用內共享常量, 兩位工程師在兩個類中分別定義了“YES” 的變量:
類 A 中: public static final String YES = "yes";
類 B 中: public static final String YES = "y";
A.YES.equals(B.YES),預期是 true,但實際返回為 false,導致線上問題。
3) 子工程內部共享常量:即在當前子工程的 constant 目錄下。
4) 包內共享常量:即在當前包下單獨的 constant 目錄下。
5) 類內共享常量:直接在類內部 private static final 定義。
?
5. 【推薦】 如果變量值僅在一個固定范圍內變化用 enum 類型來定義。
說明: 如果存在名稱之外的延伸屬性應使用 enum 類型,下面正例中的數字就是延伸信息,表示一年中的第幾個季節。
正例:
(三) 代碼格式
1. 【強制】 如果是大括號內為空,則簡潔地寫成{}即可,大括號中間無需換行和空格;如果是非空代碼塊則:
1) 左大括號前不換行。
2) 左大括號后換行。
3) 右大括號前換行。
4) 右大括號后還有 else 等代碼則不換行;表示終止的右大括號后必須換行。
?
2. 【強制】 左小括號和字符之間不出現空格; 同樣,右小括號和字符之間也不出現空格;而左大括號前需要空格。詳見第 5 條下方正例提示。
反例:
?
3. 【強制】 if/for/while/switch/do 等保留字與括號之間都必須加空格。
?
4. 【強制】 任何二目、 三目運算符的左右兩邊都需要加一個空格。
說明: 運算符包括賦值運算符=、邏輯運算符&&、加減乘除符號等。
?
5. 【強制】 采用 4 個空格縮進,禁止使用 tab 字符。
說明: 如果使用 tab 縮進,必須設置 1 個 tab 為 4 個空格。 IDEA 設置 tab 為 4 個空格時,請勿勾選 Use tab character;而在 eclipse 中,必須勾選 insert spaces for tabs。
正例: (涉及 1-5 點)
6. 【強制】 注釋的雙斜線與注釋內容之間有且僅有一個空格。
正例:
?
?
7. 【強制】 在進行類型強制轉換時,右括號與強制轉換值之間不需要任何空格隔開。
正例:
?
8. 【強制】 單行字符數限制不超過 120 個,超出需要換行,換行時遵循如下原則:
1) 第二行相對第一行縮進 4 個空格,從第三行開始,不再繼續縮進,參考示例。
2) 運算符與下文一起換行。
3) 方法調用的點符號與下文一起換行。
4) 方法調用中的多個參數需要換行時, 在逗號后進行。
5)在括號前不要換行,見反例。
正例:
反例:
StringBuilder sb = new StringBuilder(); // 超過 120 個字符的情況下,不要在括號前換行 sb.append("Jack").append("Ma")...append("alibaba"); // 參數很多的方法調用可能超過 120 個字符, 不要在逗號前換行 method(args1, args2, args3, ..., argsX);9. 【強制】 方法參數在定義和傳入時,多個參數逗號后邊必須加空格。
正例: 下例中實參的 args1, 后邊必須要有一個空格。
10.【強制】 IDE 的 text file encoding 設置為 UTF-8; IDE 中文件的換行符使用 Unix 格式,不要使用 Windows 格式。
?
11.【推薦】 單個方法的總行數不超過 80 行。
說明: 除注釋之外的方法簽名、 左右大括號、方法內代碼、空行、回車及任何不可見字符的總行數不超過80 行。
正例: 代碼邏輯分清紅花和綠葉,個性和共性,綠葉邏輯單獨出來成為額外方法,使主干代碼更加清晰;共性邏輯抽取成為共性方法,便于復用和維護。
?
12.【推薦】 沒有必要增加若干空格來使變量的賦值等號與上一行對應位置的等號對齊。
正例:
說明: 增加 sb 這個變量,如果需要對齊,則給 one、 two、 three 都要增加幾個空格,在變量比較多的情況下,是非常累贅的事情。
13.【推薦】 不同邏輯、不同語義、不同業務的代碼之間插入一個空行分隔開來以提升可讀性。
說明: 任何情形, 沒有必要插入多個空行進行隔開。
?
(四) OOP 規約
1. 【強制】 避免通過一個類的對象引用訪問此類的靜態變量或靜態方法,無謂增加編譯器解析成本,直接用類名來訪問即可。
2. 【強制】 所有的覆寫方法,必須加@Override 注解。
說明: getObject()與 get0bject()的問題。一個是字母的 O,一個是數字的 0,加@Override 可以準確判斷是否覆蓋成功。另外,如果在抽象類中對方法簽名進行修改,其實現類會馬上編譯報錯。
3. 【強制】 相同參數類型,相同業務含義,才可以使用 Java 的可變參數,避免使用 Object。
說明: 可變參數必須放置在參數列表的最后。(提倡同學們盡量不用可變參數編程)
正例: public List<User> listUsers(String type, Long... ids) {...}
4. 【強制】 外部正在調用或者二方庫依賴的接口,不允許修改方法簽名,避免對接口調用方產生影響。接口過時必須加@Deprecated 注解,并清晰地說明采用的新接口或者新服務是什么。
5. 【強制】 不能使用過時的類或方法。
說明: java.net.URLDecoder 中的方法 decode(String encodeStr) 這個方法已經過時,應該使用雙參數decode(String source, String encode)。接口提供方既然明確是過時接口,那么有義務同時提供新的接口;作為調用方來說,有義務去考證過時方法的新實現是什么。
6. 【強制】 Object 的 equals 方法容易拋空指針異常,應使用常量或確定有值的對象來調用equals。
正例: "test".equals(object);
反例: object.equals("test");
說明: 推薦使用 java.util.Objects#equals( JDK7 引入的工具類) 。
7. 【強制】 所有整型包裝類對象之間值的比較, 全部使用 equals 方法比較。
說明: 對于 Integer var = ? 在-128 至 127 范圍內的賦值, Integer 對象是在 IntegerCache.cache 產生,會復用已有對象,這個區間內的 Integer 值可以直接使用==進行判斷,但是這個區間之外的所有數據,都會在堆上產生,并不會復用已有對象,這是一個大坑,推薦使用 equals 方法進行判斷。
8. 【強制】 浮點數之間的等值判斷,基本數據類型不能用==來比較,包裝數據類型不能用equals 來判斷。
說明: 浮點數采用“尾數+階碼” 的編碼方式,類似于科學計數法的“有效數字+指數” 的表示方式。二進制無法精確表示大部分的十進制小數,具體原理參考《碼出高效》 。
反例:
正例:
(1) 指定一個誤差范圍,兩個浮點數的差值在此范圍之內,則認為是相等的。
float a = 1.0f - 0.9f; float b = 0.9f - 0.8f; float diff = 1e-6f; if (Math.abs(a - b) < diff) {System.out.println("true"); }(2) 使用 BigDecimal 來定義值,再進行浮點數的運算操作。
BigDecimal a = new BigDecimal("1.0"); BigDecimal b = new BigDecimal("0.9"); BigDecimal c = new BigDecimal("0.8"); BigDecimal x = a.subtract(b); BigDecimal y = b.subtract(c); if (x.equals(y)) {System.out.println("true"); }9. 【強制】 定義數據對象 DO 類時,屬性類型要與數據庫字段類型相匹配。
正例: 數據庫字段的 bigint 必須與類屬性的 Long 類型相對應。
反例: 某個案例的數據庫表 id 字段定義類型 bigint unsigned,實際類對象屬性為 Integer,隨著 id 越來越大,超過 Integer 的表示范圍而溢出成為負數。
10.【強制】 為了防止精度損失, 禁止使用構造方法 BigDecimal(double)的方式把 double 值轉化為 BigDecimal 對象。
說明: BigDecimal(double)存在精度損失風險,在精確計算或值比較的場景中可能會導致業務邏輯異常。
如: BigDecimal g = new BigDecimal(0.1f); 實際的存儲值為: 0.10000000149
正例: 優先推薦入參為 String 的構造方法,或使用 BigDecimal 的 valueOf 方法,此方法內部其實執行了Double 的 toString,而 Double 的 toString 按 double 的實際能表達的精度對尾數進行了截斷。
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
?
11.關于基本數據類型與包裝數據類型的使用標準如下:
1) 【強制】 所有的 POJO 類屬性必須使用包裝數據類型。
2) 【強制】 RPC 方法的返回值和參數必須使用包裝數據類型。
3) 【推薦】 所有的局部變量使用基本數據類型。
說明: POJO 類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何 NPE 問題,或者入庫檢查,都由使用者來保證。
正例: 數據庫的查詢結果可能是 null,因為自動拆箱,用基本數據類型接收有 NPE 風險。
反例: 比如顯示成交總額漲跌情況,即正負 x%, x 為基本數據類型,調用的 RPC 服務,調用不成功時,返回的是默認值,頁面顯示為 0%,這是不合理的,應該顯示成中劃線。所以包裝數據類型的 null 值,能夠表示額外的信息,如:遠程調用失敗,異常退出。
12.【強制】 定義 DO/DTO/VO 等 POJO 類時,不要設定任何屬性默認值。
反例: POJO 類的 createTime 默認值為 new Date(), 但是這個屬性在數據提取時并沒有置入具體值,在更新其它字段時又附帶更新了此字段,導致創建時間被修改成當前時間。
13.【強制】 序列化類新增屬性時,請不要修改 serialVersionUID 字段,避免反序列失敗;如果完全不兼容升級,避免反序列化混亂,那么請修改 serialVersionUID 值。
說明: 注意 serialVersionUID 不一致會拋出序列化運行時異常。
14.【強制】 構造方法里面禁止加入任何業務邏輯,如果有初始化邏輯,請放在 init 方法中。
15.【強制】 POJO 類必須寫 toString 方法。使用 IDE 中的工具: source> generate toString時,如果繼承了另一個 POJO 類,注意在前面加一下 super.toString。
說明: 在方法執行拋出異常時,可以直接調用 POJO 的 toString()方法打印其屬性值,便于排查問題。
16.【強制】 禁止在 POJO 類中,同時存在對應屬性 xxx 的 isXxx()和 getXxx()方法。
說明: 框架在調用屬性 xxx 的提取方法時,并不能確定哪個方法一定是被優先調用到。
17.【推薦】 使用索引訪問用 String 的 split 方法得到的數組時,需做最后一個分隔符后有無內容的檢查,否則會有拋 IndexOutOfBoundsException 的風險。
說明:
String str = "a,b,c,,";
String[] ary = str.split(",");
// 預期大于 3,結果是 3
System.out.println(ary.length);
18.【推薦】 當一個類有多個構造方法,或者多個同名方法,這些方法應該按順序放置在一起,便于閱讀,此條規則優先于下一條。
19.【推薦】 類內方法定義的順序依次是:公有方法或保護方法 > 私有方法 > getter / setter方法。
說明: 公有方法是類的調用者和維護者最關心的方法,首屏展示最好;保護方法雖然只是子類關心,也可能是“模板設計模式” 下的核心方法;而私有方法外部一般不需要特別關心,是一個黑盒實現; 因為承載的信息價值較低,所有 Service 和 DAO 的 getter/setter 方法放在類體最后。
?
20.【推薦】 setter 方法中,參數名稱與類成員變量名稱一致, this.成員名 = 參數名。在getter/setter 方法中, 不要增加業務邏輯,增加排查問題的難度。
反例:
21.【推薦】 循環體內,字符串的連接方式,使用 StringBuilder 的 append 方法進行擴展。
說明: 下例中, 反編譯出的字節碼文件顯示每次循環都會 new 出一個 StringBuilder 對象,然后進行append 操作,最后通過 toString 方法返回 String 對象,造成內存資源浪費。
反例:
22.【推薦】 final 可以聲明類、成員變量、方法、以及本地變量,下列情況使用 final 關鍵字:
1) 不允許被繼承的類,如: String 類。
2) 不允許修改引用的域對象。
3) 不允許被覆寫的方法,如: POJO 類的 setter 方法。
4) 不允許運行過程中重新賦值的局部變量。
5) 避免上下文重復使用一個變量,使用 final 可以強制重新定義一個變量,方便更好地進行重構。
?
23.【推薦】 慎用 Object 的 clone 方法來拷貝對象。
說明: 對象 clone 方法默認是淺拷貝,若想實現深拷貝需覆寫 clone 方法實現域對象的深度遍歷式拷貝。
24.【推薦】 類成員與方法訪問控制從嚴:
1) 如果不允許外部直接通過 new 來創建對象,那么構造方法必須是 private。
2) 工具類不允許有 public 或 default 構造方法。
3) 類非 static 成員變量并且與子類共享,必須是 protected。
4) 類非 static 成員變量并且僅在本類使用,必須是 private。
5) 類 static 成員變量如果僅在本類使用,必須是 private。
6) 若是 static 成員變量, 考慮是否為 final。
7) 類成員方法只供類內部調用,必須是 private。
8) 類成員方法只對繼承類公開,那么限制為 protected。
說明: 任何類、方法、參數、變量,嚴控訪問范圍。過于寬泛的訪問范圍,不利于模塊解耦。思考:如果是一個 private 的方法,想刪除就刪除,可是一個 public 的 service 成員方法或成員變量,刪除一下,不得手心冒點汗嗎?變量像自己的小孩,盡量在自己的視線內,變量作用域太大, 無限制的到處跑,那么你會擔心的。
?
(五) 集合處理
1. 【強制】 關于 hashCode 和 equals 的處理,遵循如下規則:
1) 只要覆寫 equals,就必須覆寫 hashCode。
2) 因為 Set 存儲的是不重復的對象,依據 hashCode 和 equals 進行判斷,所以 Set 存儲的對象必須覆寫這兩個方法。
3) 如果自定義對象作為 Map 的鍵,那么必須覆寫 hashCode 和 equals。
說明: String 已覆寫 hashCode 和 equals 方法,所以我們可以愉快地使用 String 對象作為 key 來使用。
2. 【強制】 ArrayList 的 subList 結果不可強轉成 ArrayList,否則會拋出 ClassCastException 異常, 即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。
說明: subList 返回的是 ArrayList 的內部類 SubList, 并不是 ArrayList 而是 ArrayList 的一個視圖,對于 SubList 子列表的所有操作最終會反映到原列表上。
3. 【強制】 使用 Map 的方法 keySet()/values()/entrySet()返回集合對象時,不可以對其進行添加元素操作,否則會拋出 UnsupportedOperationException 異常。
4. 【強制】 Collections 類返回的對象,如: emptyList()/singletonList()等都是 immutablelist,不可對其進行添加或者刪除元素的操作。
反例: 如果查詢無結果,返回 Collections.emptyList()空集合對象,調用方一旦進行了添加元素的操作,就會觸發 UnsupportedOperationException 異常。
5. 【強制】 在 subList 場景中, 高度注意對原集合元素的增加或刪除, 均會導致子列表的遍歷、增加、刪除產生 ConcurrentModificationException 異常。
6. 【強制】 使用集合轉數組的方法,必須使用集合的 toArray(T[] array),傳入的是類型完全一致、長度為 0 的空數組。
反例: 直接使用 toArray 無參方法存在問題,此方法返回值只能是 Object[]類,若強轉其它類型數組將出現 ClassCastException 錯誤。
正例:
List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
String[] array = list.toArray(new String[0]);
說明: 使用 toArray 帶參方法,數組空間大小的 length:
1) 等于 0,動態創建與 size 相同的數組,性能最好。
2) 大于 0 但小于 size,重新創建大小等于 size 的數組,增加 GC 負擔。
3) 等于 size,在高并發情況下,數組創建完成之后, size 正在變大的情況下,負面影響與上相同。
4) 大于 size,空間浪費,且在 size 處插入 null 值,存在 NPE 隱患。
7. 【強制】 在使用 Collection 接口任何實現類的 addAll()方法時,都要對輸入的集合參數進行NPE 判斷。
說明: 在 ArrayList#addAll 方法的第一行代碼即 Object[] a = c.toArray(); 其中 c 為輸入集合參數,如果為 null,則直接拋出異常。
8. 【強制】 使用工具類 Arrays.asList()把數組轉換成集合時,不能使用其修改集合相關的方法,它的 add/remove/clear 方法會拋出 UnsupportedOperationException 異常。
說明: asList 的返回對象是一個 Arrays 內部類,并沒有實現集合的修改方法。 Arrays.asList 體現的是適配器模式,只是轉換接口,后臺的數據仍是數組。
String[] str = new String[] { "yang", "hao" };
List list = Arrays.asList(str);
第一種情況: list.add("yangguanbao"); 運行時異常。
第二種情況: str[0] = "changed"; 也會隨之修改,反之亦然。
9. 【強制】 泛型通配符<? extends T>來接收返回的數據,此寫法的泛型集合不能使用 add 方法, 而<? super T>不能使用 get 方法,作為接口調用賦值時易出錯。
說明: 擴展說一下 PECS(Producer Extends Consumer Super)原則: 第一、 頻繁往外讀取內容的,適合用<? extends T>。 第二、 經常往里插入的,適合用<? super T>
10.【強制】 在無泛型限制定義的集合賦值給泛型限制的集合時,在使用集合元素時,需要進行instanceof 判斷,避免拋出 ClassCastException 異常。
說明: 畢竟泛型是在 JDK5 后才出現,考慮到向前兼容,編譯器是允許非泛型集合與泛型集合互相賦值。
反例:
List<String> generics = null;
List notGenerics = new ArrayList(10);
notGenerics.add(new Object());
notGenerics.add(new Integer(1));
generics = notGenerics;
// 此處拋出 ClassCastException 異常
String string = generics.get(0);
11.【強制】 不要在 foreach 循環里進行元素的 remove/add 操作。 remove 元素請使用Iterator 方式,如果并發操作,需要對 Iterator 對象加鎖。
正例:
反例:
for (String item : list) {if ("1".equals(item)) {list.remove(item);} }說明: 以上代碼的執行結果肯定會出乎大家的意料,那么試一下把“1” 換成“2” ,會是同樣的結果嗎?
?
12.【強制】 在 JDK7 版本及以上, Comparator 實現類要滿足如下三個條件,不然 Arrays.sort,Collections.sort 會拋 IllegalArgumentException 異常。
說明: 三個條件如下
1) x, y 的比較結果和 y, x 的比較結果相反。
2) x>y, y>z, 則 x>z。
3) x=y, 則 x, z 比較結果和 y, z 比較結果相同。
反例: 下例中沒有處理相等的情況,交換兩個對象判斷結果并不互反,不符合第一個條件,在實際使用中可能會出現異常。
13.【推薦】 集合泛型定義時, 在 JDK7 及以上,使用 diamond 語法或全省略。
說明: 菱形泛型,即 diamond, 直接使用<>來指代前邊已經指定的類型。
正例:
// diamond 方式,即<>
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);
?
14.【推薦】 集合初始化時, 指定集合初始值大小。
說明: HashMap 使用 HashMap(int initialCapacity) 初始化。
正例: initialCapacity = (需要存儲的元素個數 / 負載因子) + 1。 注意負載因子(即 loader factor) 默認為 0.75,如果暫時無法確定初始值大小,請設置為 16(即默認值) 。
反例: HashMap 需要放置 1024 個元素,由于沒有設置容量初始大小,隨著元素不斷增加,容量 7 次被迫擴大, resize 需要重建 hash 表,嚴重影響性能。
15.【推薦】 使用 entrySet 遍歷 Map 類集合 KV,而不是 keySet 方式進行遍歷。
說明: keySet 其實是遍歷了 2 次,一次是轉為 Iterator 對象,另一次是從 hashMap 中取出 key 所對應的 value。而 entrySet 只是遍歷了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用 Map.forEach 方法。
正例: values()返回的是 V 值集合,是一個 list 集合對象; keySet()返回的是 K 值集合,是一個 Set 集合
對象; entrySet()返回的是 K-V 值組合集合。
?
16.【推薦】 高度注意 Map 類集合 K/V 能不能存儲 null 值的情況,如下表格:
反例: 由于 HashMap 的干擾,很多人認為 ConcurrentHashMap 是可以置入 null 值,而事實上, 存儲null 值時會拋出 NPE 異常。
?
17.【參考】 合理利用好集合的有序性(sort)和穩定性(order),避免集合的無序性(unsort)和不穩定性(unorder)帶來的負面影響。
說明: 有序性是指遍歷的結果是按某種比較規則依次排列的。 穩定性指集合每次遍歷的元素次序是一定的。 如: ArrayList 是 order/unsort; HashMap 是 unorder/unsort; TreeSet 是 order/sort。
18.【參考】 利用 Set 元素唯一的特性,可以快速對一個集合進行去重操作,避免使用 List 的contains 方法進行遍歷、對比、 去重操作。
?
(六) 并發處理
1. 【強制】 獲取單例對象需要保證線程安全,其中的方法也要保證線程安全。
說明: 資源驅動類、工具類、單例工廠類都需要注意。
2. 【強制】 創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。
正例: 自定義線程工廠,并且根據外部特征進行分組,比如機房信息。
3. 【強制】 線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。
說明: 線程池的好處是減少在創建和銷毀線程上所消耗的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換” 的問題。
4. 【強制】 線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。
說明: Executors 返回的線程池對象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2) CachedThreadPool:
允許的創建線程數量為 Integer.MAX_VALUE, 可能會創建大量的線程,從而導致 OOM。
5. 【強制】 SimpleDateFormat 是線程不安全的類,一般不要定義為 static 變量,如果定義為
static,必須加鎖,或者使用 DateUtils 工具類。
正例: 注意線程安全,使用 DateUtils。亦推薦如下處理:
說明: 如果是 JDK8 的應用,可以使用 Instant 代替 Date, LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方給出的解釋: simple beautiful strong immutable thread-safe。
6. 【強制】 必須回收自定義的 ThreadLocal 變量,尤其在線程池場景下,線程經常會被復用,如果不清理自定義的 ThreadLocal 變量,可能會影響后續業務邏輯和造成內存泄露等問題。盡量在代理中使用 try-finally 塊進行回收。
正例:
7. 【強制】 高并發時,同步調用應該去考量鎖的性能損耗。能用無鎖數據結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體; 能用對象鎖,就不要用類鎖。
說明: 盡可能使加鎖的代碼塊工作量盡可能的小,避免在鎖代碼塊中調用 RPC 方法。
8. 【強制】 對多個資源、數據庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。
說明: 線程一需要對表 A、 B、 C 依次全部加鎖后才可以進行更新操作,那么線程二的加鎖順序也必須是A、 B、 C,否則可能出現死鎖。
9. 【強制】 在使用阻塞等待獲取鎖的方式中,必須在 try 代碼塊之外,并且在加鎖方法與 try 代碼塊之間沒有任何可能拋出異常的方法調用,避免加鎖成功后,在 finally 中無法解鎖。
說明一: 如果在 lock 方法與 try 代碼塊之間的方法調用拋出異常,那么無法解鎖,造成其它線程無法成功獲取鎖。
說明二: 如果 lock 方法在 try 代碼塊之內,可能由于其它方法拋出異常,導致在 finally 代碼塊中,unlock 對未加鎖的對象解鎖,它會調用 AQS 的 tryRelease 方法(取決于具體實現類),拋出IllegalMonitorStateException 異常。
說明三: 在 Lock 對象的 lock 方法實現中可能拋出 unchecked 異常,產生的后果與說明二相同。
正例:
反例:
?
10. 【強制】 在使用嘗試機制來獲取鎖的方式中,進入業務代碼塊之前,必須先判斷當前線程是否持有鎖。鎖的釋放規則與鎖的阻塞等待方式相同。
說明: Lock 對象的 unlock 方法在執行時,它會調用 AQS 的 tryRelease 方法(取決于具體實現類),如果當前線程不持有鎖,則拋出 IllegalMonitorStateException 異常。
正例:
11.【強制】 并發修改同一記錄時,避免更新丟失, 需要加鎖。 要么在應用層加鎖,要么在緩存加鎖,要么在數據庫層使用樂觀鎖,使用 version 作為更新依據。
說明: 如果每次訪問沖突概率小于 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小于3 次。
12.【強制】 多線程并行處理定時任務時, Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行, 如果在處理定時任務時使用ScheduledExecutorService 則沒有這個問題。
13.【推薦】 資金相關的金融敏感信息,使用悲觀鎖策略。
說明: 樂觀鎖在獲得鎖的同時已經完成了更新操作,校驗邏輯容易出現漏洞,另外,樂觀鎖對沖突的解決策略有較復雜的要求,處理不當容易造成系統壓力或數據異常,所以資金相關的金融敏感信息不建議使用樂觀鎖更新。
14.【推薦】 使用 CountDownLatch 進行異步轉同步操作,每個線程退出前必須調用 countDown方法,線程執行代碼注意 catch 異常,確保 countDown 方法被執行到,避免主線程無法執行至 await 方法,直到超時才返回結果。
說明: 注意,子線程拋出異常堆棧,不能在主線程 try-catch 到。
15.【推薦】 避免 Random 實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一seed 導致的性能下降。
說明: Random 實例包括 java.util.Random 的實例或者 Math.random()的方式。
正例: 在 JDK7 之后,可以直接使用 API ThreadLocalRandom, 而在 JDK7 之前, 需要編碼保證每個線程持有一個實例。
16.【推薦】 在并發場景下, 通過雙重檢查鎖( double-checked locking) 實現延遲初始化的優化問題隱患(可參考 The "Double-Checked Locking is Broken" Declaration), 推薦解決方案中較為簡單一種( 適用于 JDK5 及以上版本) ,將目標屬性聲明為 volatile 型。
反例:
17.【參考】 volatile 解決多線程內存不可見問題。對于一寫多讀,是可以解決變量同步問題,但是如果多寫,同樣無法解決線程安全問題。
說明: 如果是 count++操作,使用如下類實現: AtomicInteger count = new AtomicInteger();
count.addAndGet(1); 如果是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。
18.【參考】 HashMap 在容量不夠進行 resize 時由于高并發可能出現死鏈,導致 CPU 飆升,在開發過程中可以使用其它數據結構或加鎖來規避此風險。
19.【參考】 ThreadLocal 對象使用 static 修飾, ThreadLocal 無法解決共享對象的更新問題。
說明: 這個變量是針對一個線程內所有操作共享的,所以設置為靜態變量,所有此類實例共享此靜態變量,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只要是這個線程內定義的)都可以操控這個變量。
(七) 控制語句
1. 【強制】 在一個 switch 塊內,每個 case 要么通過 continue/break/return 等來終止,要么注釋說明程序將繼續執行到哪一個 case 為止;在一個 switch 塊內,都必須包含一個default 語句并且放在最后,即使它什么代碼也沒有。
說明: 注意 break 是退出 switch 語句塊,而 return 是退出方法體。
2. 【強制】 當 switch 括號內的變量類型為 String 并且此變量為外部參數時,必須先進行 null判斷。
反例: 猜猜下面的代碼輸出是什么?
3. 【強制】 在 if/else/for/while/do 語句中必須使用大括號。
說明: 即使只有一行代碼,避免采用單行的編碼方式: if (condition) statements;
4. 【強制】 在高并發場景中,避免使用” 等于” 判斷作為中斷或退出的條件。
說明: 如果并發控制沒有處理好,容易產生等值判斷被“擊穿” 的情況,使用大于或小于的區間判斷條件來代替。
反例: 判斷剩余獎品數量等于 0 時,終止發放獎品,但因為并發處理錯誤導致獎品數量瞬間變成了負數,這樣的話,活動無法終止。
5. 【推薦】 表達異常的分支時, 少用 if-else 方式, 這種方式可以改寫成:
if (condition) {
...
return obj;
}
// 接著寫 else 的業務邏輯代碼;
說明: 如果非使用 if()...else if()...else...方式表達邏輯, 避免后續代碼維護困難, 【強制】 請勿超過 3 層。
正例: 超過 3 層的 if-else 的邏輯判斷代碼可以使用衛語句、策略模式、狀態模式等來實現,其中衛語句即代碼邏輯先考慮失敗、異常、中斷、退出等直接返回的情況,以方法多個出口的方式,解決代碼中判斷分支嵌套的問題,這是逆向思維的體現。
示例如下:
6. 【推薦】 除常用方法(如 getXxx/isXxx)等外,不要在條件判斷中執行其它復雜的語句,將復雜邏輯判斷的結果賦值給一個有意義的布爾變量名,以提高可讀性。
說明: 很多 if 語句內的邏輯表達式相當復雜,與、或、取反混合運算,甚至各種方法縱深調用,理解成本非常高。如果賦值一個非常好理解的布爾變量名字,則是件令人爽心悅目的事情。
正例:
反例:
public final void acquire(long arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {selfInterrupt();} }7. 【推薦】 不要在其它表達式(尤其是條件表達式)中,插入賦值語句。
說明: 賦值點類似于人體的穴位,對于代碼的理解至關重要,所以賦值語句需要清晰地單獨成為一行。
反例:
8. 【推薦】 循環體中的語句要考量性能,以下操作盡量移至循環體外處理,如定義對象、變量、獲取數據庫連接,進行不必要的 try-catch 操作( 這個 try-catch 是否可以移至循環體外) 。
9. 【推薦】 避免采用取反邏輯運算符。
說明: 取反邏輯不利于快速理解,并且取反邏輯寫法必然存在對應的正向邏輯寫法。
正例: 使用 if (x < 628) 來表達 x 小于 628。
反例: 使用 if (!(x >= 628)) 來表達 x 小于 628。
10.【推薦】 接口入參保護,這種場景常見的是用作批量操作的接口。
11.【參考】 下列情形,需要進行參數校驗:
1) 調用頻次低的方法。
2) 執行時間開銷很大的方法。 此情形中, 參數校驗時間幾乎可以忽略不計,但如果因為參數錯誤導致
中間執行回退,或者錯誤,那得不償失。
3) 需要極高穩定性和可用性的方法。
4) 對外提供的開放接口,不管是 RPC/API/HTTP 接口。
5) 敏感權限入口。
?
12.【參考】 下列情形, 不需要進行參數校驗:
1) 極有可能被循環調用的方法。但在方法說明里必須注明外部參數檢查要求。
2) 底層調用頻度比較高的方法。畢竟是像純凈水過濾的最后一道,參數錯誤不太可能到底層才會暴露問題。一般 DAO 層與 Service 層都在同一個應用中,部署在同一臺服務器中,所以 DAO 的參數校驗,可以省略。
3) 被聲明成 private 只會被自己代碼所調用的方法,如果能夠確定調用方法的代碼傳入參數已經做過檢查或者肯定不會有問題,此時可以不校驗參數。
(八) 注釋規約
1. 【強制】 類、類屬性、類方法的注釋必須使用 Javadoc 規范,使用/**內容*/格式,不得使用
// xxx 方式。
說明: 在 IDE 編輯窗口中, Javadoc 方式會提示相關注釋,生成 Javadoc 可以正確輸出相應注釋;在 IDE中,工程調用方法時,不進入方法即可懸浮提示方法、參數、返回值的意義,提高閱讀效率。
2. 【強制】 所有的抽象方法( 包括接口中的方法) 必須要用 Javadoc 注釋、除了返回值、參數、異常說明外,還必須指出該方法做什么事情,實現什么功能。
說明: 對子類的實現要求,或者調用注意事項,請一并說明。
3. 【強制】 所有的類都必須添加創建者和創建日期。
4. 【強制】 方法內部單行注釋,在被注釋語句上方另起一行,使用//注釋。方法內部多行注釋使用/* */注釋,注意與代碼對齊。
5. 【強制】 所有的枚舉類型字段必須要有注釋,說明每個數據項的用途。
6. 【推薦】 與其“半吊子” 英文來注釋,不如用中文注釋把問題說清楚。專有名詞與關鍵字保持英文原文即可。
反例: “TCP 連接超時” 解釋成“傳輸控制協議連接超時” ,理解反而費腦筋。
7. 【推薦】 代碼修改的同時,注釋也要進行相應的修改,尤其是參數、返回值、異常、核心邏輯等的修改。
說明: 代碼與注釋更新不同步,就像路網與導航軟件更新不同步一樣,如果導航軟件嚴重滯后,就失去了導航的意義。
8. 【參考】 謹慎注釋掉代碼。 在上方詳細說明,而不是簡單地注釋掉。 如果無用,則刪除。
說明: 代碼被注釋掉有兩種可能性: 1)后續會恢復此段代碼邏輯。 2)永久不用。前者如果沒有備注信息,難以知曉注釋動機。后者建議直接刪掉(代碼倉庫已然保存了歷史代碼)。
9. 【參考】 對于注釋的要求:第一、能夠準確反映設計思想和代碼邏輯; 第二、能夠描述業務含義,使別的程序員能夠迅速了解到代碼背后的信息。完全沒有注釋的大段代碼對于閱讀者形同天書,注釋是給自己看的,即使隔很長時間,也能清晰理解當時的思路; 注釋也是給繼任者看的,使其能夠快速接替自己的工作。
10.【參考】 好的命名、代碼結構是自解釋的,注釋力求精簡準確、表達到位。避免出現注釋的一個極端:過多過濫的注釋,代碼的邏輯一旦修改,修改注釋是相當大的負擔。
反例:
// put elephant into fridge
put(elephant, fridge);
方法名 put,加上兩個有意義的變量名 elephant 和 fridge,已經說明了這是在干什么,語義清晰的代碼不需要額外的注釋。
11.【參考】 特殊注釋標記,請注明標記人與標記時間。注意及時處理這些標記,通過標記掃描,經常清理此類標記。線上故障有時候就是來源于這些標記處的代碼。
1) 待辦事宜( TODO) :(標記人,標記時間, [預計處理時間])表示需要實現,但目前還未實現的功能。這實際上是一個 Javadoc 的標簽,目前的 Javadoc 還沒有實現,但已經被廣泛使用。只能應用于類,接口和方法(因為它是一個 Javadoc 標簽)。
2) 錯誤,不能工作( FIXME) :(標記人,標記時間, [預計處理時間])在注釋中用 FIXME 標記某代碼是錯誤的,而且不能工作,需要及時糾正的情況。
(九) 其它
1. 【強制】 在使用正則表達式時,利用好其預編譯功能,可以有效加快正則匹配速度。
說明: 不要在方法體內定義: Pattern pattern = Pattern.compile(“規則” );
2. 【強制】 velocity 調用 POJO 類的屬性時,直接使用屬性名取值即可,模板引擎會自動按規范調用 POJO 的 getXxx(),如果是 boolean 基本數據類型變量( boolean 命名不需要加 is 前綴) ,會自動調用 isXxx()方法。
說明: 注意如果是 Boolean 包裝類對象,優先調用 getXxx()的方法。
3. 【強制】 后臺輸送給頁面的變量必須加$!{var}——中間的感嘆號。
說明: 如果 var 等于 null 或者不存在,那么${var}會直接顯示在頁面上。
4. 【強制】 注意 Math.random() 這個方法返回是 double 類型,注意取值的范圍 0≤x<1( 能夠取到零值,注意除零異常) ,如果想獲取整數類型的隨機數,不要將 x 放大 10 的若干倍然后取整,直接使用 Random 對象的 nextInt 或者 nextLong 方法。
5. 【強制】 獲取當前毫秒數 System.currentTimeMillis(); 而不是 new Date().getTime();
說明: 如果想獲取更加精確的納秒級時間值, 使用 System.nanoTime()的方式。在 JDK8 中,針對統計時間等場景,推薦使用 Instant 類。
6. 【強制】 日期格式化時,傳入 pattern 中表示年份統一使用小寫的 y。
說明: 日期格式化時, yyyy 表示當天所在的年,而大寫的 YYYY 代表是 week in which year( JDK7 之后引入的概念),意思是當天所在的周屬于的年份,一周從周日開始,周六結束,只要本周跨年,返回的 YYYY 就是下一年。另外需要注意:
? 表示月份是大寫的 M
? 表示分鐘則是小寫的 m
? 24 小時制的是大寫的 H
? 12 小時制的則是小寫的 h
正例: 表示日期和時間的格式如下所示:
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
7. 【推薦】 不要在視圖模板中加入任何復雜的邏輯。
說明: 根據 MVC 理論,視圖的職責是展示,不要搶模型和控制器的活。
8. 【推薦】 任何數據結構的構造或初始化,都應指定大小,避免數據結構無限增長吃光內存。
9. 【推薦】 及時清理不再使用的代碼段或配置信息。
說明: 對于垃圾代碼或過時配置,堅決清理干凈,避免程序過度臃腫,代碼冗余。
正例: 對于暫時被注釋掉,后續可能恢復使用的代碼片斷,在注釋代碼上方,統一規定使用三個斜杠(///)來說明注釋掉代碼的理由。
?
總結
以上是生活随笔為你收集整理的熟读《阿里巴巴java开发手册》(一、编程规约)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2019建中台,2020拆中台,为什么很
- 下一篇: 熟读《阿里巴巴java开发手册》(二、异