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

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

  • 鑑於本手冊是社區開發者集體智慧的結晶,本版本移除阿里巴巴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包)。正則表達式

    1、 編程規約

    (一) 命名風格

    正例:
  • 國際通用的名稱,可視同英文;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
  • 各層命名規約:spring

    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)原則:第1、頻繁往外讀取內容的,適合用<? extends T>。第2、常常往裏插入的,適合用<? 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除了保證內容可見性還有防止指令重排序
    // 對象的建立其實是三條指令:
    // 一、分配內存地址 二、內存地址初始化 三、返回內存地址句柄
    // 其中二、3之間可能發生指令重排序
    // 重排序可能致使線程A建立對象先執行一、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、可以準確反映設計思想和代碼邏輯;第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");
  • 任何數據結構的構造或初始化,都應指定大小,避免數據結構無限增加吃光內存
  • 及時清理再也不使用的代碼段或配置信息【說明:對於垃圾代碼或過期配置,堅定清理乾淨,避免程序過分臃腫,代碼冗餘。正例:對於暫時被註釋掉,後續可能恢復使用的代碼片段,在註釋代碼上方,統一規定使用三個斜槓(///)來講明註釋掉代碼的理由】
相關文章
相關標籤/搜索