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
示例、finally
的return
示例等。
專有名詞解釋
POJO
(Plain Ordinary Java Object): 在本手冊中,POJO專指只有setter、getter、toString的簡單類,包括DO、DTO、BO、VO等。GAV
(GroupId、ArtifactctId、Version): Maven坐標,是用來唯一標識jar包。OOP
(Object Oriented Programming): 本手冊泛指類、對象的編程處理方式。ORM
(Object Relation Mapping): 對象關系映射,對象領域模型與底層數據之間的轉換,本文泛指ibatis, mybatis等框架。NPE
(java.lang.NullPointerException): 空指針異常。SOA
(Service-Oriented Architecture): 面向服務架構,它可以根據需求通過網絡對松散耦合的粗粒度應用組件進行分布式部署、組合和使用,有利於提升組件可重用性,可維護性。IDE
(Integrated Development Environment): 用於提供程序開發環境的應用程序,一般包括代碼編輯器、編譯器、調試器和圖形用戶界面等工具,本《手冊》泛指 IntelliJ IDEA 和eclipse。OOM
(Out Of Memory): 源於java.lang.OutOfMemoryError,當JVM沒有足夠的內存來為對象分配空間並且垃圾回收器也無法回收空間時,系統出現的嚴重狀況。一方庫
:本工程內部子項目模塊依賴的庫(jar包)。二方庫
:公司內部發布到中央倉庫,可供公司內部其它應用依賴的庫(jar包)。三方庫
:公司之外的開源庫(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
- 抽象類命名使用
Abstract
或Base
開頭;異常類命名使用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";
- 對於
Service
和DAO
類,基於SOA
的理念,暴露出來的服務一定是接口,內部的實現類用Impl
的后綴與接口區別。如CacheServiceImpl
實現CacheService
接口 - 如果是
形容能力
的接口
名稱,取對應的形容詞為接口名(通常是–able
的形容詞)如AbstractTranslator
實現Translatable
接口 - 枚舉類名帶上
Enum
后綴,枚舉成員名稱需要全大寫
,單詞間用下划線
隔開。(說明:枚舉其實就是特殊的類,域成員均為常量,且構造方法被默認強制是私有。)枚舉名字為ProcessStatusEnum
的成員名稱:SUCCESS
/UNKNOWN_REASON
- 各層命名規約:
A) Service/DAO 層方法命名規約
- 獲取單個對象的方法用
get
做前綴。- 獲取多個對象的方法用
list
做前綴,復數形式結尾如:listObjects
。- 獲取統計值的方法用
count
做前綴。- 插入的方法用
save
/insert
做前綴。- 刪除的方法用
remove
/delete
做前綴。- 修改的方法用
update
做前綴。
B) 領域模型命名規約
- 數據對象:
xxxDO
,xxx即為數據表名。- 數據傳輸對象:
xxxDTO
,xxx為業務領域相關的名稱。- 展示對象:
xxxVO
,xxx一般為網頁名稱。- POJO是DO/DTO/BO/VO的統稱,禁止命名成xxxPOJO。
反例:
- 不能以下划線或美元符號開始、結束,如:
_name、$name、name_ - 嚴禁使用拼音與英文混合的方式,如:
DaZhePromotion[打折]、getPingfenByName()[評分] - 避免在子父類的成員變量之間、或者不同代碼塊的局部變量之間采用完全相同的命名,使可讀性降低。
- 杜絕完全不規范的縮寫,避免望文不知義。
AbstractClass
“縮寫”命名成AbsClass,condition
“縮寫”命名成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
的構造方法,或使用BigDecimal
的valueOf
方法,此方法內部其實執行了Double
的toString
,而Double的toString
按double
的實際能表達的精度對尾數進行了截斷。
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
方法中,不要增加業務邏輯,增加排查問題的難度 - 循環體內,字符串的連接方式,使用
StringBuilder
的append
方法進行擴展 final
可以聲明類(不允許被繼承的類,如String
類)、成員變量(不允許修改引用的域對象)、方法、以及本地變量(不允許運行過程中重新賦值的局部變量),避免上下文重復使用一個變量,使用final可以強制重新定義一個變量,方便更好地進行重構- 慎用
Object
的clone
方法來拷貝對象,對象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");
}
(五) 集合處理
- 關於
hashCode
和equals
的處理,遵循如下規則:1)只要覆寫equals
,就必須覆寫hashCode
。2)因為Set
存儲的是不重復的對象,依據hashCode
和equals
進行判斷,所以Set
存儲的對象必須覆寫這兩個方法。3)如果自定義對象作為Map
的鍵,那么必須覆寫hashCode
和equals
。【說明:String
已覆寫hashCode
和equals
方法,所以我們可以愉快地使用String
對象作為key
來使用】 ArrayList
的subList
結果不可強轉成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.sort
,Collections.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
只是遍歷了一次就把key
和value
都放到了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
)帶來的負面影響。【說明:有序性是指遍歷的結果是按某種比較規則依次排列的。穩定性指集合每次遍歷的元素次序是一定的。如:ArrayList
是order/unsort
;HashMap
是unorder/unsort
;TreeSet
是order/sort
。】 - 利用
Set
元素唯一的特性,可以快速對一個集合進行去重操作,避免使用List
的contains
方法進行遍歷、對比、去重操作
(六) 並發處理
- 獲取單例對象需要保證線程安全,其中的方法也要保證線程安全【說明:資源驅動類、工具類、單例工廠類都需要注意】
- 創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯
// 正例:自定義線程工廠,並且根據外部特征進行分組,比如機房信息
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)
FixedThreadPool
和SingleThreadPool
:
允許的請求隊列長度為Integer.MAX_VALUE
,可能會堆積大量的請求,從而導致OOM
。
2)
CachedThreadPool
:
允許的創建線程數量為Integer.MAX_VALUE
,可能會創建大量的線程,從而導致OOM
。
SimpleDateFormat
是線程不安全的類,一般不要定義為static
變量,如果定義為static
,必須加鎖。【說明:如果是JDK8的應用,可以使用Instant
代替Date
,LocalDateTime
代替Calendar
,DateTimeFormatter
代替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
對未加鎖的對象解鎖,它會調用AQS
的tryRelease
方法(取決於具體實現類),拋出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");
】 - 任何數據結構的構造或初始化,都應指定大小,避免數據結構無限增長吃光內存
- 及時清理不再使用的代碼段或配置信息【說明:對於垃圾代碼或過時配置,堅決清理干凈,避免程序過度臃腫,代碼冗余。正例:對於暫時被注釋掉,后續可能恢復使用的代碼片斷,在注釋代碼上方,統一規定使用三個斜杠(
///
)來說明注釋掉代碼的理由】