1. 程式人生 > >Java開發最佳實踐(一) ——《Java開發手冊》之"程式設計規約"

Java開發最佳實踐(一) ——《Java開發手冊》之"程式設計規約"

  • Java開發手冊版本更新說明
  • 專有名詞解釋
  • 一、 程式設計規約
    • (一) 命名風格
    • (二) 常量定義
    • (三) 程式碼格式
    • (四) OOP 規約
    • (五) 集合處理
    • (六) 併發處理
    • (七) 控制語句
    • (八) 註釋規約
    • (九) 其它

Java開發手冊版本更新說明

版本號 版本名 更新日期 備註
1.3.0 終極版 2017.09.25 單元測試規約,IDE程式碼規約外掛
1.3.1 紀念版 2017.11.30 修正部分描述
1.4.0 詳盡版 2018.05.20 增加設計規約大類,共16條
1.5.0 華山版 2019.06.19 詳細更新見下面

本筆記主要基於華山版(1.5.0)的總結。華山版具體更新如下:

  • 鑑於本手冊是社群開發者集體智慧的結晶,本版本移除阿里巴巴Java開發手冊的限定詞阿里巴巴
  • 新增21條新規約。比如,switch的NPE問題、浮點數的比較、無泛型限制、鎖的使用方式、判斷表示式、日期格式等
  • 修改描述112處。比如,IFNULL的判斷、集合的toArray、日誌處理等
  • 完善若干處示例。比如,命名示例、衛語句示例、enum示例、finallyreturn示例等。

PDF下載地址: https://pan.baidu.com/s/1K-GZ_CzRC0igIxMgLGVtZQ 密碼:關注行無際的微信公眾號:it_wild,回覆java開發手冊

專有名詞解釋

  1. POJO(Plain Ordinary Java Object): 在本手冊中,POJO專指只有setter、getter、toString的簡單類,包括DO、DTO、BO、VO等。
  2. GAV(GroupId、ArtifactctId、Version): Maven座標,是用來唯一標識jar包。
  3. OOP(Object Oriented Programming): 本手冊泛指類、物件的程式設計處理方式。
  4. ORM(Object Relation Mapping): 物件關係對映,物件領域模型與底層資料之間的轉換,本文泛指ibatis, mybatis等框架。
  5. NPE(java.lang.NullPointerException): 空指標異常。
  6. SOA(Service-Oriented Architecture): 面向服務架構,它可以根據需求通過網路對鬆散耦合的粗粒度應用元件進行分散式部署、組合和使用,有利於提升元件可重用性,可維護性。
  7. IDE(Integrated Development Environment): 用於提供程式開發環境的應用程式,一般包括程式碼編輯器、編譯器、偵錯程式和圖形使用者介面等工具,本《手冊》泛指 IntelliJ IDEA 和eclipse。
  8. OOM(Out Of Memory): 源於java.lang.OutOfMemoryError,當JVM沒有足夠的記憶體來為物件分配空間並且垃圾回收器也無法回收空間時,系統出現的嚴重狀況。
  9. 一方庫:本工程內部子專案模組依賴的庫(jar包)。
  10. 二方庫:公司內部發布到中央倉庫,可供公司內部其它應用依賴的庫(jar包)。
  11. 三方庫:公司之外的開源庫(jar包)。

    一、 程式設計規約

    (一) 命名風格

    正例:
  • 國際通用的名稱,可視同英文;alibaba / youku / hangzhou
  • 類名使用UpperCamelCase風格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。如:UserDO / XmlService / TcpUdpDeal
  • 方法名、引數名、成員變數、區域性變數都統一使用lowerCamelCase風格,必須遵從駝峰形式;localValue / getHttpMessage / inputUserId
  • 常量命名全部大寫,單詞間用下劃線隔開,力求語義表達完整清楚,不要嫌名字長。MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
  • 抽象類命名使用AbstractBase開頭;異常類命名使用Exception結尾;測試類命名以它要測試的類的名稱開始,以Test結尾
  • 型別與中括號緊挨相連來表示陣列,定義整形陣列 int[] arrayDemo;
  • 包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用單數形式,但是類名如果有複數含義,類名可以使用複數形式。包名com.alibaba.ai.util,類名為MessageUtils(此規則參考spring的框架結構)
  • 為了達到程式碼自解釋的目標,任何自定義程式設計元素在命名時,使用盡量完整的單詞組合來表達其意。在JDK中,表達原子更新的類名為:AtomicReferenceFieldUpdater
  • 在常量與變數的命名時,表示型別的名詞放在詞尾,以提升辨識度。如:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
  • 如果模組、介面、類、方法使用了設計模式,在命名時需體現出具體模式(將設計模式體現在名字中,有利於閱讀者快速理解架構設計理念)。如: class OrderFactory / class LoginProxy / class ResourceObserver
  • 介面類中的方法和屬性不要加任何修飾符號(public也不要加),保持程式碼的簡潔性,並加上有效的Javadoc註釋。儘量不要在接口裡定義變數,如果一定要定義變數,肯定是與介面方法相關,並且是整個應用的基礎常量。介面方法簽名void commit();,介面基礎常量String COMPANY = "alibaba";
  • 對於ServiceDAO類,基於SOA的理念,暴露出來的服務一定是介面,內部的實現類用Impl的字尾與介面區別。如CacheServiceImpl實現CacheService介面
  • 如果是形容能力介面名稱,取對應的形容詞為介面名(通常是–able的形容詞)如 AbstractTranslator實現Translatable介面
  • 列舉類名帶上Enum字尾,列舉成員名稱需要全大寫,單詞間用下劃線隔開。(說明:列舉其實就是特殊的類,域成員均為常量,且構造方法被預設強制是私有。)列舉名字為ProcessStatusEnum的成員名稱:SUCCESS / UNKNOWN_REASON
  • 各層命名規約:

    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。

反例:

  • 不能以下劃線或美元符號開始、結束,如:name$namename
  • 嚴禁使用拼音與英文混合的方式,如:DaZhePromotion[打折]、 getPingfenByName() [評分]
  • 避免在子父類的成員變數之間、或者不同程式碼塊的區域性變數之間採用完全相同的命名,使可讀性降低。
  • 杜絕完全不規範的縮寫,避免望文不知義。AbstractClass“縮寫”命名成AbsClasscondition“縮寫”命名成condi,此類隨意縮寫嚴重降低了程式碼的可閱讀性。
  • 介面類中的方法和屬性不要加任何修飾符號(public也不要加) public abstract void f();
  • POJO類中布林型別變數都不要加is字首,否則部分框架解析會引起序列化錯誤

    定義為基本資料型別Boolean isDeleted的屬性,它的方法也是isDeleted(),RPC框架在反向解析的時候,“誤以為”對應的屬性名稱是deleted,導致屬性獲取不到,進而丟擲異常。

(二) 常量定義

  • 不允許任何魔法值(即未經預先定義的常量)直接出現在程式碼中 String key ="Id#taobao_" + tradeId;
  • long或者Long賦值時,數值後使用大寫的L,不能是小寫的l,小寫容易跟數字1混淆,造成誤解。Long a = 2l;
  • 不要使用一個常量類維護所有常量,要按常量功能進行歸類,分開維護。正例:快取相關常量放在類CacheConsts下;系統配置相關常量放在類ConfigConsts下。
  • 如果變數值僅在一個固定範圍內變化用enum型別來定義。

(三) 程式碼格式

  • 採用4個空格縮排,禁止使用tab字元。
  • 註釋的雙斜線與註釋內容之間有且僅有一個空格。
  • 在進行型別強制轉換時,右括號與強制轉換值之間不需要任何空格隔開int second = (int)first + 2;
  • IDE的text file encoding設定為UTF-8;IDE中檔案的換行符使用Unix格式,不要使用Windows格式。
  • 單個方法的總行數不超過80行
  • 不同邏輯、不同語義、不同業務的程式碼之間插入一個空行分隔開來以提升可讀性(說明:任何情形,沒有必要插入多個空行進行隔開。)

    (四) OOP 規約

  • 避免通過一個類的物件引用訪問此類的靜態變數靜態方法,無謂增加編譯器解析成本,直接用類名來訪問即可
  • 所有的覆寫方法,必須加@Override註解
  • 外部正在呼叫或者二方庫依賴的介面,不允許修改方法簽名,避免對介面呼叫方產生影響。介面過時必須加@Deprecated註解,並清晰地說明採用的新介面或者新服務是麼。
  • 不能使用過時的類或方法。
  • Object的equals方法容易拋空指標異常,應使用常量或確定有值的物件來呼叫equals,"test".equals(object);【推薦使用 java.util.Objects#equals(JDK7 引入的工具類】
  • 所有整型包裝類物件之間值的比較,全部使用equals方法比較。【在-128至127這個區間之外的所有資料,都會在堆上產生,並不會複用已有物件,這是一個大坑,推薦使用equals方法進行判斷】
  • 定義資料物件DO類時,屬性型別要與資料庫欄位型別相匹配。資料庫欄位的bigint必須與類屬性的Long型別相對應。
  • 為了防止精度損失,禁止使用構造方法BigDecimal(double)的方式把double值轉化為BigDecimal物件(在精確計算或值比較的場景中可能會導致業務邏輯異常)。BigDecimal g = new BigDecimal(0.1f);實際的儲存值為:0.10000000149。正例:優先推薦入參為String的構造方法,或使用BigDecimalvalueOf方法,此方法內部其實執行了DoubletoString,而Double的toStringdouble的實際能表達的精度對尾數進行了截斷。
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
  • 關於基本資料型別與包裝資料型別的使用標準如下:1)【強制】所有的POJO類屬性必須使用包裝資料型別。2)【強制】RPC方法的返回值和引數必須使用包裝資料型別。3) 【推薦】所有的區域性變數使用基本資料型別。【說明:POJO類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何NPE問題,或者入庫檢查,都由使用者來保證。正例:資料庫的查詢結果可能是null,因為自動拆箱,用基本資料型別接收有NPE風險。反例:比如顯示成交總額漲跌情況,即正負x%,x為基本資料型別,呼叫的RPC服務,呼叫不成功時,返回的是預設值,頁面顯示為0%,這是不合理的,應該顯示成中劃線。所以包裝資料型別的null值,能夠表示額外的資訊,如:遠端呼叫失敗,異常退出。
  • 定義 DO/DTO/VO等POJO類時,不要設定任何屬性預設值。【反例:POJO 類的 createTime 預設值為 new Date(),但是這個屬性在資料提取時並沒有置入具體值,在更新其它欄位時又附帶更新了此欄位,導致建立時間被修改成當前時間。】
  • 序列化類新增屬性時,請不要修改serialVersionUID欄位,避免反序列失敗;如果完全不相容升級,避免反序列化混亂,那麼請修改serialVersionUID值。(說明:注意serialVersionUID不一致會丟擲序列化執行時異常。)
  • 構造方法裡面禁止加入任何業務邏輯,如果有初始化邏輯,請放在init方法中。
  • POJO類必須寫toString方法。使用IDE中的工具:source> generate toString時,如果繼承了另一個POJO類,注意在前面加一下super.toString。【說明:在方法執行丟擲異常時,可以直接呼叫 POJO 的 toString()方法列印其屬性值,便於排查問題】
  • 禁止在POJO類中,同時存在對應屬性xxx的isXxx()getXxx()方法。【說明:框架在呼叫屬性 xxx 的提取方法時,並不能確定哪個方法一定是被優先呼叫到】
  • 使用索引訪問用String的split方法得到的陣列時,需做最後一個分隔符後有無內容的檢查,否則會有拋IndexOutOfBoundsException的風險
  • 當一個類有多個構造方法,或者多個同名方法,這些方法應該按順序放置在一起,便於閱讀,此條規則優先於下一條
  • 類內方法定義的順序依次是:公有方法或保護方法 > 私有方法 > getter/setter方法。
  • getter/setter方法中,不要增加業務邏輯,增加排查問題的難度
  • 迴圈體內,字串的連線方式,使用StringBuilderappend方法進行擴充套件
  • final可以宣告類(不允許被繼承的類,如String類)、成員變數(不允許修改引用的域物件)、方法、以及本地變數(不允許執行過程中重新賦值的區域性變數),避免上下文重複使用一個變數,使用final可以強制重新定義一個變數,方便更好地進行重構
  • 慎用Objectclone方法來拷貝物件,物件clone方法預設是淺拷貝,若想實現深拷貝需覆寫clone方法實現域物件的深度遍歷式拷貝。
  • 類成員與方法訪問控制從嚴。1)如果不允許外部直接通過new來建立物件,那麼構造方法必須是private。2)工具類不允許有public或default構造方法。3)類非static 成員變數並且與子類共享,必須是protected。 4)類非static成員變數並且僅在本類使用,必須是private。5)類static成員變數如果僅在本類使用,必須是private。 6)若是static成員變數,考慮是否為final。7)類成員方法只供類內部呼叫,必須是 private。8)類成員方法只對繼承類公開,那麼限制為protected。【說明:任何類、方法、引數、變數,嚴控訪問範圍。過於寬泛的訪問範圍,不利於模組解耦】
  • 浮點數之間的等值判斷,基本資料型別不能用==來比較,包裝資料型別不能用equals 來判斷。【浮點數採用“尾數+階碼”的編碼方式,類似於科學計數法的“有效數字+指數”的表示方式】
// 反例
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
if (a == b) { // 預期進入此程式碼快,執行其它業務邏輯
// 但事實上 a==b 的結果為 false
}
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
if (x.equals(y)) { // 預期進入此程式碼快,執行其它業務邏輯
// 但事實上 equals 的結果為 false
}

// 正例

// (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構造的時候注意事項 見上文
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");
}

(五) 集合處理

  • 關於hashCodeequals的處理,遵循如下規則:1)只要覆寫equals,就必須覆寫hashCode。2)因為Set儲存的是不重複的物件,依據hashCodeequals進行判斷,所以Set儲存的物件必須覆寫這兩個方法。3)如果自定義物件作為Map的鍵,那麼必須覆寫hashCodeequals。【說明:String已覆寫hashCodeequals方法,所以我們可以愉快地使用String物件作為key來使用】
  • ArrayListsubList結果不可強轉成ArrayList,否則會丟擲ClassCastException異常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList【說明:subList返回的是ArrayList的內部類SubList,並不是ArrayList而是ArrayList的一個檢視,對於SubList子列表的所有操作最終會反映到原列表上】
  • 使用Map的方法keySet()/values()/entrySet()返回集合物件時,不可以對其進行新增元素操作,否則會丟擲UnsupportedOperationException異常
  • Collections類返回的物件,如:emptyList()/singletonList()等都是immutable list,不可對其進行新增或者刪除元素的操作【反例:如果查詢無結果,返回 Collections.emptyList()空集合物件,呼叫方一旦進行了新增元素的操作,就會觸發UnsupportedOperationException異常。】
  • subList場景中,高度注意對原集合元素的增加或刪除,均會導致子列表的遍歷、增加、刪除產生ConcurrentModificationException異常
  • 使用集合轉陣列的方法,必須使用集合的toArray(T[] array),傳入的是型別完全一致、長度為0的空陣列【反例:直接使用toArray無參方法存在問題,此方法返回值只能是Object[]類,若強轉其它型別陣列將出現ClassCastException錯誤。】
// 正例
List<String> list = new ArrayList<>(2);
list.add("行無際");
list.add("itwild");
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隱患
*/
  • 在使用Collection介面任何實現類的addAll()方法時,都要對輸入的集合引數進行NPE判斷 【說明:在ArrayList#addAll方法的第一行程式碼即Object[] a = c.toArray();其中c為輸入集合引數,如果為null,則直接丟擲異常。】
  • 使用工具類Arrays.asList()把陣列轉換成集合時,不能使用其修改集合相關的方法,它的add/remove/clear方法會丟擲UnsupportedOperationException異常【說明:asList的返回物件是一個Arrays內部類,並沒有實現集合的修改方法。Arrays.asList體現的是介面卡模式,只是轉換介面,後臺的資料仍是陣列。】
String[] str = new String[] { "it", "wild" };
List list = Arrays.asList(str);

// 第一種情況:list.add("itwild"); 執行時異常
// 第二種情況:str[0] = "changed1"; 也會隨之修改
// 反之亦然 list.set(0, "changed2");
  • 泛型萬用字元<? extends T>來接收返回的資料,此寫法的泛型集合不能使用add方法,而<? super T>不能使用get方法,作為介面呼叫賦值時易出錯。【說明:擴充套件說一下PECS(Producer Extends Consumer Super)原則:第一、頻繁往外讀取內容的,適合用<? extends T>。第二、經常往裡插入的,適合用<? super T>

    這個地方我覺得有必要簡單解釋一下(行無際本人的個人理解哈,有不對的地方歡迎指出),上面的說法可能有點官方或者難懂。其實我們一直也是這麼幹的,不過沒注意而已。舉個最簡單的例子,用泛型的時候,如果你遍歷(read)一個List,你是不是希望List裡面裝的越具體越好啊,你希望裡面裝的是Object嗎,如果裡面裝的是Object那麼你想想你會有多痛苦,每個物件都用instanceof判斷一下再型別強轉,所以這個方法的引數List主要用於遍歷(read)的時候,大多數情況你可能會要求裡面的元素最大是T型別,即用<? extends T>限制一下。再看你往List裡面插入(write)資料又會怎麼樣,為了靈活性和可擴充套件性,你馬上可能就要說我當然希望List裡面裝的是Object了,這樣我什麼型別的物件都能往List裡面寫啊,這樣設計出來的介面的靈活性和可擴充套件性才強啊,如果裡面裝的型別太靠下(假定繼承層次從上往下,父類在上,子孫類在下),那麼位於上級的很多型別的資料你就無法寫入了,這個時候用<? super T>來限制一下最小是T型別。下面我們來看Collections.copy()這個例子。

// 這裡就要求dest的List裡面的元素型別 不能在src的List元素型別 之下
// 如果dest的List元素型別位於src的List元素型別之下,就會出現寫不進dest
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    //....省略具體的copy程式碼
}

// 下面再看我寫的測試程式碼就更容易理解了
static class Animal {}

static class Dog extends Animal {}

static class BlackDog extends Dog {}

@Test
public void test() throws Exception {

    List<Dog> dogList = new ArrayList<>(2);
    dogList.add(new BlackDog());
    dogList.add(new BlackDog());

    List<Animal> animalList = new ArrayList<>(2);
    animalList.add(new Animal());
    animalList.add(new Animal());

    // 錯誤,無法編譯通過
    Collections.copy(dogList, animalList);

    // 正確
    Collections.copy(animalList, dogList);
    
    // Collections.copy()的泛型引數就起作到了很好的限制作用
    // 編譯期就能發現型別不對
}
  • 在無泛型限制定義的集合賦值給泛型限制的集合時,在使用集合元素時,需要進行instanceof判斷,避免丟擲ClassCastException異常。
// 反例
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);
  • 不要在foreach迴圈裡進行元素的remove/add操作。remove元素請使用Iterator方式,如果併發操作,需要對Iterator物件加鎖
// 正例
List<String> list = new ArrayList<>(); 
list.add("1"); 
list.add("2"); 

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

// 反例
for (String item : list) { 
  if ("1".equals(item)) { 
    list.remove(item); 
  } 
}
  • 在JDK7版本及以上,Comparator實現類要滿足如下三個條件,不然Arrays.sortCollections.sort會拋IllegalArgumentException異常【說明:三個條件如下 1)x,y 的比較結果和 y,x 的比較結果相反。2)x>y,y>z,則x>z。 3) x=y,則x,z比較結果和y,z 比較結果相同。】
// 反例:下例中沒有處理相等的情況
new Comparator<Student>() { 
  @Override 
  public int compare(Student o1, Student o2) { 
    return o1.getId() > o2.getId() ? 1 : -1; 
  } 
};
  • 集合泛型定義時,在JDK7及以上,使用diamond語法或全省略。【說明:菱形泛型,即 diamond,直接使用<>來指代前邊已經指定的型別】
// 正例
// diamond 方式,即<>
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);
  • 集合初始化時,指定集合初始值大小【說明:HashMap使用HashMap(int initialCapacity)初始化。】

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

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

  • 使用entrySet遍歷Map類集合KV,而不是keySet方式進行遍歷。【說明:keySet其實是遍歷了2次,一次是轉為Iterator物件,另一次是從hashMap中取出 key所對應的value。而entrySet只是遍歷了一次就把keyvalue都放到了entry中,效率更高。如果是JDK8,使用Map.forEach方法。】

    正例:values()返回的是V值集合,是一個list集合物件;keySet()返回的是K值集合,是一個Set集合物件;entrySet()返回的是K-V值組合集合。

  • 高度注意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異常。

  • 合理利用好集合的有序性(sort)和穩定性(order),避免集合的無序性(unsort)和不穩定性(unorder)帶來的負面影響。【說明:有序性是指遍歷的結果是按某種比較規則依次排列的。穩定性指集合每次遍歷的元素次序是一定的。如:ArrayListorder/unsortHashMapunorder/unsortTreeSetorder/sort。】
  • 利用Set元素唯一的特性,可以快速對一個集合進行去重操作,避免使用Listcontains方法進行遍歷、對比、去重操作

(六) 併發處理

  • 獲取單例物件需要保證執行緒安全,其中的方法也要保證執行緒安全【說明:資源驅動類、工具類、單例工廠類都需要注意】
  • 建立執行緒或執行緒池時請指定有意義的執行緒名稱,方便出錯時回溯
// 正例:自定義執行緒工廠,並且根據外部特徵進行分組,比如機房資訊
public class UserThreadFactory implements ThreadFactory {
    private final String namePrefix;
    private final AtomicInteger nextId = new AtomicInteger(1);
    // 定義執行緒組名稱,在 jstack 問題排查時,非常有幫助
    UserThreadFactory(String whatFeaturOfGroup) {
        namePrefix = "From UserThreadFactory's " + whatFeaturOfGroup + "-Worker-"; 
    }
    
    @Override
    public Thread newThread(Runnable task) {
        String name = namePrefix + nextId.getAndIncrement();
        Thread thread = new Thread(null, task, name, 0, false);
        System.out.println(thread.getName());
        return thread; 
    } 
}
  • 執行緒資源必須通過執行緒池提供,不允許在應用中自行顯式建立執行緒【說明:執行緒池的好處是減少在建立和銷燬執行緒上所消耗的時間以及系統資源的開銷,解決資源不足的問題。如果不使用執行緒池,有可能造成系統建立大量同類執行緒而導致消耗完記憶體或者“過度切換”的問題】
  • 執行緒池不允許使用Executors去建立,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險

    說明:Executors返回的執行緒池物件的弊端如下:

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

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

  • SimpleDateFormat是執行緒不安全的類,一般不要定義為static變數,如果定義為static,必須加鎖。【說明:如果是JDK8的應用,可以使用Instant代替DateLocalDateTime代替CalendarDateTimeFormatter代替SimpleDateFormat,官方給出的解釋:simple beautiful strong immutable thread-safe。】
// 正例:注意執行緒安全。亦推薦如下處理
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { 
    @Override 
    protected DateFormat initialValue() { 
        return new SimpleDateFormat("yyyy-MM-dd"); 
    } 
};
  • 必須回收自定義的ThreadLocal變數,尤其線上程池場景下,執行緒經常會被複用,如果不清理自定義的ThreadLocal變數,可能會影響後續業務邏輯和造成記憶體洩露等問題。儘量使用try-finally塊進行回收
// 正例
objectThreadLocal.set(userInfo);
try {
    // ...
} finally {
    objectThreadLocal.remove();
}
  • 高併發時,同步呼叫應該去考量鎖的效能損耗。能用無鎖資料結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用物件鎖,就不要用類鎖【說明:儘可能使加鎖的程式碼塊工作量儘可能的小,避免在鎖程式碼塊中呼叫RPC方法。】
  • 對多個資源、資料庫表、物件同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。【說明:執行緒一需要對錶A、B、C依次全部加鎖後才可以進行更新操作,那麼執行緒二的加鎖順序也必須是A、B、C,否則可能出現死鎖。】
  • 在使用阻塞等待獲取鎖的方式中,必須在try程式碼塊之外,並且在加鎖方法與try程式碼塊之間沒有任何可能丟擲異常的方法呼叫,避免加鎖成功後,在finally中無法解鎖

    說明一:如果在lock方法與try程式碼塊之間的方法呼叫丟擲異常,那麼無法解鎖,造成其它執行緒無法成功獲取鎖。

說明二:如果lock方法在try程式碼塊之內,可能由於其它方法丟擲異常,導致在 finally程式碼塊中,unlock對未加鎖的物件解鎖,它會呼叫AQStryRelease方法(取決於具體實現類),丟擲IllegalMonitorStateException異常。

說明三:在Lock物件的lock方法實現中可能丟擲unchecked異常,產生的後果與說明二相同

// 正例
Lock lock = new XxxLock();
// ...
lock.lock();
try {
    doSomething();
    doOthers();
} finally {
    lock.unlock();
}

// 反例
Lock lock = new XxxLock();
// ...
try {
    // 如果此處丟擲異常,則直接執行 finally 程式碼塊
    doSomething();
    // 無論加鎖是否成功,finally 程式碼塊都會執行
    lock.lock();
    doOthers();
} finally {
    lock.unlock();
}
  • 在使用嘗試機制來獲取鎖的方式中,進入業務程式碼塊之前,必須先判斷當前執行緒是否持有鎖。鎖的釋放規則與鎖的阻塞等待方式相同
// 正例
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
    try {
      doSomething();
      doOthers();
    } finally {
      lock.unlock();
    } 
}
  • 併發修改同一記錄時,避免更新丟失,需要加鎖。要麼在應用層加鎖,要麼在快取加鎖,要麼在資料庫層使用樂觀鎖,使用version作為更新依據【說明:如果每次訪問衝突概率小於20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於3次。】
  • 多執行緒並行處理定時任務時,Timer執行多個TimeTask時,只要其中之一沒有捕獲丟擲的異常,其它任務便會自動終止執行,如果在處理定時任務時使用ScheduledExecutorService則沒有這個問題
  • 資金相關的金融敏感資訊,使用悲觀鎖策略。【說明:樂觀鎖在獲得鎖的同時已經完成了更新操作,校驗邏輯容易出現漏洞,另外,樂觀鎖對衝突的解決策略有較複雜的要求,處理不當容易造成系統壓力或資料異常,所以資金相關的金融敏感資訊不建議使用樂觀鎖更新。】
  • 使用CountDownLatch進行非同步轉同步操作,每個執行緒退出前必須呼叫countDown方法,執行緒執行程式碼注意catch異常,確保countDown方法被執行到,避免主執行緒無法執行至await方法,直到超時才返回結果【說明:注意,子執行緒丟擲異常堆疊,不能在主執行緒try-catch到。】
  • 避免Random例項被多執行緒使用,雖然共享該例項是執行緒安全的,但會因競爭同一seed導致的效能下降【說明:Random例項包括java.util.Random的例項或者Math.random()的方式。正例:在JDK7之後,可以直接使用APIThreadLocalRandom,而在JDK7之前,需要編碼保證每個執行緒持有一個例項】
  • 在併發場景下,通過雙重檢查鎖(double-checked locking)實現延遲初始化的優化問題隱患(可參考 The "Double-Checked Locking is Broken" Declaration),推薦解決方案中較為簡單一種(適用於JDK5及以上版本),將目標屬性宣告為volatile型。
// 注意 這裡的程式碼並非出自官方的《java開發手冊》
// 參考 https://blog.csdn.net/lovelion/article/details/7420886
public class LazySingleton { 
    // volatile除了保證內容可見性還有防止指令重排序
    // 物件的建立實際上是三條指令:
    // 1、分配記憶體地址 2、記憶體地址初始化 3、返回記憶體地址控制代碼
    // 其中2、3之間可能發生指令重排序
    // 重排序可能導致執行緒A建立物件先執行1、3兩步,
    // 結果執行緒B進來判斷控制代碼已經不為空,直接返回給上層方法
    // 此時物件還沒有正確初始化記憶體,導致上層方法發生嚴重錯誤
    private volatile static LazySingleton instance = null; 
 
    private LazySingleton() { } 
    
    public static LazySingleton getInstance() { 
        // 第一重判斷
        if (instance == null) {
            synchronized (LazySingleton.class) {
                // 第二重判斷
                if (instance == null) {
                    // 建立單例例項
                    instance = new LazySingleton(); 
                }
            }
        }
        return instance; 
    }
}

// 既然這裡提到 單例懶載入,還有這樣寫的
// 參考 https://blog.csdn.net/lovelion/article/details/7420888
class Singleton {
  private Singleton() { }
  
  private static class HolderClass {
  // 由Java虛擬機器來保證其執行緒安全性,確保該成員變數只能初始化一次
    final static Singleton instance = new Singleton();
  }

  public static Singleton getInstance() {
      return HolderClass.instance;
  }
}
  • volatile解決多執行緒記憶體不可見問題。對於一寫多讀,是可以解決變數同步問題,但是如果多寫,同樣無法解決執行緒安全問題。【說明:如果是count++操作,使用如下類實現:AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是JDK8,推薦使用LongAdder物件,比AtomicLong效能更好(減少樂觀鎖的重試次數)。】
  • HashMap在容量不夠進行resize時由於高併發可能出現死鏈,導致CPU飆升,在開發過程中可以使用其它資料結構或加鎖來規避此風險
  • ThreadLocal物件使用static修飾,ThreadLocal無法解決共享物件的更新問題【說明:這個變數是針對一個執行緒內所有操作共享的,所以設定為靜態變數,所有此類例項共享此靜態變數,也就是說在類第一次被使用時裝載,只分配一塊儲存空間,所有此類的物件(只要是這個執行緒內定義的)都可以操控這個變數】

(七) 控制語句

  • switch括號內的變數型別為String並且此變數為外部引數時,必須先進行null判斷。
public class SwitchString {
    
    public static void main(String[] args) {
        // 這裡會拋異常 java.lang.NullPointerException
        method(null);
    }
    
    public static void method(String param) {
        switch (param) {
            // 肯定不是進入這裡
            case "sth":
                System.out.println("it's sth");
                break;
            // 也不是進入這裡
            case "null":
                System.out.println("it's null");
                break;
            // 也不是進入這裡
            default:
                System.out.println("default");
        } 
    }
}
  • if/else/for/while/do語句中必須使用大括號。【說明:即使只有一行程式碼,避免採用單行的編碼方式:if (condition) statements;
  • 高併發場景中,避免使用”等於”判斷作為中斷或退出的條件。【說明:如果併發控制沒有處理好,容易產生等值判斷被“擊穿”的情況,使用大於或小於的區間判斷條件來代替。】

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

  • 表達異常的分支時,少用if-else方式,這種方式可以改寫成下面程式碼:【說明:如果非使用if()...else if()...else...方式表達邏輯,避免後續程式碼維護困難,請勿超過3層】
if (condition) { 
    ...
    return obj; 
} 
// 接著寫 else 的業務邏輯程式碼;

超過3層的if-else的邏輯判斷程式碼可以使用衛語句策略模式狀態模式等來實現。其中衛語句即程式碼邏輯先考慮失敗、異常、中斷、退出等直接返回的情況,以方法多個出口的方式,解決程式碼中判斷分支巢狀的問題,這是逆向思維的體現。

// 示例程式碼
public void findBoyfriend(Man man) {
  if (man.isUgly()) {
    System.out.println("本姑娘是外貌協會的資深會員");
    return;
  }
  if (man.isPoor()) {
    System.out.println("貧賤夫妻百事哀");
    return;
  }
  if (man.isBadTemper()) {
    System.out.println("銀河有多遠,你就給我滾多遠");
    return;
  }
  System.out.println("可以先交往一段時間看看");
}
  • 除常用方法(如getXxx/isXxx)等外,不要在條件判斷中執行其它複雜的語句,將複雜邏輯判斷的結果賦值給一個有意義的布林變數名,以提高可讀性。【說明:很多if語句內的邏輯表示式相當複雜,與、或、取反混合運算,甚至各種方法縱深呼叫,理解成本非常高。如果賦值一個非常好理解的布林變數名字,則是件令人爽心悅目的事情。】
// 正例
// 虛擬碼如下
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
  ...
}

// 反例
// 哈哈,這好像是ReentrantLock裡面有類似風格的程式碼
// 連Doug Lea的程式碼都拿來當做反面教材啊
// 早前就聽別人說過“程式設計不識Doug Lea,寫盡Java也枉然!!!”
public final void acquire(long arg) {
  if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
      selfInterrupt();
  } 
}
  • 不要在其它表示式(尤其是條件表示式)中,插入賦值語句【說明:賦值點類似於人體的穴位,對於程式碼的理解至關重要,所以賦值語句需要清晰地單獨成為一行。】
// 反例
public Lock getLock(boolean fair) {
  // 算術表示式中出現賦值操作,容易忽略 count 值已經被改變
  threshold = (count = Integer.MAX_VALUE) - 1;
  // 條件表示式中出現賦值操作,容易誤認為是 sync==fair
  return (sync = fair) ? new FairSync() : new NonfairSync();
}
  • 迴圈體中的語句要考量效能,以下操作儘量移至迴圈體外處理,如定義物件、變數、獲取資料庫連線,進行不必要的try-catch操作(這個try-catch是否可以移至迴圈體外)
  • 避免採用取反邏輯運算子。【說明:取反邏輯不利於快速理解,並且取反邏輯寫法必然存在對應的正向邏輯寫法。正例:使用if (x < 628)來表達x小於628。反例:使用 if (!(x >= 628))來表達x小於628。】
  • 下列情形,需要進行引數校驗

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

  • 下列情形,不需要進行引數校驗:

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

(八) 註釋規約

  • 類、類屬性、類方法的註釋必須使用Javadoc規範,使用/**內容*/格式,不得使用// xxx方式。【說明:在IDE編輯視窗中,Javadoc方式會提示相關注釋,生成 Javadoc可以正確輸出相應註釋;在IDE中,工程呼叫方法時,不進入方法即可懸浮提示方法、引數、返回值的意義,提高閱讀效率。】
  • 所有的抽象方法(包括介面中的方法)必須要用Javadoc註釋、除了返回值、引數、異常說明外,還必須指出該方法做什麼事情,實現什麼功能【說明:對子類的實現要求,或者呼叫注意事項,請一併說明】
  • 所有的類都必須新增建立者和建立日期。
  • 方法內部單行註釋,在被註釋語句上方另起一行,使用//註釋。方法內部多行註釋使用/* */註釋,注意與程式碼對齊
  • 所有的列舉型別欄位必須要有註釋,說明每個資料項的用途
  • 與其“半吊子”英文來註釋,不如用中文註釋把問題說清楚。專有名詞與關鍵字保持英文原文即可【反例:“TCP 連線超時”解釋成“傳輸控制協議連線超時”,理解反而費腦筋。】
  • 程式碼修改的同時,註釋也要進行相應的修改,尤其是引數、返回值、異常、核心邏輯等的修改。【程式碼與註釋更新不同步,就像路網與導航軟體更新不同步一樣,如果導航軟體嚴重滯後,就失去了導航的意義】
  • 謹慎註釋掉程式碼。在上方詳細說明,而不是簡單地註釋掉。如果無用,則刪除。【說明:程式碼被註釋掉有兩種可能性:1)後續會恢復此段程式碼邏輯。2)永久不用。前者如果沒有備註資訊,難以知曉註釋動機。後者建議直接刪掉(程式碼倉庫已然儲存了歷史程式碼)】
  • 對於註釋的要求:第一、能夠準確反映設計思想和程式碼邏輯;第二、能夠描述業務含義,使別的程式設計師能夠迅速瞭解到程式碼背後的資訊。完全沒有註釋的大段程式碼對於閱讀者形同天書,註釋是給自己看的,即使隔很長時間,也能清晰理解當時的思路;註釋也是給繼任者看的,使其能夠快速接替自己的工作。
  • 好的命名、程式碼結構是自解釋的,註釋力求精簡準確、表達到位。避免出現註釋的一個極端:過多過濫的註釋,程式碼的邏輯一旦修改,修改註釋是相當大的負擔【語義清晰的程式碼不需要額外的註釋。】
  • 特殊註釋標記,請註明標記人與標記時間。注意及時處理這些標記,通過標記掃描,經常清理此類標記。線上故障有時候就是來源於這些標記處的程式碼。

    1)待辦事宜(TODO):(標記人,標記時間,[預計處理時間])表示需要實現,但目前還未實現的功能。這實際上是一個Javadoc的標籤,目前的Javadoc還沒
    有實現,但已經被廣泛使用。只能應用於類,介面和方法(因為它是一個Javadoc標籤)。2)錯誤,不能工作(FIXME):(標記人,標記時間,[預計處理時間])
    在註釋中用FIXME標記某程式碼是錯誤的,而且不能工作,需要及時糾正的情況。

(九) 其它

  • 在使用正則表示式時,利用好其預編譯功能,可以有效加快正則匹配速度【說明:不要在方法體內定義:Pattern pattern = Pattern.compile(“規則”);
  • 注意Math.random()這個方法返回是double型別,注意取值的範圍0≤x<1(能夠取到零值,注意除零異常),如果想獲取整數型別的隨機數,不要將x放大10的若干倍然後取整,直接使用Random物件的nextInt或者nextLong方法。
  • 獲取當前毫秒數System.currentTimeMillis();而不是new Date().getTime();【說明:如果想獲取更加精確的納秒級時間值,使用System.nanoTime()的方式。在JDK8中,針對統計時間等場景,推薦使用Instant類。】
  • 日期格式化時,傳入pattern中表示年份統一使用小寫的y。【說明:日期格式化時,yyyy表示當天所在的年,而大寫的YYYY代表是 week in which year(JDK7之後引入的概念),意思是當天所在的周屬於的年份,一週從週日開始,週六結束,只要本週跨年,返回的YYYY就是下一年。另外需要注意:表示月份是大寫的M,表示分鐘則是小寫的m,24小時制的是大寫的H,12小時制的則是小寫的h 。正例:new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  • 任何資料結構的構造或初始化,都應指定大小,避免資料結構無限增長吃光記憶體
  • 及時清理不再使用的程式碼段或配置資訊【說明:對於垃圾程式碼或過時配置,堅決清理乾淨,避免程式過度臃腫,程式碼冗餘。正例:對於暫時被註釋掉,後續可能恢復使用的程式碼片斷,在註釋程式碼上方,統一規定使用三個斜槓(///)來說明註釋掉程式碼的理由】