版本號 | 製作團隊 | 更新日期 | 備註 |
---|---|---|---|
1.0.0 | 阿里巴巴集團技術部 | 2016.12.7 | 首次向 Java 業界公開 |
一、編程規約
(一) 命名規約
1 【強制】所有編程相關命名均不能以下劃線或美元符號開始,也不能以下劃線或美元符號結束。
反例:
_name / __name / $Object / name_ / name$ / Object$
2 【強制】所有編程相關的命名嚴禁使用拼音與英文混合的方式,更不允許直接使用中文的方式。 說明:正確的英文拼寫和語法可以讓閱讀者易於理解,避免歧義。注意,即使純拼音命名方式 也要避免採用。
反例:
DaZhePromotion [打折] / getPingfenByName() [評分] / int 變量 = 3;
正例:
ali / alibaba / taobao / cainiao / aliyun / youku / hangzhou
等國際通用的名稱,可視爲英文。
3 【強制】類名使用 UpperCamelCase 風格,必須遵從駝峯形式,但以下情形例外:(領域模型 的相關命名)DO / DTO / VO / DAO 等。
正例:
MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion
反例:
macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion
4 【強制】方法名、參數名、成員變量、局部變量都統一使用 lowerCamelCase 風格,必須遵從駝峯形式。
正例:
localValue / getHttpMessage() / inputUserId
5 【強制】常量命名全部大寫,單詞間用下劃線隔開,力求語義表達完整清楚,不要嫌名字長。
正例:
MAX_STOCK_COUNT
反例:
MAX_COUNT
6 【強制】抽象類命名使用 Abstract 或 Base 開頭;異常類命名使用 Exception 結尾;測試類命 名以它要測試的類的名稱開始,以 Test 結尾。
7 【強制】中括號是數組類型的一部分,數組定義如下:String[] args;
反例:
請勿使用 String args[]的方式來定義
8 【強制】POJO 類中的任何布爾類型的變量,都不要加 is,否則部分框架解析會引起序列化錯誤。
反例:
定義爲基本數據類型 boolean isSuccess;的屬性,它的方法也是 isSuccess(),RPC框架在反向解析的時候,“以爲”對應的屬性名稱是 success,導致屬性獲取不到,進而拋出異常。
9 【強制】包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用單數形式,但是類名如果有複數含義,類名可以使用複數形式。
正例:
應用工具類包名爲com.alibaba.mpp.util、類名爲MessageUtils(此規則參考spring的框架結構)
10【強制】杜絕完全不規範的縮寫,避免望文不知義。
反例:
<某業務代碼>AbstractClass“縮寫”命名成AbsClass;condition“縮寫”命名成 condi,此類隨意縮寫嚴重降低了代碼的可閱讀性。
11【推薦】如果使用到了設計模式,建議在類名中體現出具體模式。 說明:將設計模式體現在名字中,有利於閱讀者快速理解架構設計思想。 正例:
public class OrderFactory;
public class LoginProxy;
public class ResourceObserver;
12【推薦】接口類中的方法和屬性不要加任何修飾符號(public 也不要加),保持代碼的簡潔性,並加上有效的 javadoc 註釋。儘量不要在接口裏定義變量,如果一定要定義變量,肯定是與接口方法相關,並且是整個應用的基礎常量。
正例:
接口方法簽名:void f();
接口基礎常量表示:String COMPANY = "alibaba";
反例:
接口方法定義:public abstract void f();
說明:JDK8 中接口允許有默認實現,那麼這個 default 方法,是對所有實現類都有價值的默認實現。
13.接口和實現類的命名有兩套規則:
1)【強制】對於 Service 和 DAO 類,基於 SOA 的理念,暴露出來的服務一定是接口,內部的實現類用 Impl 的後綴與接口區別。
正例:
CacheServiceImpl 實現 CacheService 接口。
2)【推薦】 如果是形容能力的接口名稱,取對應的形容詞做接口名(通常是–able 的形式)。
正例:
AbstractTranslator 實現 Translatable。
14.【參考】枚舉類名建議帶上Enum後綴,枚舉成員名稱需要全大寫,單詞間用下劃線隔開。說明:枚舉其實就是特殊的常量類,且構造方法被默認強制是私有。
正例:
枚舉名字:DealStatusEnum;
成員名稱:SUCCESS / UNKOWN_REASON。
15.【參考】各層命名規約:
A) Service/DAO 層方法命名規約
- 1) 獲取單個對象的方法用 get 做前綴。
- 2) 獲取多個對象的方法用 list 做前綴。
- 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);
2 【強制】long 或者 Long 初始賦值時,必須使用大寫的 L,不能是小寫的 l,小寫容易跟數字 1 混淆,造成誤解。說明:Long a = 2l; 寫的是數字的 21,還是 Long 型的 2?
3 【推薦】不要使用一個常量類維護所有常量,應該按常量功能進行歸類,分開維護。如:緩存 相關的常量放在類:CacheConsts 下;系統配置相關的常量放在類:ConfigConsts 下。 說明:大而全的常量類,非得 ctrl+f 才定位到修改的常量,不利於理解,也不利於維護。
4 【推薦】常量的複用層次有五層:跨應用共享常量、應用內共享常量、子工程內共享常量、包內共享常量、類內共享常量。
- 1) 跨應用共享常量:放置在二方庫中,通常是 client.jar 中的 const 目錄下。
- 2) 應用內共享常量:放置在一方庫的 modules 中的 const 目錄下。
反例:
易懂變量也要統一定義成應用內共享常量,兩位攻城師在兩個類中分別定義了表示 “是”的變量:
類 A 中:public static final String YES = "yes";
類 B 中:public static final String YES = "y";
A.YES.equals(B.YES),預期是 true,但實際返回爲 false,導致產生線上問題。
- 3) 子工程內部共享常量:即在當前子工程的 const 目錄下。
- 4) 包內共享常量:即在當前包下單獨的 const 目錄下。
- 5) 類內共享常量:直接在類內部 private static final 定義。
5 【推薦】如果變量值僅在一個範圍內變化用 Enum 類。如果還帶有名稱之外的延伸屬性,必須使用 Enum 類,下面正例中的數字就是延伸信息,表示星期幾。
正例:
public Enum{ MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5), SATURDAY(6), SUNDAY(7);}
(三) 格式規約
1 【強制】大括號的使用約定。如果是大括號內爲空,則簡潔地寫成{}即可,不需要換行;如果是非空代碼塊則:
- 1) 左大括號前不換行。
- 2) 左大括號後換行。
- 3) 右大括號前換行。
- 4) 右大括號後還有 else 等代碼則不換行;表示終止右大括號後必須換行。
2 【強制】 左括號和後一個字符之間不出現空格;同樣,右括號和前一個字符之間也不出現空格。詳見第 5 條下方正例提示。
3 【強制】if/for/while/switch/do 等保留字與左右括號之間都必須加空格。
4 【強制】任何運算符左右必須加一個空格。
說明:運算符包括賦值運算符=、邏輯運算符&&、加減乘除符號、三目運行符等。
5 【強制】代碼塊縮進 4 個空格,如果使用 tab 縮進,請設置成 1 個 tab 爲 4 個空格。 正例: (涉及 1-5 點)
public static void main(String args[]) {
// 縮進4個空格
String say = "hello";
// 運算符的左右必須有一個空格
int flag = 0;
// 關鍵詞 if 與括號之間必須有一個空格,括號內 f 與左括號,1 與右括號不需要空格
if (flag == 0) {
System.out.println(say);
}
// 左大括號前加空格且不換行;左大括號後換行
if (flag == 1) {
System.out.println("world");
// 右大括號前換行,右大括號後有 else,不用換行
} else {
System.out.println("ok");
// 右大括號做爲結束,必須換行
}
}
6 【強制】單行字符數限制不超過 120 個,超出需要換行,換行時,遵循如下原則:
- 1) 換行時相對上一行縮進 4 個空格。
- 2) 運算符與下文一起換行。
- 3) 方法調用的點符號與下文一起換行。
- 4) 在多個參數超長,逗號後進行換行。
- 5) 在括號前不要換行,見反例。
正例:
StringBuffer sb = new StringBuffer();
//超過 120 個字符的情況下,換行縮進 4 個空格,並且方法前的點符號一起換行
sb.append("zi").append("xin")...
.append("huang");
反例:
StringBuffer sb = new StringBuffer();
//超過 120 個字符的情況下,不要在括號前換行
sb.append("zi").append("xin")...append
("huang");
//參數很多的方法調用也超過 120 個字符,逗號後纔是換行處
method(args1, args2, args3, ...
, argsX);
7 【強制】方法參數在定義和傳入時,多個參數逗號後邊必須加空格。
正例:
下例中實參的"a",後邊必須要有一個空格。
method("a", "b", "c");
8 【推薦】沒有必要增加若干空格來使某一行的字符與上一行的相應字符對齊。
正例:
int a = 3;
long b = 4L;
float c = 5F;
StringBuffer sb = new StringBuffer();
說明:增加 sb 這個變量,如果需要對齊,則給 a、b、c 都要增加幾個空格,在變量比較多的 情況下,是一種累贅的事情。
9 【強制】IDE 的 text file encoding 設置爲 UTF-8; IDE 中文件的換行符使用 Unix 格式,不要使用 windows 格式。
10【推薦】方法體內的執行語句組、變量的定義語句組、不同的業務邏輯之間或者不同的語義之間插入一個空行。相同業務邏輯和語義之間不需要插入空行。 說明:沒有必要插入多行空格進行隔開。
(四) OOP 規約
1 【強制】避免通過一個類的對象引用訪問此類的靜態變量或靜態方法,無謂增加編譯器解析成本,直接用類名來訪問即可。
2 【強制】所有的覆寫方法,必須加@Override 註解。
反例:
getObject()與 get0bject()的問題。一個是字母的 O,一個是數字的 0,加@Override 可以準確判斷是否覆蓋成功。另外,如果在抽象類中對方法簽名進行修改,其實現類會馬上編譯報錯。
3 【強制】相同參數類型,相同業務含義,纔可以使用 Java 的可變參數,避免使用 Object。 說明:可變參數必須放置在參數列表的最後。(提倡同學們儘量不用可變參數編程)
正例:
public User getUsers(String type, Integer... 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 【強制】關於基本數據類型與包裝數據類型的使用標準如下:
- 1) 所有的 POJO 類屬性必須使用包裝數據類型。
- 2) RPC 方法的返回值和參數必須使用包裝數據類型。
- 3) 所有的局部變量推薦使用基本數據類型。
說明:POJO 類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何
NPE 問題,或者入庫檢查,都由使用者來保證。
正例:
數據庫的查詢結果可能是 null,因爲自動拆箱,用基本數據類型接收有 NPE 風險。
反例:
某業務的交易報表上顯示成交總額漲跌情況,即正負 x%,x 爲基本數據類型,調用的
RPC 服務,調用不成功時,返回的是默認值,頁面顯示:0%,這是不合理的,應該顯示成中劃 線-。所以包裝數據類型的 null 值,能夠表示額外的信息,如:遠程調用失敗,異常退出。
9【強制】定義 DO/DTO/VO 等 POJO 類時,不要設定任何屬性默認值。
反例:
某業務的 DO 的 gmtCreate 默認值爲 new Date();但是這個屬性在數據提取時並沒有置入具體值,在更新其它字段時又附帶更新了此字段,導致創建時間被修改成當前時間。
10【強制】序列化類新增屬性時,請不要修改 serialVersionUID 字段,避免反序列失敗;如果完全不兼容升級,避免反序列化混亂,那麼請修改 serialVersionUID 值。說明:注意 serialVersionUID 不一致會拋出序列化運行時異常。
11【強制】構造方法裏面禁止加入任何業務邏輯,如果有初始化邏輯,請放在 init 方法中。
12【強制】POJO 類必須寫 toString 方法。使用工具類 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);
14【推薦】當一個類有多個構造方法,或者多個同名方法,這些方法應該按順序放置在一起,便於閱讀。
15【推薦】 類內方法定義順序依次是:公有方法或保護方法 > 私有方法 > getter/setter 方 法。說明:公有方法是類的調用者和維護者最關心的方法,首屏展示最好;保護方法雖然只是子類關心,也可能是“模板設計模式”下的核心方法;而私有方法外部一般不需要特別關心,是一個黑盒實現;因爲方法信息價值較低,所有 Service 和 DAO 的 getter/setter 方法放在類體最 後。
16【推薦】setter 方法中,參數名稱與類成員變量名稱一致,this.成員名=參數名。在 getter/setter 方法中,儘量不要增加業務邏輯,增加排查問題難度。
反例:
public Integer getData(){
if(true) {
return data + 100;
} else {
return data - 100;
}
}
17.【推薦】循環體內,字符串的聯接方式,使用 StringBuilder 的 append 方法進行擴展。
反例:
String str = "start";
for(int i=0; i<100; i++){
str = str + "hello";
}
說明:反編譯出的字節碼文件顯示每次循環都會 new 出一個 StringBuilder 對象,然後進行 append 操作,最後通過 toString 方法返回 String 對象,造成內存資源浪費。
18【推薦】final 可提高程序響應效率,聲明成 final 的情況:
- 1) 不需要重新賦值的變量,包括類屬性、局部變量。
- 2) 對象參數前加 final,表示不允許修改引用的指向。
- 3) 類方法確定不允許被重寫。
19.【推薦】慎用 Object 的 clone 方法來拷貝對象。
說明:對象的 clone 方法默認是淺拷貝,若想實現深拷貝需要重寫 clone 方法實現屬性對象的拷貝。
20【推薦】類成員與方法訪問控制從嚴:
- 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 方法,或者一 個 public 的成員變量,刪除一下,不得手心冒點汗嗎?變量像自己的小孩,儘量在自己的視線內,變量作用域太大,如果無限制的到處跑,那麼你會擔心的。
(五) 集合處理
1 【強制】Map/Set 的 key 爲自定義對象時,必須重寫 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 無參方法存在問題,此方法返回值只能是 Object[]類,若強轉其它 類型數組將出現 ClassCastException 錯誤。
正例:
List<String> list = new ArrayList<String>(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);
說明:使用 toArray 帶參方法,入參分配的數組空間不夠大時,toArray 方法內部將重新分配內存空間,並返回新數組地址;如果數組元素大於實際所需,下標爲[ list.size() ]的數組 元素將被置爲 null,其它數組元素保持原值,因此最好將方法入參數組大小定義與集合元素 個數一致。
5 【強制】使用工具類Arrays.asList()把數組轉換成集合時,不能使用其修改集合相關的方法, 它的 add/remove/clear 方法會拋出 UnsupportedOperationException 異常。說明:asList 的返回對象是一個 Arrays 內部類,並沒有實現集合的修改方法。Arrays.asList 體現的是適配器模式,只是轉換接口,後臺的數據仍是數組。
String[] str = new String[] { "a", "b" };
List list = Arrays.asList(str);
第一種情況:list.add(“c”); 運行時異常。
第二種情況:str[0]= “gujin”; 那麼 list.get(0)也會隨之修改。
6 【強制】泛型通配符
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
if("1".equals(temp)){
a.remove(temp);
}
}
說明:這個例子的執行結果會出乎大家的意料,那麼試一下把“1”換成“2”,會是同樣的結果嗎?
正例:
Iterator<String> it = a.iterator();
while(it.hasNext()){
String temp = it.next();
if(刪除元素的條件){
it.remove();
}
}
8 【強制】在 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;
}
}
9 【推薦】集合初始化時,儘量指定集合初始值大小。說明:ArrayList 儘量使用 ArrayList(int initialCapacity) 初始化。
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 | 線程局部安全 |
TreeMap | 不允許爲 null | 允許爲 null | AbstractMap | 線程不安全 |
HashMap | 允許爲 null | 允許爲 null | AbstractMap | 線程不安全 |
反例:
很多同學認爲 ConcurrentHashMap 是可以置入 null 值。在批量翻譯場景中,子線程分發時,出現置入 null 值的情況,但主線程沒有捕獲到此異常,導致排查困難。
12.【參考】合理利用好集合的有序性(sort)和穩定性(order),避免集合的無序性(unsort)和不穩定性(unorder)帶來的負面影響。說明:穩定性指集合每次遍歷的元素次序是一定的。有序性是指遍歷的結果是按某種比較則依次排列的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort。
13.【參考】利用 Set 元素唯一的特性,可以快速對另一個集合進行去重操作,避免使用 List 的 contains 方法進行遍歷去重操作。
(六) 併發處理
1 【強制】獲取單例對象要線程安全。在單例對象裏面做操作也要保證線程安全。
說明:資源驅動類、工具類、單例工廠類都需要注意。
2 【強制】線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。
說明:使用線程池的好處是減少在創建和銷燬線程上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者 “過度切換”的問題。
3 【強制】SimpleDateFormat 是線程不安全的類,一般不要定義爲 static 變量,如果定義爲 static,必須加鎖,或者使用 DateUtils 工具類。
正例:
注意線程安全,使用 DateUtils。亦推薦如下處理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
說明:如果是 JDK8 的應用,可以使用 instant 代替 Date,Localdatetime 代替 Calendar,Datetimeformatter 代替 Simpledateformatter,官方給出的解釋:simple beautiful strong immutable thread-safe。
4 【強制】高併發時,同步調用應該去考量鎖的性能損耗。
- 能用無鎖數據結構,就不要用鎖;
- 能鎖區塊,就不要鎖整個方法體;
- 能用對象鎖,就不要用類鎖。
5 【強制】對多個資源、數據庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。
說明:線程一需要對錶 A、B、C 依次全部加鎖後纔可以進行更新操作,那麼線程二的加鎖順序 也必須是 A、B、C,否則可能出現死鎖。
6 【強制】併發修改同一記錄時,避免更新丟失,要麼在應用層加鎖,要麼在緩存加鎖,要麼在數據庫層使用樂觀鎖,使用 version 作爲更新依據。
說明:如果每次訪問衝突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於 3 次。
7 【強制】多線程並行處理定時任務時,Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,使用 ScheduledExecutorService 則沒有這個問題。
8 【強制】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors 各個方法的弊端:
- 1)newFixedThreadPool 和 newSingleThreadExecutor:
主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至 OOM。 - 2)newCachedThreadPool 和 newScheduledThreadPool:
主要問題是線程數最大數是 Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至 OOM。
9 【強制】創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。
正例:
public class TimerTaskThread extends Thread {
public TimerTaskThread(){
super.setName("TimerTaskThread");
...
}
10【推薦】使用 CountDownLatch 進行異步轉同步操作,每個線程退出前必須調用 countDown 方 法,線程執行代碼注意 catch 異常,確保 countDown 方法可以執行,避免主線程無法執行至 countDown 方法,直到超時才返回結果。
說明:注意,子線程拋出異常堆棧,不能在主線程 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 型(比如反例 中修改 helper 的屬性聲明爲 private volatile Helper helper = null;);
反例:
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
// other functions and members...
}
13【參考】volatile 解決多線程內存不可見問題。對於一寫多讀,是可以解決變量同步問題, 但是如果多寫,同樣無法解決線程安全問題。如果想取回 count++數據,使用如下類實現: AtomicInteger count = new AtomicInteger(); count.addAndGet(1); count++操作如果是JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。
14【參考】注意 HashMap 的擴容死鏈,導致 CPU 飆升的問題。
15.【參考】ThreadLocal 無法解決共享對象的更新問題,ThreadLocal 對象建議使用 static 修飾。 這個變量是針對一個線程內所有操作共有的,所以設置爲靜態變量,所有此類實例共享此靜態變量,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只要是這 個線程內定義的)都可以操控這個變量。
(七) 控制語句
1 【強制】在一個 switch 塊內,每個 case 要麼通過 break/return 來終止,要麼註釋說明程序 將繼續執行到哪一個 case 爲止;在一個 switch 塊內,都必須包含一個 default 語句並且放在最後,即使它什麼代碼也沒有。
2 【強制】在 if/else/for/while/do 語句中必須使用大括號,即使只有一行代碼,避免使用下 面的形式:
if (condition) statements;
3 【推薦】推薦儘量少用 else, if-else 的方式可以改寫成:
if(condition){
...
return obj;
}
// 接着寫 else 的業務邏輯代碼;
說明:如果使用要 if-else if-else 方式表達邏輯,【強制】請勿超過 3 層,超過請使用狀態設計模式。
4 【推薦】除常用方法(如 getXxx/isXxx)等外,不要在條件判斷中執行復雜的語句,以提高可讀性。
正例:
//僞代碼如下
InputStream stream = file.open(fileName, "w");
if (stream != null) {
...
}
反例:
if (file.open(fileName, "w") != null)) {
...
}
5【推薦】循環體中的語句要考量性能,以下操作儘量移至循環體外處理,如定義對象、變量、 獲取數據庫連接,進行不必要的 try-catch 操作(這個 try-catch 是否可以移至循環體外)。
6 【推薦】接口入參保護,這種場景常見的是用於做批量操作的接口。
7 【參考】方法中需要進行參數校驗的場景:
- 1) 調用頻次低的方法。
- 2) 執行時間開銷很大的方法,參數校驗時間幾乎可以忽略不計,但如果因爲參數錯誤導致中間執行回退,或者錯誤,那得不償失。
- 3) 需要極高穩定性和可用性的方法。
- 4) 對外提供的開放接口,不管是 RPC/API/HTTP 接口。
8 【參考】方法中不需要參數校驗的場景:
- 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 【強制】避免用 Apache Beanutils 進行屬性的 copy。
說明:Apache BeanUtils 性能較差,可以使用其他方案比如 Spring BeanUtils, Cglib BeanCopier。
3 【強制】velocity 調用 POJO 類的屬性時,建議直接使用屬性名取值即可,模板引擎會自動按規範調用 POJO 的 getXxx(),如果是 boolean 基本數據類型變量(注意,boolean 命名不需要 加 is 前綴),會自動調用 isXxx()方法。
說明:注意如果是 Boolean 包裝類對象,優先調用 getXxx()的方法。
4 【強制】後臺輸送給頁面的變量必須加$!{var}
——中間的感嘆號。
說明:如果 var=null 或者不存在,那麼${var}會直接顯示在頁面上。
5 【強制】注意 Math.random() 這個方法返回是 double 類型,注意取值範圍 0≤x<1(能夠取到零值,注意除零異常),如果想獲取整數類型的隨機數,不要將 x 放大 10 的若干倍然後取整,直接使用 Random 對象的 nextInt 或者 nextLong 方法。
6 【強制】獲取當前毫秒數:System.currentTimeMillis(); 而不是 new Date().getTime();
說明:如果想獲取更加精確的納秒級時間值,用 System.nanoTime。在 JDK8 中,針對統計時間等場景,推薦使用 Instant 類。
7 【推薦】儘量不要在 vm 中加入變量聲明、邏輯運算符,更不要在 vm 模板中加入任何複雜的邏輯。
8 【推薦】任何數據結構的使用都應限制大小。
說明:這點很難完全做到,但很多次的故障都是因爲數據結構自增長,結果造成內存被吃光。
9 【推薦】對於“明確停止使用的代碼和配置”,如方法、變量、類、配置文件、動態配置屬性等要堅決從程序中清理出去,避免造成過多垃圾。清理這類垃圾代碼是技術氣場,不要有這樣的觀念:“不做不錯,多做多錯”。
個人博客: http://www.shengyang.me