1. 程式人生 > >JAVA程式設計規範

JAVA程式設計規範

一、程式設計規約

(一)、命名風格

1.[強制]程式碼中的命名均不能以下劃線或美元符號開始,也不能一下劃線或美元符號結束。 反例:_name / name / name/name/namename / name_ / name / name

2.[強制]程式碼中的命名嚴禁使用拼音與英文混合的方式,更不允許直接使用中文的方式。 說明:正確的英文拼寫和語法可以讓閱讀者易於理解,避免歧義。注意,即使純拼音命名方式也要避免採用。 正例:alibaba / taobao / youku / hangzhou 等國際通用的名稱,可視同英文。 反例:DaZhePromotion [打折] / getPingfenByName() [評分] / int 某變數 = 3

3.[強制]類名使用UpperCamelCase風格,但是以下情形例外:DO / BO / DTO / VO / AO / PO 等。 正例:MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion 反例:macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion

4.[強制]方法名、引數名、成員變數、區域性變數都統一使用 lowerCamelCase 風格,必須遵循駝峰形式。 正例: localValue / getHttpMessage() / inputUserId

5.[強制]變數和常量的命名方式:

非公有(private/protected/default)變數前面要加上小寫m; 靜態變數(static)前面加上小寫s; 其它變數以小寫字母開頭; 靜態常量(static final)全大寫。

6.[強制]常量命名全部大寫,單詞鍵用下劃線隔開,力求語義表達完整,不要嫌名字長。 正例:MAX_STOCK_COUNT 反例:MAX_COUNT

7.[強制]抽象類命名使用 Abstract 或 Base 開頭;異常類命名使用 Exception 結尾;測試類命名以它要測試的類名開始,以Test結尾。

8.[強制]型別與中括號緊挨相連來定義陣列。 正例:定義整形陣列 int[] arrayDemo; 反例:在 main 引數中,使用 String args[]來定義。

9.[強制] POJO類中布林型別的標量,都不要加is字首,否則部分框架解析會引起序列化錯誤。 反例:定義為基本資料型別 Boolean isDeleted;的屬性,它的方法也是 isDeleted(),RPC框架在反向解析的時候,“誤以為”對應的屬性名稱是 deleted,導致屬性獲取不到,進而丟擲異常。

10.[強制]包名統一是用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用單數形式,但是類名如有複數含義,類名可以使用複數形式。其實很多團隊都會用公司官方網址的倒寫作為包名。這些都是可以主觀規定的。 正例:應用工具類包名為 com.alibaba.ai.util、類名為 MessageUtils(此規則參考 spring 的框架結構)

11.[強制]杜絕完全不規範的縮寫,避免忘文不知義。 反例:AbstractClass“縮寫”命名成 AbsClass;condition“縮寫”命名成 condi,此類隨意縮寫嚴重降低了程式碼的可閱讀性。

12.[推薦]為了達到程式碼自己自解釋的目標,任何自定義程式設計元素在命名時,使用精良完整的單詞組合來表達其意。但是有些名稱實在過於長,這個可以適當的縮寫,不要忘文不知義就可以。這個不是客觀的規定。 正例:從遠端倉庫拉取程式碼的類命名為 PullCodeFromRemoteRepository。 反例:變數 int a; 的隨意命名方式。

13.[推薦]如果模組、介面、類、方法使用了設計模式,在命名時體現出具體模式。 說明:將設計模式體現在名字中,有利於閱讀者快速理解架構設計理念。 正例:

public class OrderFactory; public class LoginProxy; public class ResourceObserver;123

14.[推薦]介面類中的方法和屬性不要加任何修飾符號(public 也不要加),保持程式碼的簡潔性,並加上有效的javadoc註釋。精良不要在接口裡定義變數,如果一定要定義變數,肯定是與介面方法相關,並且是整個應用的基礎常量。 正例:介面方法簽名 void f(); 介面基礎常量 String COMPANY = “alibaba”; 反例:介面方法定義 public abstract void f(); 說明:JDK8 中介面允許有預設實現,那麼這個 default 方法,是對所有實現類都有價值的預設實現。

15.介面和實現類的命名有兩套規則:

對於Service和DAO類,基於SOA的理念,暴露出來的服務一定是介面,內部的實現類用lmpl的字尾與介面區別。

正例:CacheServiceImpl 實現 CacheService 介面。

[推薦]如果是形容能力的介面名稱,取對應的形容詞為介面名(通常是-able的形式)。

正例:AbstractTranslator 實現 Translatable。

16.[參考]列舉類名建議帶上Enum字尾,列舉成員名稱需要全部大寫,單詞間用下劃線隔開。 說明:列舉其實就是特殊的常量類,且構造方法被預設強制是私有。 正例:列舉名字為 ProcessStatusEnum 的成員名稱:SUCCESS / UNKNOWN_REASON。

17.[參考]各層命名規約: A)Service/DAO層方法命名規約:

獲取單個物件的方法用get作字首。 獲取多個物件的方法用list作字首。 獲取統計值的方法用count作字首。 插入的方法用save/insert作字首。 刪除的方法用remove/delete作字首。 修改的方法用update作字首。

B)領域模型命名規約

資料物件xxxDO,xxx即為資料表名。 資料傳輸物件:xxxDTO,xxx為業務領域相關的名稱。 展示物件:xxxVO,xxx一般為網頁名稱。 POJO是DO/DTO/BO/VO 的統稱,禁止命名成xxxPOJO。

(二)、常量定義

1.[強制]不允許任何魔法值(即未經預先定義的常量)直接出現在程式碼中。 反例:

String key = “Id#taobao_” + tradeId; cache.put(key, value);12

說明:上面的”Id#taobao_”就是魔法值,需要先定義再使用: 正例:

private static final String NAME_TAOBAO = “Id#taobao_”; String key = NAME_TAOBAO + tradeId; cache.put(key, value);123

2.[強制] long或者Long初始賦值時,使用大寫的L,不能小寫的l,小寫容易跟數字1混淆,造成誤解。 說明:Long a = 2l; 寫的是數字的 21,還是 Long 型的 2l

3.[推薦]不要使用一個常量類維護所有常量,按常量功能進行歸類,分開維護。 說明:大而全的長兩類,非得使用查詢功能才能定位到修改的常量,不利於理解和維護。 正例:快取相關常量放在類 CacheConsts 下;系統配置相關常量放在類 ConfigConsts 下。

4.[推薦]常量的複用層次有五層:跨應用共享常量、應用內共享常量、子工程內共享常量、包內共存常量、類內共享常量。

跨應用共享常量:放置雜二方庫中,通常是 client.jar 中的 constant 目錄下。 應用內共享常量:放置在一方庫中,通常是子模組中的 constant 目錄下。

反例:易懂變數也要統一定義成應用內共享常量,兩位攻城師在兩個類中分別定義了表示“是”的變數: 類 A 中:public static final String YES = “yes”; 類B 中:public static final String YES = “y”; A.YES.equals(B.YES),預期是 true,但實際返回為 false,導致線上問題。

子工程內部共享常量:即在當前子工程的 constant 目錄下。 包內共享常量:即在當前包下單獨的 constant 目錄下。 類內共享常量:直接在類內部 private static final 定義。

5.[推薦]如果變數值盡在一個固定範圍內變化用enum型別來定義。 說明如果存在名稱之外的延伸屬性使用enum型別,下面整理中的數字就是延伸資訊,表示一年中的第幾個季節。 正例:

public enum SeasonEnum {
    SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);
    int seq;

    SeasonEnum(int seq) {
        this.seq = seq;
    }
}12345678

(三)程式碼格式

1.[強制]大括號的使用約定。如果是大括號內為空,則簡介地寫成{}即可,不需要換行;如果是非空程式碼塊則:

左大括號前不換行。 左大括號後換行。 右大括號前換行。 右大括號後還有 else 等程式碼則不換行;表示終止的右大括號後必須換行。

2.[強制]左小括號和字元之間不出現空格;同樣的,有小括號和字元之間也不出現空格。詳見第5條下面正例提示。 反例:if (空格 a == b 空格)

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 點)

public static void main(String[] args) {
    // 縮排 4 個空格
    String say = "hello";
    // 運算子的左右必須有一個空格 int flag = 0;
    // 關鍵詞 if 與括號之間必須有一個空格,括號內的 f 與左括號,0 與右括號不需要空格 if (flag == 0) {
    System.out.println(say);
}

    // 左大括號前加空格且不換行;左大括號後換行
    if (flag == 1) {
        System.out.println("world");
        // 右大括號前換行,右大括號後有 else,不用換行
    } else {
        System.out.println("ok");

        // 在右大括號後直接結束,則必須換行
    }

}123456789101112131415161718

6.[強制]註釋的雙斜線與註釋內容之間有且僅有一個空格。 正例:

// 這是示例註釋,請注意在雙斜線之後有一個空格 String ygb = new String();12

7.[強制]單行字元數限制不超過120個,超出需要換行,換行時遵循如下原則:

第二行相對第一行縮排 4 個空格,從第三行開始,不再繼續縮排,參考示例。 運算子與下文一起換行。 方法呼叫的點符號與下文一起換行。 方法呼叫時,多個引數,需要換行時,在逗號後進行。 在括號前不要換行,見反例。

正例:

StringBuffer sb = new StringBuffer(); //超過 120 個字元的情況下,換行縮排 4 個空格,點號和方法名稱一起換行 sb.append(“zi”).append(“xin”)… .append(“huang”)… .append(“huang”)… .append(“huang”);123456

反例:

StringBuffer sb = new StringBuffer(); //超過 120 個字元的情況下,不要在括號前換行 sb.append(“zi”).append(“xin”)…append (“huang”);

//引數很多的方法呼叫可能超過 120 個字元,不要在逗號前換行 method(args1, args2, args3, … , argsX);12345678

8.[強制]方法引數在定義和傳入是,多個引數逗號後邊必須加空格。 正例:下例中實參的”a”,後邊必須要有一個空格。

method(“a”, “b”, “c”);1

9.[強制] IDE的text file encoding設定為UTF-8; IDE 中檔案的換行符使用 Unix 格式,不要使用 Windows 格式。

10.[推薦]沒有必要增加若干空格來是耨一行的字元與上一行對應位置的字元對齊。 正例:

int a = 3; long b = 4L; float c = 5F; StringBuffer sb = new StringBuffer();1234

說明:增加 sb 這個變數,如果需要對齊,則給 a、b、c 都要增加幾個空格,在變數比較多的情況下,是非常累贅的事情。

11.[推薦]不同邏輯、不同語義、不同業務的程式碼之間插入一個空行分割開來以提升可讀性。 說明:沒有必要插入多個空行進行隔開

(四)OOP規約

1.[強制]避免通過一個類的物件引用訪問此類的靜態變數或靜態方法,無謂增加編譯器解析成本,直接用類名來訪問即可。

2.[強制]所有的覆寫方法,必須加@Override註解。 說明:getObject()與 get0bject()的問題。一個是字母的 O,一個是數字的 0,加@Override可以準確判斷是否覆蓋成功。另外,如果在抽象類中對方法簽名進行修改,其實現類會馬上編譯報錯。

3.[強制]相同引數型別,相同業務含義,才可以使用Java的可變引數,避免使用Object。 說明:可變引數必須放置在引數列表的最後。(提倡同學們儘量不用可變引數程式設計) 正例:

public User getUsers(String type, Integer… ids) {…}1

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.關於基本資料型別與包裝資料型別的使用標準如下:

[強制]所有的 POJO 類屬性必須使用包裝資料型別。 [強制]RPC 方法的返回值和引數必須使用包裝資料型別。 [推薦]所有的區域性變數使用基本資料型別。

說明:POJO 類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何NPE 問題,或者入庫檢查,都由使用者來保證。 正例:資料庫的查詢結果可能是 null,因為自動拆箱,用基本資料型別接收有 NPE 風險。

反例:比如顯示成交總額漲跌情況,即正負 x%,x 為基本資料型別,呼叫的 RPC 服務,呼叫不成功時,返回的是預設值,頁面顯示為 0%,這是不合理的,應該顯示成中劃線。所以包裝資料型別的 null 值,能夠表示額外的資訊,如:遠端呼叫失敗,異常退出。

9.[強制]定義 DO/DTO/VO 等 POJO 類時,不要設定任何屬性預設值。 反例:POJO 類的 gmtCreate 預設值為 new Date();但是這個屬性在資料提取時並沒有置入具體值,在更新其它欄位時又附帶更新了此欄位,導致建立時間被修改成當前時間。

10.[強制]序列化類新增屬性時,請不要修改 serialVersionUID 欄位,避免反序列化失敗;如果完全不相容升級,避免反序列化混亂,那麼請修改 serialVersionUID 值。 說明:注意 serialVersionUID 不一致會丟擲序列化執行時異常。

11.[強制]構造方法裡面進位制加入任何業務邏輯。使用IDE中的工具,請放在init方法中。

12.[強制] POJO類必須寫toString方法。使用IDE中的工具:source> generate toString時,如果繼承了另一個 POJO 類,注意在前面加一下 super.toString。 說明:在方法執行丟擲異常時,可以直接呼叫 POJO 的 toString()方法列印其屬性值,便於排查問題。

13.[推薦]使用索引訪問String的split方法得到的陣列時,需做最後一個分隔符後有無內容的檢查,否則會有拋 IndexOutOfBoundsException 的風險。 說明:

String str = “a,b,c,”; String[] ary = str.split(","); //預期大於 3,結果是 3 System.out.println(ary.length);1234

14.[推薦]當一個類有多個構造方法,或者多個同名方法,這些方法應該按順序放置在一起,便於閱讀,此條規則優先於第15條規則。

15.[推薦]類內方法定義的順序此意是:公有方法或保護方法>私有方法>getter/setter方法。 說明:公有方法是類的呼叫者和維護者最關心的方法,首屏展示最好;保護方法雖然只是子類關心,也可能是“模板設計模式”下的核心方法;而私有方法外部一般不需要特別關心,是一個黑盒實現;因為承載的資訊價值較低,所有 Service 和 DAO 的 getter/setter 方法放在類體最後。

16.[推薦] setter方法中,引數名稱與類成員變數名稱一致,this.成員名 = 引數名。在getter/setter方法中,不要增加業務邏輯,增加排查問題的難度。 反例:

public Integer getData() {
    if (condition) {
        return this.data + 100;
    } else {
        return this.data - 100;
    }
}1234567

17.[推薦]迴圈體內,字串的連線方法,使用StringBuilder的append方法進行擴充套件。 說明:反編譯出的位元組碼檔案顯示每次迴圈都會 new 出一個 StringBuilder 物件,然後進行 append 操作,最後通過 toString 方法返回 String 物件,造成記憶體資源浪費。 反例:

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

18.[推薦] final可以宣告類、成員變數、方法、以及本地變數,下列情況使用final關鍵字:

不允許被繼承的類,如:String 類。 不允許修改引用的域物件,如:POJO 類的域變數。 不允許被重寫的方法,如:POJO 類的 setter 方法。 不允許執行過程中重新賦值的區域性變數。 避免上下文重複使用一個變數,使用 final 描述可以強制重新定義一個變數,方便更好地進行重構。

19.[推薦]慎用Object的clone方法來拷貝物件。 說明:物件的clone方法預設是淺拷貝,若想實現深拷貝需要重寫 clone 方法實現屬性物件的拷貝。

20.[推薦]類成員與方法訪問控制從嚴:

如果不允許外部直接通過 new 來建立物件,那麼構造方法必須是 private。 工具類不允許有 public 或 default 構造方法。 類非 static 成員變數並且與子類共享,必須是 protected。 類非 static 成員變數並且僅在本類使用,必須是 private。 類 static 成員變數如果僅在本類使用,必須是 private。 若是 static 成員變數,必須考慮是否為 final。 類成員方法只供類內部呼叫,必須是 private。 類成員方法只對繼承類公開,那麼限制為 protected。

說明:任何類、方法、引數、變數,嚴控訪問範圍。過於寬泛的訪問範圍,不利於模組解耦。 思考:如果是一個private的方法,想刪除就刪除,可是一個public的service成員方法或成員變數,刪除一下,不得手心冒點汗嗎?變數像自己的小孩,儘量在自己的視線內,變數作用域太大,無限制的到處跑,那麼你會擔心的。

(五)集合處理

1.[強制]關於hsahCode和equals的處理,遵循如下規則:

只要重寫 equals,就必須重寫 hashCode。 因為 Set 儲存的是不重複的物件,依據 hashCode 和 equals 進行判斷,所以 Set 儲存的物件必須重寫這兩個方法。 如果自定義物件作為 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.[強制]在 subList 場景中,高度注意對原集合元素個數的修改,會導致子列表的遍歷、增加、刪除均會產生 ConcurrentModificationException 異常。

4.[強制]使用集合轉陣列的方法,必須使用集合的 toArray(T[] array),傳入的是型別完全一樣的陣列,大小就是list.size()。 說明:使用 toArray 帶參方法,入參分配的陣列空間不夠大時,toArray 方法內部將重新分配記憶體空間,並返回新陣列地址;如果陣列元素個數大於實際所需,下標為[ list.size() ] 的陣列元素將被置為 null,其它陣列元素保持原值,因此最好將方法入引數組大小

定義與集合元素個數一致。 正例:

    List<String> list = new ArrayList<String>(2);
    list.add("guan");
    list.add("bao");
    String[] array = new String[list.size()];
    array = list.toArray(array);12345

反例:直接使用 toArray 無參方法存在問題,此方法返回值只能是 Object[]類,若強轉其它型別陣列將出現 ClassCastException 錯誤。

5.[強制]使用工具類 Arrays.asList()把陣列轉換成集合時,不能使用其修改集合相關的方法,它的 add/remove/clear 方法丟擲UnsupportedOperationException 異常。 說明:asList 的返回物件是一個 Arrays 內部類,並沒有實現集合的修改方法。Arrays.asList體現的是介面卡模式,只是轉換介面,後臺的資料仍是陣列。

String[] str = new String[] { “you”, “wu” }; List list = Arrays.asList(str);12

第一種情況:list.add(“yangguanbao”); 執行時異常。 第二種情況:str[0] = “gujin”; 那麼 list.get(0)也會隨之修改。

6.[強制]泛型萬用字元<? extends T>來接收返回的資料,此寫法的泛型集合不能使用 add 方法,而<? super T>不能使用 get 方法,作為介面呼叫賦值時易出錯。 說明:擴充套件說一下 PECS(Producer Extends Consumer Super)原則:第一、頻繁往外讀取內容的,適合用<? extends T>。第二、經常往裡插入的,適合用<? super T>。

7.[強制]不要在 foreach 迴圈裡進行元素的 remove/add 操作。remove 元素請使用 Iterator方式,如果併發操作,需要對 Iterator 物件加鎖。 正例:

    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        String item = iterator.next();
        if (刪除元素的條件) {
            iterator.remove();
        }
    }1234567

反例:

    List<String> list = new ArrayList<String>();
    list.add("1");
    list.add("2");
    for (String item : list) {
        if ("1".equals(item)) {
            list.remove(item);
        }
    }12345678

8.[強制]在 JDK7 版本及以上,Comparator 要滿足如下三個條件,不然 Arrays.sort,Collections.sort 會報 IllegalArgumentException 異常。 說明:三個條件如下

x,y 的比較結果和 y,x 的比較結果相反。 x>y,y>z,則 x>z。 x=y,則 x,z 比較結果和 y,z 比較結果相同。

反例:下例中沒有處理相等的情況,實際使用中可能會出現異常:

    new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getId() > o2.getId() ? 1 : -1;
        }
    };123456

9.[推薦]集合初始化時,指定集合初始值大小。 說明:HashMap 使用 HashMap(int initialCapacity) 初始化,

正例:initialCapacity = (需要儲存的元素個數 / 負載因子) + 1。注意負載因子(即 loader factor)預設為 0.75,如果暫時無法確定初始值大小,請設定為 16(即預設值)。

反例:HashMap 需要放置 1024 個元素,由於沒有設定容量初始大小,隨著元素不斷增加,容量7 次被迫擴大,resize 需要重建 hash 表,嚴重影響效能。

10.[推薦]使用 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 值組合集合。

11.[推薦]高度注意 Map 類集合 K/V 能不能儲存 null 值的情況,如下表格:

集合類 Key Value Super 說明

Hashtable 不允許為 null 不允許為 null Dictionary 執行緒安全

ConcurrentHashMap 不允許為 null 不允許為 null AbstractMap 鎖分段技術(JDK8:CAS)

TreeMap 不允許為 null 允許為 null AbstractMap 執行緒不安全

HashMap 允許為 null 允許為 null AbstractMap 執行緒不安全

反例: 由於 HashMap 的干擾,很多人認為 ConcurrentHashMap 是可以置入 null 值,而事實上,儲存 null 值時會丟擲 NPE 異常。

12.[參考]合理利用好集合的有序性(sort)和穩定性(order),避免集合的無序性(unsort)和不穩定性(unorder)帶來的負面影響。 說明:有序性是指遍歷的結果是按某種比較規則依次排列的。穩定性指集合每次遍歷的元素次序是一定的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是order/sort。

13.[參考]利用 Set 元素唯一的特性,可以快速對一個集合進行去重操作,避免使用 List 的 contains 方法進行遍歷、對比、去重操作。

(六)併發處理

1.[強制]獲取單例物件需要保證執行緒安全,其中的方法也要保證執行緒安全。 說明:資源驅動類、工具類、單例工廠類都需要注意。

2.[強制]建立執行緒或執行緒池時請指定有意義的執行緒名稱,方便出錯時回溯。 正例:

public class TimerTaskThread extends Thread { public TimerTaskThread() { super.setName(“TimerTaskThread”); … }12345

3.[強制]執行緒資源必須通過執行緒池提供,不允許在應用中自行顯示建立執行緒。 說明:使用執行緒池的好處是減少在建立和銷燬執行緒上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用執行緒池,有可能造成系統建立大量同類執行緒而導致消耗完記憶體或者“過度切換”的問題。

4.[強制]執行緒池不允許使用Executors去建立,而是通過 ThreadPoolExecutor的方法,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。 說明:Executors 返回的執行緒池物件的弊端如下:

FixedThreadPool 和 SingleThreadPool:

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

CachedThreadPool 和 ScheduledThreadPool:

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

5.[強制] SimpleDateFormat 是執行緒不安全的類,一般不要定義為 static 變數,如果定義為static,必須加鎖,或者使用 DateUtils 工具類。 正例:注意執行緒安全,使用 DateUtils。亦推薦如下處理:

private static final ThreadLocal df = new ThreadLocal() { @Override protected DateFormat initialValue() { return new SimpleDateFormat(“yyyy-MM-dd”); } };123456

說明:如果是 JDK8 的應用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方給出的解釋: simple beautiful strong immutable thread-safe。

6.[強制]高併發時同步呼叫應該去考量鎖的效能損耗。能用無鎖資料結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用物件鎖,就不要用類鎖。 說明:儘可能使加鎖的程式碼塊工作量儘可能的小,避免在鎖程式碼塊中呼叫 RPC 方法。

7.[強制]對多個資源、資料庫表、物件同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。 說明:執行緒一需要對錶 A、B、C 依次全部加鎖後才可以進行更新操作,那麼執行緒二的加鎖順序也必須是 A、B、C,否則可能出現死鎖。

8.[強制]併發修改同一記錄時,避免更新丟失,需要枷鎖。要麼在應用層加鎖,要麼在快取加鎖,要麼在資料庫層使用樂觀鎖,使用version作為更新依據。 說明:如果每次訪問衝突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於 3 次。

9.[強制]多執行緒併發處理定時任務時,Timer執行多個TimeTask時,只要其中之一沒有捕獲丟擲的異常,其他任務便會自動終止執行,使用ScheduledExecutorService 則沒有這個問題。

10.[推薦]使用CountDownLatch 進行非同步轉同步操作,每個執行緒退出前必須呼叫 countDown 方法,執行緒執行程式碼注意 catch 異常,確保 countDown 方法被執行到,避免主執行緒無法執行至 await 方法,直到超時才返回結果。 說明:注意,子執行緒丟擲異常堆疊,不能在主執行緒 try-catch 到。

11.[推薦]避免 Random 例項被多執行緒使用,雖然共享該例項是執行緒安全的,但會因競爭同一 seed 導致的效能下降。 說明:Random 例項包括 java.util.Random 的例項或者 Math.random()的方式。 正例:在 JDK7 之後,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要編碼保證每個執行緒持有一個例項。

12.[推薦]在併發場景下,通過雙重檢查鎖(double-checked locking)實現延遲初始化的優化問題隱患(可參考 The “Double-Checked Locking is Broken” Declaration),推薦解決方案中較為簡單一種(適用於 JDK5 及以上版本),將目標屬性宣告為 volatile 型。 反例:

class Singleton {
    private Helper helper = null;

    public Helper getHelper() {
        if (helper == null)
            synchronized (this) {
                if (helper == null)
                    helper = new Helper();
            }
        return helper;
    }
    // other methods and fields...
}12345678910111213

13.[參考] volatile 解決多執行緒記憶體不可見問題。對於一寫多讀,是可以解決變數同步問題,但是如果多寫,同樣無法解決執行緒安全問題。如果是 count++操作,使用如下類實現: AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推薦使用 LongAdder 物件,比 AtomicLong 效能更好(減少樂觀鎖的重試次數)。

14.[參考] HashMap 在容量不夠進行 resize 時由於高併發可能出現死鏈,導致 CPU 飆升,在開發過程中可以使用其它資料結構或加鎖來規避此風險。

15.[參考] ThreadLocal 無法解決共享物件的更新問題,ThreadLocal 物件建議使用 static 修飾。這個變數是針對一個執行緒內所有操作共享的,所以設定為靜態變數,所有此類例項共享此靜態變數 ,也就是說在類第一次被使用時裝載,只分配一塊儲存空間,所有此類的物件(只要是這個執行緒內定義的)都可以操控這個變數。

(七)控制語句

1.[強制]在一個switch塊內,每一個case要麼通過break/return等來終止,要麼註釋說明程式將繼續執行到哪一個case為止;在一個switch塊內,都必須包含一個default語句並且放在最後,即使空程式碼。

2.[強制]在 if/else/for/while/do 語句中必須使用大括號。即使只有一行程式碼,避免採用單行的編碼方式:if (condition) statements;

3.[強制]在高併發場景中,避免使用”等於”判斷作為中斷或退出的條件。 說明:如果併發控制沒有處理好,容易產生等值判斷被“擊穿”的情況,使用大於或小於的區間判斷條件來代替。

反例:判斷剩餘獎品數量等於 0 時,終止發放獎品,但因為併發處理錯誤導致獎品數量瞬間變成了負數,這樣的話,活動無法終止。

4.[推薦]表達異常的分支時,少用 if-else 方式,這種方式可以改寫成:

    if (condition) {
        ...
        return obj;
        }
        //接著寫 else 的業務邏輯程式碼;12345

說明:如果非得使用 if()…else if()…else…方式表達邏輯,【強制】避免後續程式碼維護困難,請勿超過 3 層。 正例:超過 3 層的 if-else 的邏輯判斷程式碼可以使用衛語句、策略模式、狀態模式等來實現, 其中衛語句示例如下:

public void today() {
    if (isBusy()) {
        System.out.println(“change time.”);
        return;
    }

    if (isFree()) {
        System.out.println(“go to travel.”);
        return;
    }
        System.out.println(“stay at home to learn Alibaba Java Coding Guidelines.”); 
        return;
    }12345678910111213

5.[推薦]除常用方法(如 getXxx/isXxx)等外,不要在條件判斷中執行其它複雜的語句,將複雜邏輯判斷的結果賦值給一個有意義的布林變數名,以提高可讀性。

說明:很多 if 語句內的邏輯相當複雜,閱讀者需要分析條件表示式的最終結果,才能明確什麼樣的條件執行什麼樣的語句,那麼,如果閱讀者分析邏輯表示式錯誤呢?

正例:

//虛擬碼如下 final boolean existed = (file.open(fileName, “w”) != null) && (…) || (…); if (existed) { … }1234

反例:

if ((file.open(fileName, “w”) != null) && (…) || (…)) { … }123

6.[推薦]迴圈體中的語句要考量效能,以下操作儘量移至迴圈體外處理,如定義物件、變數、獲取資料庫連線,進行不必要的 try-catch 操作(這個 try-catch 是否可以移至迴圈體外)。

7.[推薦]避免採用取反邏輯運算子。 說明:取反邏輯不利於快速理解,並且取反邏輯寫法必然存在對應的正向邏輯寫法。 正例:使用 if (x < 628) 來表達 x 小於 628。 反例:使用 if (!(x >= 628)) 來表達 x 小於 628。

8.[推薦]介面入參保護,這種場景常見的是用作鼻樑操作的介面。

9.[參考]下列情形,需要進行引數校驗:

呼叫頻次低的方法。 執行時間開銷很大的方法。此情形中,引數校驗時間幾乎可以忽略不計,但如果因為引數錯誤導致中間執行回退,或者錯誤,那得不償失。 需要極高穩定性和可用性的方法。 對外提供的開放介面,不管是 RPC/API/HTTP 介面。 敏感許可權入口。

10.[參考]下列情形,不需要進行引數校驗:

極有可能被迴圈呼叫的方法。但在方法說明裡必須註明外部引數檢查要求。 底層呼叫頻度比較高的方法。畢竟是像純淨水過濾的最後一道,引數錯誤不太可能到底層才會暴露問題。一般 DAO 層與 Service 層都在同一個應用中,部署在同一臺伺服器中,所以 DAO 的引數校驗,可以省略。 被宣告成 private 只會被自己程式碼所呼叫的方法,如果能夠確定呼叫方法的程式碼傳入引數已經做過檢查或者肯定不會有問題,此時可以不校驗引數。

(八)註釋規約

1.[強制]類、類屬性、類方法的註釋必須使用Javadoc規約,使用/*內容/格式,不得使用//xxx 方式。 說明:在 IDE 編輯視窗中,Javadoc 方式會提示相關注釋,生成 Javadoc 可以正確輸出相應註釋;在 IDE 中,工程呼叫方法時,不進入方法即可懸浮提示方法、引數、返回值的意義,提高閱讀效率。

2.[強制]所有的抽象方法(包括介面中的方法)必須要用javadoc註釋、除了返回值、引數、異常說明外,還必須指出該方法做什麼事情,實現什麼功能。 說明:對子類的實現要求,或者呼叫注意實現,請一併說明。

3.[強制]所有類都必須新增建立者和建立日期。

4.[強制]方法內部單行註釋,在被註釋語句上方另起一行,使用//珠海。方法內部多行註釋使用/**/註釋,注意與程式碼對齊。

5.[強制]所有的列舉型別欄位必須要有註釋,說明每個資料項的用途。

6.[推薦]與其“半吊子”英文註釋,不如用中文註釋把問題說清楚。專有名詞與關鍵字保持英文原文即可。 反例:“TCP 連線超時”解釋成“傳輸控制協議連線超時”,理解反而費腦筋。

7.[推薦]程式碼修改的同時,註釋也要進行相應的修改,尤其是引數、返回值、異常、核心邏輯等的修改。 說明:程式碼與註釋更新不同步,就像路網與導航軟體更新不同步一樣,如果導航軟體嚴重滯後,就失去了導航的意義。

8.[參考]謹慎註釋掉程式碼。在上方詳細說明,而不是簡單地註釋掉。如果無用,則刪除。 說明:程式碼被註釋掉有兩種可能性:

後續會恢復此段程式碼邏輯。 永久不用。前者如果沒有備註資訊,難以知曉註釋動機。後者建議直接刪掉(程式碼倉庫儲存了歷史程式碼)。

9.[參考]對於註釋的要求: 第一、能夠準確反應設計思想和程式碼邏輯; 第二、能夠描述業務含義,使別的程式設計師能夠迅速瞭解到程式碼背後的資訊。完全沒有註釋的大段程式碼對於閱讀者形同天書,註釋是給自己看的,即使隔很長時間,也能清晰理解當時的思路;註釋也是給繼任者看的,使其能夠快速接替自己的工作。

10.[參考]好的命名、程式碼結構是自解釋的,註釋力求精簡準確、表達到位。避免出現註釋的一個極端:過多過濫的註釋,程式碼的邏輯一旦修改,修改註釋是相當大的負擔。 反例:

//put elephant into fridge put(elephant, fridge);12

方法名 put,加上兩個有意義的變數名 elephant 和 fridge,已經說明了這是在幹什麼,語義清晰的程式碼不需要額外的註釋。

11.[參考]特殊註釋標記,請註明標記人與標記時間。注意及時處理這些標記,通過標記掃描,經常清理此類標記。線上故障有時候就是來源於這些標記處的程式碼。

待辦事宜(TODO):( 標記人,標記時間,[預計處理時間])

表示需要實現,但目前還未實現的功能。這實際上是一個 Javadoc 的標籤,目前的 Javadoc還沒有實現,但已經被廣泛使用。只能應用於類,介面和方法(因為它是一個 Javadoc 標籤)。

錯誤,不能工作(FIXME):(標記人,標記時間,[預計處理時間])

在註釋中用 FIXME 標記某程式碼是錯誤的,而且不能工作,需要及時糾正的情況。

(九)其他

1.[強制]在使用正則表示式時,利用好其預編譯功能,可以有效加快正則匹配速度。 說明:不要在方法體內定義:Pattern pattern = Pattern.compile(規則);

2.[強制] velocity 呼叫 POJO 類的屬性時,建議直接使用屬性名取值即可,模板引擎會自動按規範呼叫 POJO 的 getXxx(),如果是 boolean 基本資料型別變數(boolean 命名不需要加 is字首),會自動呼叫 isXxx()方法。 說明:注意如果是 Boolean 包裝類物件,優先呼叫 getXxx()的方法。

3.[強制]後臺輸送給頁面的變數必須加!var——中間的感嘆號。說明:如果var=null或者不存在,那麼!var——中間的感嘆號。說明:如果var=null或者不存在,那麼!{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.[推薦]不要在檢視模板中加入任何複雜的邏輯。 說明:根據 MVC 理論,檢視的職責是展示,不要搶模型和控制器的活。

7.[推薦]任何資料結構的構造或初始化,都應指定大小,避免資料結構無限增長吃光記憶體。

8.[推薦]及時清理不再使用的程式碼段或配置資訊。 說明:對於垃圾程式碼或過時配置,堅決清理乾淨,避免程式過度臃腫,程式碼冗餘。 正例:對於暫時被註釋掉,後續可能恢復使用的程式碼片斷,在註釋程式碼上方,統一規定使用三個斜槓(///)來說明註釋掉程式碼的理由。

二、異常日誌

(一)異常處理

1.[強制] Java 類庫中定義的可以通過預檢查方式規避的 RuntimeException 異常不應該通過catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException 等等。 說明:無法通過預檢查的異常除外,比如,在解析字串形式的數字時,不得不通過 catchNumberFormatException 來實現。 正例:if (obj != null) {…} 反例:try { obj.method() } catch (NullPointerException e) {…}

2.[強制]異常不要用來做流程控制,條件控制。 說明:異常設計的初中是解決程式執行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多。

3.[強制] catch時請分清穩定程式碼和非穩定程式碼,穩定程式碼指的是無論如何不會出錯的程式碼。對於非穩定程式碼的catch儘可能進行區分異常型別,再做對應的異常處理。 說明:對大段程式碼進行try-catch,是程式無法根據不同的異常做出正確的應急反應,也不利於定位問題,這是一種不負責任的表現。 正例:使用者註冊的場景中,如果使用者輸入非法字元,或使用者名稱稱已存在,或使用者輸入密碼過於簡單,在程式上作出分門別類的判斷,並提示給使用者。

4.[強制]鋪貨異常是為了處理它,不要鋪貨了卻什麼都不處理而拋棄之,如果不想處理它,請將該異常拋給它的呼叫者。最外層的業務使用者,必須處理異常,將其轉化為使用者可以理解的內容。

5.[強制]有 try 塊放到了事務程式碼中,catch 異常後,如果需要回滾事務,一定要注意手動回滾事務。

6.[強制] finally 塊必須對資源物件、流物件進行關閉,有異常也要做 try-catch。 說明:如果 JDK7 及以上,可以使用 try-with-resources 方式。

7.[強制]不要在 finally 塊中使用 return。 說明:finally 塊中的 return 返回後方法結束執行,不會再執行 try 塊中的 return 語句。

8.[強制]捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。 說明:如果預期對方拋的是繡球,實際接到的是鉛球,就會產生意外情況。

9.[推薦]方法的返回值可以為 null,不強制返回空集合,或者空物件等,必須添加註釋充分說明什麼情況下會返回 null 值。 說明:本手冊明確防止 NPE 是呼叫者的責任。即使被呼叫方法返回空集合或者空物件,對呼叫者來說,也並非高枕無憂,必須考慮到遠端呼叫失敗、序列化失敗、執行時異常等場景返回null 的情況。

10.[推薦]防止 NPE,是程式設計師的基本修養,注意 NPE 產生的場景:

返回型別為基本資料型別,return 包裝資料型別的物件時,自動拆箱有可能產生 NPE。

反例:public int f() { return Integer 物件}, 如果為 null,自動解箱拋 NPE。

資料庫的查詢結果可能為 null。 集合裡的元素即使 isNotEmpty,取出的資料元素也可能為 null。 遠端呼叫返回物件時,一律要求進行空指標判斷,防止 NPE。 對於 Session 中獲取的資料,建議 NPE 檢查,避免空指標。 級聯呼叫 obj.getA().getB().getC();一連串呼叫,易產生 NPE。

正例:使用 JDK8 的 Optional 類來防止 NPE 問題。

11.[推薦]定義時區分 unchecked / checked 異常,避免直接丟擲 new RuntimeException(),更不允許丟擲 Exception 或者 Throwable,應使用有業務含義的自定義異常。推薦業界已定義過的自定義異常,如:DAOException / ServiceException 等。

12.[參考]對於公司外的 http/api 開放介面必須使用“錯誤碼”;而應用內部推薦異常丟擲;跨應用間 RPC 呼叫優先考慮使用 Result 方式,封裝 isSuccess()方法、“錯誤碼”、“錯誤簡簡訊息”。 說明:關於 RPC 方法返回方式使用 Result 方式的理由:

使用拋異常返回方式,呼叫方如果沒有捕獲到就會產生執行時錯誤。 如果不加棧資訊,只是 new 自定義異常,加入自己的理解的 error message,對於呼叫端解決問題的幫助不會太多。如果加了棧資訊,在頻繁調用出錯的情況下,資料序列化和傳輸的效能損耗也是問題。

13.[參考]避免出現重複的程式碼(Don’t Repeat Yourself),即 DRY 原則。 說明:隨意複製和貼上程式碼,必然會導致程式碼的重複,在以後需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是元件化。

正例:一個類中有多個 public 方法,都需要進行數行相同的引數校驗操作,這個時候請抽取:

private boolean checkParam(DTO dto) {…}1

其實以上的規範也沒有什麼特別值得去挑剔的。因為很多的規範其實是主觀觀點來的,並非全部都是,或者都需遵循以上的規則的。 一般的我們對於好的參考都是採取精華,剔除雜質,或者剔除對自己不利的。以上的規則只是《阿里java開發手冊》的一部分,想要了解更多的就去下載源文件來看。