傳送門
前言
阿里Java開發手冊談不上圣經,但確實是大量程序員踩坑踩出來的一部非常有價值的寶典。其從代碼規范性、性能、健壯性、安全性等方面出發,對程序員提出了一系列簡單直觀的要求,對于人員流動性強,程序員技術水平參差不齊的團隊來說,尤其具備價值。
阿里Java開發手冊中,有一部分規約是針對阿里自己的工程環境特點設置的,其他團隊可以用于借鑒,無需照搬,而大部分的規約,都是具備推廣價值的。
然而這本手冊中的規約眾多,部分搭配了簡短的說明,相當一部分規約則對原理說明的不夠詳細。本著“知道為什么要這樣做”強于“知道應該這樣做”的思想,本文在列出阿里Java開發手冊的同時,對其中部分語焉不詳的規約進行了比較詳細的說明,并盡可能搭配代碼樣例。
本文覆蓋阿里Java開發手冊中的前兩章,即編程規約和異常日志兩章,后三章MySQL規約、工程規約、安全規約不列入主要有兩個考慮,一是這三章的內容與Java不緊密相關,二是這三章中除MySQL之外的規約與阿里現行的技術架構捆綁的比較緊,普適性較低。
本文中,在阿里Java開發手冊基礎上增加的說明內容全部以引用的形式出現,即
引用部分的文字是本文作者對阿里Java規約的附加說明
一、編程規約
(一) 命名規約
本節中的規約非常簡單直白,故基本不做特殊說明
【強制】所有編程相關命名均不能以下劃線或美元符號開始,也不能以下劃線或美元符號結束。
反例: _name / __name / $Object / name_ / name$ / Object$【強制】所有編程相關的命名嚴禁使用拼音與英文混合的方式,更不允許直接使用中文的方式。
說明:正確的英文拼寫和語法可以讓閱讀者易于理解,避免歧義。注意,即使純拼音命名方式也要避免采用。
反例: DaZhePromotion [打折] / getPingfenByName() [評分] / int 變量 = 3;
正例: ali / alibaba / taobao / cainiao / aliyun / youku / hangzhou 等國際通用的名稱,可視為英文。【強制】類名使用UpperCamelCase風格,必須遵從駝峰形式,但以下情形例外:(領域模型的相關命名)DO / DTO / VO / DAO等。
正例:MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion
反例:macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion【強制】方法名、參數名、成員變量、局部變量都統一使用lowerCamelCase風格,必須遵從駝峰形式。
正例: localValue / getHttpMessage() / inputUserId【強制】常量命名全部大寫,單詞間用下劃線隔開,力求語義表達完整清楚,不要嫌名字長。
正例: MAX_STOCK_COUNT 反例: MAX_COUNT【強制】抽象類命名使用Abstract或Base開頭;異常類命名使用Exception結尾;測試類命名以它要測試的類的名稱開始,以Test結尾。
【強制】中括號是數組類型的一部分,數組定義如下:String[] args;
反例:請勿使用String args[]的方式來定義【強制】POJO類中的任何布爾類型的變量,都不要加is,否則部分框架解析會引起序列化錯誤。
反例:定義為基本數據類型boolean isSuccess;的屬性,它的方法也是isSuccess(),RPC框架在反向解析的時候,“以為”對應的屬性名稱是success,導致屬性獲取不到,進而拋出異常。【強制】包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用單數形式,但是類名如果有復數含義,類名可以使用復數形式。
正例: 應用工具類包名為com.alibaba.mpp.util、類名為MessageUtils(此規則參考spring的框架結構)【強制】杜絕完全不規范的縮寫,避免望文不知義。
反例:<某業務代碼>AbstractClass“縮寫”命名成AbsClass;condition“縮寫”命名成 condi,此類隨意縮寫嚴重降低了代碼的可閱讀性。【推薦】如果使用到了設計模式,建議在類名中體現出具體模式。
說明:將設計模式體現在名字中,有利于閱讀者快速理解架構設計思想。
正例:public class OrderFactory; public class LoginProxy; public class ResourceObserver;【推薦】接口類中的方法和屬性不要加任何修飾符號(public 也不要加),保持代碼的簡潔性,并加上有效的javadoc注釋。盡量不要在接口里定義變量,如果一定要定義變量,肯定是與接口方法相關,并且是整個應用的基礎常量。
正例:接口方法簽名:void f(); 接口基礎常量表示:String COMPANY = "alibaba";
反例:接口方法定義:public abstract void f();
說明:JDK8中接口允許有默認實現,那么這個default方法,是對所有實現類都有價值的默認實現。
最后這一句實際上隱含了另一條規約,即如果要對接口定義default方法(JDK8的新特性),那么要確保這個default方法是對該接口的所有實現類都有價值的。
如果某公共方法只針對該接口的一部分實現類有價值,那么應該定義一個抽象類實現該接口,在抽象類中定義該公共方法,需要用到該公共方法的實現類繼承此抽象類并實現此接口。
接口和實現類的命名有兩套規則:
1)【強制】對于Service和DAO類,基于SOA的理念,暴露出來的服務一定是接口,內部的實現類用Impl的后綴與接口區別。
正例:CacheServiceImpl實現CacheService接口。
2)【推薦】 如果是形容能力的接口名稱,取對應的形容詞做接口名(通常是–able的形式)。
正例:AbstractTranslator實現 Translatable。【參考】枚舉類名建議帶上Enum后綴,枚舉成員名稱需要全大寫,單詞間用下劃線隔開。
說明:枚舉其實就是特殊的常量類,且構造方法被默認強制是私有。
正例:枚舉名字:DealStatusEnum;成員名稱:SUCCESS / UNKOWN_REASON。【參考】各層命名規約:
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。
(二) 常量定義
- 【強制】不允許出現任何魔法值(即未經定義的常量)直接出現在代碼中。
反例:
String key = "Id#taobao_" + tradeId;
cache.put(key, value);
“魔法值”的意思是指“未經定義直接出現在代碼中的常量值”。上例中的"Id#taobao"就是魔法值。所以這條規約用大家聽得懂的大白話翻譯過來就是:“不允許在代碼中直接使用未經定義的常量字面量,所有在代碼中使用的常量必須預先經過定義”,如:
public static final String CACHE_PREFIX = "Id#taobao_"; String key = CACHE_PREFIX + tradeId; cache.put(key, value);
吐槽:阿里自己的規范里白紙黑字寫著“任何運算符左右必須加一個空格”,結果上面這段代碼樣例里的+運算符左右都沒有加空格。是強迫癥發作的作者給補上的……
【強制】long或者Long初始賦值時,必須使用大寫的L,不能是小寫的l,小寫容易跟數字1混淆,造成誤解。
說明:Long a = 2l; 寫的是數字的21,還是Long型的2?【推薦】不要使用一個常量類維護所有常量,應該按常量功能進行歸類,分開維護。如:緩存相關的常量放在類:CacheConsts下;系統配置相關的常量放在類:ConfigConsts下。
說明:大而全的常量類,非得ctrl+f才定位到修改的常量,不利于理解,也不利于維護。【推薦】常量的復用層次有五層:跨應用共享常量、應用內共享常量、子工程內共享常量、包內共享常量、類內共享常量。
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定義。
此規約中提到的“一方庫”、“二方庫”可能會引發一些疑惑。一方庫是指同一工程內的模塊打包成的庫;二方庫是指公司內的公共庫;三方庫就不用說了吧
- 【推薦】如果變量值僅在一個范圍內變化用Enum類。如果還帶有名稱之外的延伸屬性,必須使用Enum類,下面正例中的數字就是延伸信息,表示星期幾。
正例:
public Enum {MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5), SATURDAY(6), SUNDAY(7);}
吐槽:謎之代碼,神奇的意會式寫法,這給的代碼樣例真的就是“懂的人不看就懂,不懂的人越看越糊涂”,請還糊涂著的讀者看下面的代碼樣例吧:
public enum WeekDay { MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5), SATURDAY(6), SUNDAY(7); private WeekDay(int dayNum) { this.dayNum = dayNum; } private int dayNum; public int getDayNum() { return this.dayNum; } public static void main(String[] args) { WeekDay weekDay = FRIDAY; System.out.println(weekDay.toString() + weekDay.getDayNum()); } }
(三) 格式規約
關于Java代碼的格式,其實并沒有嚴格的對錯之分,if的大括號結束后else是在同行還是換行就能吵上一陣,這一章節完全是出于代碼的整潔性考慮,所以參考即可
【強制】大括號的使用約定。如果是大括號內為空,則簡潔地寫成{}即可,不需要換行;如果是非空代碼塊則:
1) 左大括號前不換行。
2) 左大括號后換行。
3) 右大括號前換行。
4) 右大括號后還有else等代碼則不換行;表示終止右大括號后必須換行。【強制】 左括號和后一個字符之間不出現空格;同樣,右括號和前一個字符之間也不出現空格。詳見第5條下方正例提示。
【強制】if/for/while/switch/do等保留字與左右括號之間都必須加空格。
【強制】任何運算符左右必須加一個空格。
說明:運算符包括賦值運算符=、邏輯運算符&&、加減乘除符號、三目運行符等。【強制】代碼塊縮進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");
// 右大括號做為結束,必須換行
}
}
- 【強制】單行字符數限制不超過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);
【強制】方法參數在定義和傳入時,多個參數逗號后邊必須加空格。
正例:下例中實參的"a",后邊必須要有一個空格。 method("a", "b", "c");【推薦】沒有必要增加若干空格來使某一行的字符與上一行的相應字符對齊。
正例:
int a = 3;
long b = 4L;
float c = 5F;
StringBuffer sb = new StringBuffer();
說明:增加sb這個變量,如果需要對齊,則給a、b、c都要增加幾個空格,在變量比較多的情況下,是一種累贅的事情。
- 【強制】IDE的text file encoding設置為UTF-8; IDE中文件的換行符使用Unix格式,不要使用windows格式。
這一規約中的兩項要求都是為了在各種平臺上提供統一的代碼閱讀體驗
- 【推薦】方法體內的執行語句組、變量的定義語句組、不同的業務邏輯之間或者不同的語義之間插入一個空行。相同業務邏輯和語義之間不需要插入空行。
說明:沒有必要插入多行空格進行隔開。
(四) OOP規約
此章節的分類其實比較奇怪,其中有一些規約其實和OOP沒啥關系,大家就逐條分開看吧
- 【強制】避免通過一個類的對象引用訪問此類的靜態變量或靜態方法,無謂增加編譯器解析成本,直接用類名來訪問即可。
也就是說,應該:
StringUtils.isEmpty(str);
而不要:
StringUtils stringUtils = new StringUtils(); stringUtils.isEmpty(str);
直觀的好處是少一些代碼,少創建一些對象。
原文中所說的“增加編譯器解析成本”我不太理解,不知是指節省了把java代碼編譯成字節碼時的成本,還是把字節碼JIT編譯成機器碼時的成本。
- 【強制】所有的覆寫方法,必須加@Override注解。
反例:getObject()與get0bject()的問題。一個是字母的O,一個是數字的0,加@Override可以準確判斷是否覆蓋成功。另外,如果在抽象類中對方法簽名進行修改,其實現類會馬上編譯報錯。
@Override注解本身其實不起任何作用,但使用它可以做到兩件很有用的事:
- 告訴代碼的閱讀者,這個方法是在覆蓋父類的方法
- 編寫程序時,如果方法名、參數列表、異常等定義錯誤導致不能正確覆蓋父類方法時,編譯器會提示錯誤
- 【強制】相同參數類型,相同業務含義,才可以使用Java的可變參數,避免使用Object。
說明:可變參數必須放置在參數列表的最后。(提倡同學們盡量不用可變參數編程)
正例:
public User getUsers(String type, Integer... ids);
可變參數是J2SE 1.5的新特性,用起來很方便,但在工程中使用不當會嚴重影響代碼的可讀性和可維護性。所以對于龐大的、不好管控的開發團隊來說,直接限制使用或許也是一種方法
- 【強制】對外暴露的接口簽名,原則上不允許修改方法簽名,避免對接口調用方產生影響。接口過時必須加@Deprecated注解,并清晰地說明采用的新接口或者新服務是什么。
避免修改方法簽名是因為你不清楚都有誰在什么場景下調用此方法,如果貿然修改方法簽名,可能會引發問題
【強制】不能使用過時的類或方法。
說明:java.net.URLDecoder 中的方法decode(String encodeStr) 這個方法已經過時,應該使用雙參數decode(String source, String encode)。接口提供方既然明確是過時接口,那么有義務同時提供新的接口;作為調用方來說,有義務去考證過時方法的新實現是什么。【強制】Object的equals方法容易拋空指針異常,應使用常量或確定有值的對象來調用equals。
正例: "test".equals(object);
反例: object.equals("test");
說明:推薦使用java.util.Objects#equals (JDK7引入的工具類)
通過常量來調用equals方法這一條大家應該都清楚。然而如果要比較的兩個都是對象的話,似乎把哪一個放在前面都可能發生空指針,對此,使用J2SE 1.7中引入的Objects類是最佳的:
if(Objects.equals(a, b)) { //... }
這樣的話,無論如何都不會產生空指針異常,需要注意的是,如果比較的兩個對象都為null的話,Objects.equals方法會返回true
- 【強制】所有的相同類型的包裝類對象之間值的比較,全部使用equals方法比較。
說明:對于Integer var=?在-128至127之間的賦值,Integer對象是在IntegerCache.cache產生,會復用已有對象,這個區間內的Integer值可以直接使用==進行判斷,但是這個區間之外的所有數據,都會在堆上產生,并不會復用已有對象,這是一個大坑,推薦使用equals方法進行判斷。
包裝類是指Byte/Boolean/Short/Integer/Long/Float/Double/Character等對Java基本類型進行封裝的類。在使用包裝類時,很容易把使用基本類型時的習慣帶入,引用原文的說法,這是一個大坑。看下面的例子:
int a = 1000; int b = 1000; if(a == b) //true Integer b1 = 1000; if(a == b1) //true Integer a1 = 1000; if(a1 == b1) //false Integer a2 = 10; Integer b2 = 10; if(a2 == b2) //true
對于不了解包裝類機制的程序員來說 ,上面這段代碼會造成不小的困惑。簡單解釋一下:
當包裝類對象和基本類型變量進行==判斷時,Java會把包裝類對象轉換成基本類型變量后進行比較,這和兩個基本類型變量進行==操作是等價的
當兩個包裝類對象進行==判斷時,Java比較的是兩個對象的內存地址是否一致,即判斷這兩個變量引用的對象是否是同一個,所以上例中的a1 == b1結果是false
那么為什么a2 == b2結果是true呢?因為包裝類Integer維護了一個對象緩沖池,池中有256個Integer對象,體現的是-128~127之間的數值。所以說a2和b2實際上都指向了緩沖池中的同一個對象,a2 == b2自然就是true了
包裝類Byte/Short/Integer/Long/Character中都有這樣的緩沖池機制,所以當我們對這些包裝類進行比較時,使用equals方法是最穩妥的
- 【強制】關于基本數據類型與包裝數據類型的使用標準如下:
1) 所有的POJO類屬性必須使用包裝數據類型。
2) RPC方法的返回值和參數必須使用包裝數據類型。
3) 所有的局部變量推薦使用基本數據類型。
說明:POJO類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何NPE問題,或者入庫檢查,都由使用者來保證。
正例:數據庫的查詢結果可能是null,因為自動拆箱,用基本數據類型接收有NPE風險。
反例:某業務的交易報表上顯示成交總額漲跌情況,即正負x%,x為基本數據類型,調用的RPC服務,調用不成功時,返回的是默認值,頁面顯示:0%,這是不合理的,應該顯示成中劃線-。所以包裝數據類型的null值,能夠表示額外的信息,如:遠程調用失敗,異常退出。
一句話提示:基本數據類型變量是不能賦值為null的,但包裝類對象可以
【強制】定義DO/DTO/VO等POJO類時,不要設定任何屬性默認值。
反例:某業務的DO的gmtCreate默認值為new Date();但是這個屬性在數據提取時并沒有置入具體值,在更新其它字段時又附帶更新了此字段,導致創建時間被修改成當前時間。【強制】序列化類新增屬性時,請不要修改serialVersionUID字段,避免反序列失敗;如果完全不兼容升級,避免反序列化混亂,那么請修改serialVersionUID值。
說明:注意serialVersionUID不一致會拋出序列化運行時異常。
在進行反序列化時,JVM會判斷待反序列化的字節流中的serialVersionUID和對應類的serialVersionUID是否一致,如果不一致,會產生InvalidCastException。
- 【強制】構造方法里面禁止加入任何業務邏輯,如果有初始化邏輯,請放在init方法中。
在你向構造方法里加入業務邏輯時,你是不可能確保這段業務邏輯能夠覆蓋今后所有的需求的,萬一哪一天出現“某場景下實例化此類時不可以執行這段業務邏輯”的情況,那就悲催了,只能重載一個構造方法。
【強制】POJO類必須寫toString方法。使用工具類source > generate toString時,如果繼承了另一個POJO類,注意在前面加一下super.toString。
說明:在方法執行拋出異常時,可以直接調用POJO的toString()方法打印其屬性值,便于排查問題。【推薦】使用索引訪問用String的split方法得到的數組時,需做最后一個分隔符后有無內容的檢查,否則會有拋IndexOutOfBoundsException的風險。
說明:
String str = "a,b,c,,";
String[] ary = str.split(","); //預期大于3,結果是3
System.out.println(ary.length);
String類的split(String regex)方法會丟棄最后一個分隔符后的空串
【推薦】當一個類有多個構造方法,或者多個同名方法,這些方法應該按順序放置在一起,便于閱讀。
【推薦】 類內方法定義順序依次是:公有方法或保護方法 > 私有方法 > getter/setter方法。
說明:公有方法是類的調用者和維護者最關心的方法,首屏展示最好;保護方法雖然只是子類關心,也可能是“模板設計模式”下的核心方法;而私有方法外部一般不需要特別關心,是一個黑盒實現;因為方法信息價值較低,所有Service和DAO的getter/setter方法放在類體最后。【推薦】setter方法中,參數名稱與類成員變量名稱一致,this.成員名=參數名。在getter/setter方法中,盡量不要增加業務邏輯,增加排查問題難度。
反例:
public Integer getData() {
if(true) {
return data + 100;
} else {
return data - 100;
}
}
- 【推薦】循環體內,字符串的聯接方式,使用StringBuilder的append方法進行擴展。
反例:
String str = "start";
for(int i = 0; i < 100; i++) {
str = str + "hello";
}
說明:反編譯出的字節碼文件顯示每次循環都會new出一個StringBuilder對象,然后進行append操作,最后通過toString方法返回String對象,造成內存資源浪費。
使用"+"拼接字符串的代碼,在編譯成字節碼時會被編譯器自動轉換為使用StringBuilder的append方法拼接,所以說單行代碼中使用"+"拼接字符串是沒有問題的,但如果這樣的拼接頻繁出現(不僅限于循環中),那么每次拼接時都會new一個新的StringBuffer對象,對性能產生很大影響。
所以,上面那段代碼應該寫成:StringBuilder sb = new StringBuilder("start"); for(int i = 0; i < 100; i++) { sb.append("hello"); }
吐槽:代碼樣例里操作符兩邊又不加空格,寫這個規范的大哥,真的沒問題嗎……
- 【推薦】下列情況,聲明成final會更有提示性:
1) 不需要重新賦值的變量,包括類屬性、局部變量。
2) 對象參數前加final,表示不允許修改引用的指向。
3) 類方法確定不允許被重寫。
這一條規約多廢話幾句,在前一版本的阿里Java開發手冊中,這一條規約是:“【推薦】final可提高程序響應效率,聲明成final的情況:”
這一說法是有問題的,在被吐槽之后,阿里也更新了此條規約。這里來解釋一下為什么前一版里的說法有問題:final修飾變量時對于性能是沒有任何影響的。final修飾編譯時常量時,會帶來些許的性能提升。如:
static int foo() {
int a = 2; // 聲明常量a
int b = 3; // 聲明常量b
return a + b; // 常量表達式
}
編譯成字節碼后是:
iconst_2
istore_0 // 設置a的值
iconst_3
istore_1 // 設置b的值
iload_0 // 讀取a的值
iload_1 // 讀取b的值
iadd
ireturn
而如果改成: ```java static int foo() {
final int a = 2; // 聲明常量a
final int b = 3; // 聲明常量b
return a + b; // 常量表達式
}
編譯成字節碼后則是:
iconst_5 // 常量折疊了,沒有“訪問局部變量”
ireturn
而final修飾的方法,則會在編譯期省略掉方法調用,直接把被調用的方法內容編譯過去,例如: ```java
public void foo() {
for(int i = 0; i < 1000; i++) {
bar();
}
}
private void bar() {
System.out.println("bar");
}
在編譯后等價于 ```java
public void foo() {
for(int i = 0; i < 1000; i++) {
System.out.println("bar");
}
}
然而,無論是哪種情況下final帶來的性能提升都是很小的,而且如果使用的是比較新的JVM,在經過JIT編譯后的機器碼也會屏蔽掉加不加final的性能區別。所以說final的主要價值還是在于其語義本身提供的不可變性檢查特性,從提升性能的角度來制定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。
說明:任何類、方法、參數、變量,嚴控訪問范圍。過寬泛的訪問范圍,不利于模塊解耦。
思考:如果是一個private的方法,想刪除就刪除,可是一個public的Service方法,或者一個public的成員變量,刪除一下,不得手心冒點汗嗎?變量像自己的小孩,盡量在自己的視線內,變量作用域太大,如果無限制的到處跑,那么你會擔心的。
(五) 集合處理
- 【強制】Map/Set的key為自定義對象時,必須重寫hashCode和equals。
正例:String重寫了hashCode和equals方法,所以我們可以非常愉快地使用String對象作為key來使用。
除了IdentityHashMap之外,其余的Map和Set在判斷key是否已存在時,使用的是作為key的對象的hashCode和equals方法。
HashMap的get(Object)方法源碼節選:
//如果入參key的hash和遍歷到的k的hash一致,且key.equals(k),則匹配成功
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
如果作為key的類沒有重寫hashCode和equals方法,則此處使用的就是Object類的hashCode和equals方法: ```java
public native int hashCode();
```java
public boolean equals(Object obj) {
return (this == obj);
}
Object類的hashCode是個native方法,不同的對象的hashCode一定不同,而Object類的equals方法則等同于==運算符,只要兩個對象內存地址不同,就一定會返回false 所以,如果我們用作key的類沒有重寫hashCode和equals方法,那么兩個屬性完全一樣的對象,會被Map/Set認為是不同的兩個key。見下面的代碼樣例: ```java
public class Test {
//沒有重寫hashCode和equals的Foo類
public static class Foo {
public Foo(int a) {
this.a = a;
}
public int a;
}
public static void main(String[] args) {
//兩個一樣的Foo對象f1和f2
Foo f1 = new Foo(1);
Foo f2 = new Foo(1);
Map<Foo, String> map = new HashMap<Foo, String>();
map.put(f1, "f1");
map.put(f2, "f2");
System.out.println(map.size()); //map的size為2,與預期不一致
String s = map.get(new Foo(1)); //嘗試通過key獲取value
System.out.println(s); //返回null,與預期不一致
}
}
而使用重寫了hashCode和equals方法的String類作為key時,Map的運作就正常了: ```java
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2); //false,s1和s2是兩個不同的對象
Map<String, String> map = new HashMap<String, String>();
map.put(s1, "s1");
map.put(s2, "s2");
System.out.println(map.size()); //map的size為1
String s = map.get(new String("abc"));
System.out.println(s); //返回"s2",與預期一致
}
在重寫hashCode與equals方法時,應確保: - 如果k1和k2體現的數據是一樣的,那么k1.equals(k2) - 如果k1.equals(k2),那么k2.equals(k1) - 如果k1.equals(k2),那么k1.hashCode() == k2.hashCode()
【強制】ArrayList的subList結果不可強轉成ArrayList,否則會拋出ClassCastException異常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList ;
說明:subList 返回的是 ArrayList 的內部類 SubList,并不是 ArrayList ,而是 ArrayList 的一個視圖,對于SubList子列表的所有操作最終會反映到原列表上。【強制】在subList場景中,高度注意對原集合元素個數的修改,會導致子列表的遍歷、增加、刪除均產生ConcurrentModificationException 異常。
ConcurrentModificationException(后文簡稱CME)是一個經常讓初級Java程序員產生困惑的異常。
簡單來說,Java的很多集合類都不是線程安全的(如ArrayList/LinkedList/HashMap等等),這些非線程安全的集合類如果存在并發訪問,則會引發一系列的問題(如數據異常甚至死循環)。然而Java無法在編譯期識別出對非線程安全集合的并發訪問行為,所以只能在運行期加上CME這樣一個保險措施。
CME會在使用iterator遍歷集合類時產生,非線程安全的集合類的iterator會在遍歷時隨時檢查集合對象是否在遍歷開始后發生過變更,如果檢測到變更,則會立刻拋出CME終止遍歷。這樣做是因為此時的集合對象很可能已經存在并發訪問造成的數據混亂,再繼續遍歷下去可能會出現嚴重問題。遵循這種模式的iterator被稱為fail-fast iterator。
在單線程環境下也有可能產生CME,如在遍歷集合時在循環體內對集合進行增刪操作。這是CME的一個副作用,規避方法在后面的第7條規約中有描述。
Hashtable和Vector是特例,這兩個古早的集合類雖然是synchronized的,但仍然使用了fail-fast iterator,在遍歷時發生并發訪問一樣會產生CME,除非在遍歷時對對象加鎖。
- 【強制】使用集合轉數組的方法,必須使用集合的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,其它數組元素保持原值,因此最好將方法入參數組大小定義與集合元素個數一致。
- 【強制】使用工具類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)也會隨之修改。
- 【強制】泛型通配符<? extends T>來接收返回的數據,此寫法的泛型集合不能使用add方法。
說明:蘋果裝箱后返回一個<? extends Fruits>對象,此對象就不能往里加任何水果,包括蘋果。
使用泛型通配符聲明的集合對象是不能以任何方式裝入對象的,不僅限于add方法。因為在編譯期無法確定該集合對象內存儲的對象類型。這一規約受編譯器檢查,所以也不用特別專門去注意。
- 【強制】不要在foreach循環里進行元素的remove/add操作。remove元素請使用Iterator方式,如果并發操作,需要對Iterator對象加鎖。
反例:
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();
}
}
這條規約給出的反例很“刁鉆”,又沒有給出解釋說明,非常容易引發困惑。
首先要明確的是,反例中的代碼寫法是錯誤的,其運行結果符合預期的原因僅僅是巧合。無論任何情況下,都不應該在使用增強for循環或iterator遍歷集合時,使用集合類的remove()方法刪除元素。
如筆者在第3條中所述,如果集合類使用了fail-fast的iterator,那么在iterator遍歷時會檢查集合對象是否在遍歷開始后發生過變更,如果發生過變更,便會拋出CME異常。使用fail-fast iterator的集合類都內置了一個計數器modCount,用來記錄該集合對象發生過變更的次數,每次對集合對象執行寫操作(增刪改)時,modCount都會自增1。
iterator實例化時,會記錄下當時集合對象的modCount:int expectedModCount = modCount;
接下來在每一次調用next()方法時都要比較modCount和expectedModCount,如果不相等,則拋出CME異常:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
正確的做法是使用iterator自身提供的remove()方法來移除元素,就如上文正例中的代碼。這是因為iterator自身的remove()方法會同時也修改expectedModCount。 那么,為什么反例中給出的代碼不會拋出CME?為什么把"1"換成"2"之后又會拋出CME了? 是因為增強for循環中,iterator在調用next()方法前會先調用hasNext()方法,只有hasNext()返回true時才會繼續調用next()。 ```java
public boolean hasNext() {
//如果當前遍歷的index不等于集合size,則返回true
return cursor != size;
}
在本條規約給出的反例中,list的初始size是2,第1次循環中cursor為0,刪除了"1"后,size變成了1,此時進入第2次循環,cursor為1,size也為1,于是hasNext()返回了false,循環壓根就沒有遍歷到第2個元素就提前結束了。所以說,這是一個巧合,只會在集合內有2個元素,且第1次遍歷時便刪除了1個元素的情況下發生。
- 【強制】在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;
}
}
JDK6及以下的版本,Arrays.sort和Collections.sort使用的是歸并排序算法,而從JDK7開始就改為使用TimSort算法了,TimSort是歸并排序的優化版,其對Comparator的自反性和傳遞性要求更加嚴格
- 【推薦】集合初始化時,盡量指定集合初始值大小。
說明:ArrayList盡量使用ArrayList(int initialCapacity) 初始化。
大部分集合類都有一個默認的初始大小(鏈表型的除外)。以ArrayList為例,默認初始大小是10,當向其中插入第11個元素時,ArrayList會自動擴容,擴到當前size的1.5倍。擴容動作會消耗一定的性能和內存空間(例如ArrayList在擴容時要拷貝數組,HashMap在擴容時要重排所有Entry的位置),所以應盡可能減少不必要的擴容操作。
假設我們要向一個ArrayList中存儲100條數據,如果使用new ArrayList()實例化,那么存儲的過程中ArrayList需要擴容6次。如果new ArrayList(100),那么就一次擴容都不需要了。
如果我們在實例化集合時就能夠估計出所需的空間,那么應在實例化時指定一個合適的大小,避免頻繁擴容。
【推薦】使用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值的情況,如下表格:
圖片.png
反例:很多同學認為ConcurrentHashMap是可以置入null值,而事實上,存儲null值時會拋出NPE異常。
【參考】合理利用好集合的有序性(sort)和穩定性(order),避免集合的無序性(unsort)和不穩定性(unorder)帶來的負面影響。
說明:有序性是指遍歷的結果是按某種比較規則依次排列的。穩定性指集合每次遍歷的元素次序是一定的。如:ArrayList是order/unsort;HashMap是unorder/unsort;TreeSet是order/sort。
要熟知每種集合類的特性,請參考我的文章:JAVA集合框架中的常用集合及其特點、適用場景、實現原理簡介
- 【參考】利用Set元素唯一的特性,可以快速對一個集合進行去重操作,避免使用List的contains方法進行遍歷、對比、去重操作。
(六) 并發處理
- 【強制】獲取單例對象需要保證線程安全,其中的方法也要保證線程安全。
說明:資源驅動類、工具類、單例工廠類都需要注意。
可能因為偷懶沒有給出正、反例,補充一下。
一個完全沒有考慮線程安全的單例類:
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
多線程環境下,這個單例就一點也不單了 推薦的單例實現方式有兩種:靜態內部類和枚舉,這兩種寫法都能夠保證線程安全和懶加載,且性能夠好。具體寫法網上都能找到,這里給出比較容易理解的一種,靜態內部類: ```java
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
- 【強制】創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。
正例:
public class TimerTaskThread extends Thread {
public TimerTaskThread() {
super.setName("TimerTaskThread"); ...
}
}
- 【強制】線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。
說明:使用線程池的好處是減少在創建和銷毀線程上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題。
手工創建線程時需要做嚴格檢查才能保證不超量創建線程,生手或稍有不慎就容易出簍子,所以用線程池還是最保險的
- 【強制】線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫同學更加明確線程池運行規則,避資源耗盡風險。
說明: Executors返回的線程池對象的弊端如下:
1)FixedThreadPool 和 SingleThreadPoolPool:
允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允許的創建線程數量為 Integer.MAX_VALUE,可能會創建大量的線程,從而導致OOM。
和上一條規約類似,Executors創建的線程池用不好容易出事,還是保險一點好
- 【強制】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。
SimpleDateFormat不是線程安全的,所以只能作為局部變量使用,但這樣做會頻繁實例化SimpleDateFormat對象,產生不少不必要的垃圾對象。
規約中給出的使用ThreadLocal的方法是一個好辦法,讓每個線程有一個自己專屬的SimpleDateFormat對象,這樣能夠避免多線程訪問同一個對象,同時也避免了頻繁創建對象。當然,使用commons-lang3的DateUtils類更方便。
這里還要推薦一下joda-time庫,可以用來替代Date/Calendar/SimpleDateFormat等,非常好用。https://github.com/JodaOrg/joda-time
- 【強制】高并發時,同步調用應該去考量鎖的性能損耗。能用無鎖數據結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖。
synchronized方法:該方法執行期間在對象上加鎖。但很多情況下一個方法體內不是所有的代碼都需要加鎖,如果改為鎖代碼塊,就能大幅提升并發性能。簡單地說,synchronized范圍越小,性能提升越大。
此外,ReentrantLock的表現也要優于synchronized,重入鎖可以更加靈活的控制鎖的獲取和釋放,同時也能用來實現讀寫鎖(允許多個線程同時讀,但只要有一個線程在寫,那么所有其他線程的讀寫請求都會被阻塞)
【強制】對多個資源、數據庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。 說明:線程一需要對表A、B、C依次全部加鎖后才可以進行更新操作,那么線程二的加鎖順序也必須是A、B、C,否則可能出現死鎖。
【強制】并發修改同一記錄時,避免更新丟失,需要加鎖。要么在應用層加鎖,要么在緩存加鎖,要么在數據庫層使用樂觀鎖,使用version作為更新依據。
說明:如果每次訪問沖突概率小于20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小于3次。
比較微妙的一條規約,說得模模糊糊。畢竟這種場景不會交給初級程序員處理,所以看不懂也沒關系。但話說回來,夠格的程序員也用不著看這條規約吧-_-
唉,還是簡單細化一下吧:
A請求要把數據庫中一條記錄的一個字段從10更新到11,B請求要把同一條記錄同一個字段從10更新到12,A請求先到,B請求后到,預期的結果應該是12,但有可能會是B先完成更新,這樣最終的結果是11,就發生了規約中所說的“更新丟失”。
在應用層加鎖:語焉不詳,分布式應用要怎么實現悲觀鎖?用zookeeper實現樂觀鎖?
在緩存加鎖:語焉不詳+1,緩存也沒法實現悲觀鎖吧?用Redis的WATCH命令實現樂觀鎖?
在數據庫層使用樂觀鎖,使用version作為更新依據:終于有一個說明白的了,把{當前version}作為更新的條件之一
UPDATE xxx = yyy, version = version + 1 WHERE id = ? and version = {當前version}
如果version被并發的另一個請求修改了,UPDATE會返回0,說明發生了兵法沖突,這個時候重查一次version,再重試。樂觀鎖的意思是在更新前假定不會發生并發沖突,在更新動作完成后檢查是否發生了并發沖突,如果是,則再重試若干次。
而悲觀鎖則是在更新前假定一定會發生并發沖突,所以要先把目標鎖住(比如用MySQL的排它鎖),再更新,更新后解鎖。
- 【強制】多線程并行處理定時任務時,Timer運行多個TimeTask時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,使用ScheduledExecutorService則沒有這個問題。
用Quartz或者Spring scheduler也是OK的
- 【推薦】使用CountDownLatch進行異步轉同步操作,每個線程退出前必須調用countDown方法,線程執行代碼注意catch異常,確保countDown方法可以執行,避免主線程無法執行至await方法,直到超時才返回結果。
說明:注意,子線程拋出異常堆棧,不能在主線程try-catch到。
CountDownLatch用在主線程需要等待所有子線程都執行完之后再執行一段邏輯的場景。例如主線程啟動10個線程執行任務,待所有子線程的任務都執行完成后,主線程再輸出最終結果。
這一規約的意思是要確保所有子線程在任何場景下都要調用countDown方法通知CountDownLatch自己已經執行完成,否則的話主線程可能會永遠await下去。
所以說子線程最好在finally塊中調用countDown。
- 【推薦】避免Random實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一seed導致的性能下降。
說明:Random實例包括java.util.Random 的實例或者 Math.random()的方式。
正例:在JDK7之后,可以直接使用API ThreadLocalRandom,而在 JDK7之前,需要編碼保證每個線程持有一個實例。
和本章第5條規約其實是一個意思
- 【推薦】在并發場景下,通過雙重檢查鎖(double-checked locking)實現延遲初始化的優化問題隱患(可參考 The "Double-Checked Locking is Broken" Declaration),推薦問題解決方案中較為簡單一種(適用于JDK5及以上版本),將目標屬性聲明為volatile型。
反例:
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...
}
雙重檢查鎖從理論上來說是OK的,但實際上會有低幾率出現問題,具體的原因比較復雜,不在這里贅述,可以參考規約中提到的文章。
解決方案是把Helper對象聲明為volatile的。如果是用雙重檢查鎖來實現延遲初始化的單例模式的話,也可以改用本章第1條規約中筆者給出的靜態內部類寫法。
- 【參考】volatile解決多線程內存不可見問題。對于一寫多讀,是可以解決變量同步問題,但是如果多寫,同樣無法解決線程安全問題。如果是count++操作,使用如下類實現:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是JDK8,推薦使用LongAdder對象,比AtomicLong性能更好(減少樂觀鎖的重試次數)。
volatile的語義是“易變”,用來解決可見性問題,即線程每次讀取volatile變量時,都會從主存中讀取,而不使用寄存器中的緩存。除此之外,volatile什么都不保證。
對volatile變量執行++操作時,會以如下幾步進行
- 將主存中的值讀入寄存器
- 將寄存器中的值+1
- 將寄存器中的值寫回主存
所以在并發進行++操作時,是不能保證原子性的!兩個線程可能從主存中讀到同一個值,加1后又同時寫回主存,最后主存中的值只增加了1.
- 【參考】 HashMap在容量不夠進行resize時由于高并發可能出現死鏈,導致CPU飆升,在開發過程中注意規避此風險。
千萬不要并發訪問HashMap,死循環可不好玩,請用ConcurrentHashMap
并發訪問HashMap引發死循環的原因,請見http://blog.csdn.net/xiaohui127/article/details/11928865
- 【參考】ThreadLocal無法解決共享對象的更新問題,ThreadLocal對象建議使用static修飾。這個變量是針對一個線程內所有操作共有的,所以設置為靜態變量,所有此類實例共享此靜態變量 ,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只要是這個線程內定義的)都可以操控這個變量。
ThreadLocal是用于在一個線程中共享對象的,不是用于多線程間共享對象的,明白這一點就夠了
(七) 控制語句
- 【強制】在一個switch塊內,每個case要么通過break/return等來終止,要么注釋說明程序將繼續執行到哪一個case為止;在一個switch塊內,都必須包含一個default語句并且放在最后,即使它什么代碼也沒有。
不加break的話,匹配到的case后的所有case都會被執行
- 【強制】在if/else/for/while/do語句中必須使用大括號。即使只有一行代碼,避免使用單行的形式:if (condition) statements;
主要是為了避免以后把單行改成多行時忘了加大括號
- 【推薦】表達異常的分支時,少用if-else方式,這種方式可以改寫成:
if (condition) {
...
return obj;
}
// 接著寫else的業務邏輯代碼;
說明:如果非得使用if()...else if()...else...方式表達邏輯,【強制】請勿超過3層,超過請使用狀態設計模式。
正例:邏輯上超過3層的if-else代碼可以使用衛語句,或者狀態模式來實現。
估計很多人都迷惑衛語句是什么,實際上代碼樣例中給出的就是衛語句,也就是把if...else...結構拆成多個if來表現。
這樣做的主要目的是避免復雜的if...else...條件嵌套讓代碼讀起來太費力,使用狀態模式也是為了達成同樣的目的。
- 【推薦】除常用方法(如getXxx/isXxx)等外,不要在條件判斷中執行其它復雜的語句,將復雜邏輯判斷的結果賦值給一個有意義的布爾變量名,以提高可讀性。
說明:很多if語句內的邏輯相當復雜,閱讀者需要分析條件表達式的最終結果,才能明確什么樣的條件執行什么樣的語句,那么,如果閱讀者分析邏輯表達式錯誤呢?
正例:
//偽代碼如下
boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
...
}
反例:
if ((file.open(fileName, "w") != null) && (...) || (...)) {
...
}
【推薦】循環體中的語句要考量性能,以下操作盡量移至循環體外處理,如定義對象、變量、獲取數據庫連接,進行不必要的try-catch操作(這個try-catch是否可以移至循環體外)。
【推薦】接口入參保護,這種場景常見的是用于做批量操作的接口。
有點暈,接口入參保護是啥?是指對接口的入參進行校驗吧?特別是批量操作接口,如果不校驗好入參的話,可能會出現部分失敗?浪費性能還得回滾?是這個意思嗎?說詳細點能死嗎?
【參考】下列情形,需要進行參數校驗:
1) 調用頻次低的方法。
2) 執行時間開銷很大的方法。此情形中,參數校驗時間幾乎可以忽略不計,但如果因為參數錯誤導致中間執行回退,或者錯誤,那得不償失。
3) 需要極高穩定性和可用性的方法。
4) 對外提供的開放接口,不管是RPC/API/HTTP接口。
5) 敏感權限入口。【參考】下列情形,不需要進行參數校驗:
1) 極有可能被循環調用的方法。但在方法說明里必須注明外部參數檢查要求。
2) 底層調用頻度比較高的方法。畢竟是像純凈水過濾的最后一道,參數錯誤不太可能到底層才會暴露問題。一般DAO層與Service層都在同一個應用中,部署在同一臺服務器中,所以DAO的參數校驗,可以省略。
3) 被聲明成private只會被自己代碼所調用的方法,如果能夠確定調用方法的代碼傳入參數已經做過檢查或者肯定不會有問題,此時可以不校驗參數。
7, 8兩點總結的很好,非常好
(八) 注釋規約
這一章不用過多解釋了
【強制】類、類屬性、類方法的注釋必須使用Javadoc規范,使用/**內容*/格式,不得使用//xxx方式。
說明:在IDE編輯窗口中,Javadoc方式會提示相關注釋,生成Javadoc可以正確輸出相應注釋;在IDE中,工程調用方法時,不進入方法即可懸浮提示方法、參數、返回值的意義,提高閱讀效率。【強制】所有的抽象方法(包括接口中的方法)必須要用Javadoc注釋、除了返回值、參數、異常說明外,還必須指出該方法做什么事情,實現什么功能。
說明:對子類的實現要求,或者調用注意事項,請一并說明。【強制】所有的類都必須添加創建者和創建日期。
【強制】方法內部單行注釋,在被注釋語句上方另起一行,使用//注釋。方法內部多行注釋使用/* */注釋,注意與代碼對齊。
【強制】所有的枚舉類型字段必須要有注釋,說明每個數據項的用途。
【推薦】與其“半吊子”英文來注釋,不如用中文注釋把問題說清楚。專有名詞與關鍵字保持英文原文即可。 反例:“TCP連接超時”解釋成“傳輸控制協議連接超時”,理解反而費腦筋。
【推薦】代碼修改的同時,注釋也要進行相應的修改,尤其是參數、返回值、異常、核心邏輯等的修改。 說明:代碼與注釋更新不同步,就像路網與導航軟件更新不同步一樣,如果導航軟件嚴重滯后,就失去了導航的意義。
【參考】合理處理注釋掉的代碼。盡量在目標代碼上方詳細說明,而不是簡單的注釋掉。如果無用,則直接刪除。
說明:代碼被注釋掉有兩種可能性:1)后續會恢復此段代碼邏輯。2)永久不用。前者如果沒有備注信息,難以知曉注釋動機。后者建議直接刪掉(代碼倉庫保存了歷史代碼)。【參考】對于注釋的要求:第一、能夠準確反應設計思想和代碼邏輯;第二、能夠描述業務含義,使別的程序員能夠迅速了解到代碼背后的信息。完全沒有注釋的大段代碼對于閱讀者形同天書,注釋是給自己看的,即使隔很長時間,也能清晰理解當時的思路;注釋也是給繼任者看的,使其能夠快速接替自己的工作。
【參考】好的命名、代碼結構是自解釋的,注釋力求精簡準確、表達到位。避免出現注釋的一個極端:過多過濫的注釋,代碼的邏輯一旦修改,修改注釋是相當大的負擔。
反例:
// put elephant into fridge
put(elephant, fridge);
方法名put,加上兩個有意義的變量名elephant和fridge,已經說明了這是在干什么,語義清晰的代碼不需要額外的注釋。
這一點筆者非常認同
- 【參考】特殊注釋標記,請注明標記人與標記時間。注意及時處理這些標記,通過標記掃描,經常清理此類標記。線上故障有時候就是來源于這些標記處的代碼。
1) 待辦事宜(TODO):( 標記人,標記時間,[預計處理時間]) 表示需要實現,但目前還未實現的功能。這實際上是一個Javadoc的標簽,目前的Javadoc還沒有實現,但已經被廣泛使用。只能應用于類,接口和方法(因為它是一個Javadoc標簽)。
2) 錯誤,不能工作(FIXME):(標記人,標記時間,[預計處理時間]) 在注釋中用FIXME標記某代碼是錯誤的,而且不能工作,需要及時糾正的情況。
(九) 其他
- 【強制】在使用正則表達式時,利用好其預編譯功能,可以有效加快正則匹配速度。
說明:不要在方法體內定義:Pattern pattern = Pattern.compile(規則);
反復使用的正則表達式,提前進行預編譯可以提升效率。然而如果在方法體內,只用一次的正則,就沒必要預編譯了
【強制】velocity調用POJO類的屬性時,建議直接使用屬性名取值即可,模板引擎會自動按規范調用POJO的getXxx(),如果是boolean基本數據類型變量(boolean命名不需要加is前綴),會自動調用isXxx()方法。 說明:注意如果是Boolean包裝類對象,優先調用getXxx()的方法。
【強制】后臺輸送給頁面的變量必須加$!{var}——中間的感嘆號。
說明:如果var=null或者不存在,那么${var}會直接顯示在頁面上。
誰能給我解釋一下這是在說哪個框架,velocity嗎……放在Java開發規范里沒關系嗎……
【強制】注意 Math.random() 這個方法返回是double類型,注意取值的范圍 0≤x<1(能夠取到零值,注意除零異常),如果想獲取整數類型的隨機數,不要將x放大10的若干倍然后取整,直接使用Random對象的nextInt或者nextLong方法。
【強制】獲取當前毫秒數System.currentTimeMillis(); 而不是new Date().getTime();
說明:如果想獲取更加精確的納秒級時間值,使用System.nanoTime()的方式。在JDK8中,針對統計時間等場景,推薦使用Instant類。
如果僅要獲取當前毫秒數,使用new Date().getTime()的效率低,且產生了垃圾對象
【推薦】不要在velocity模板中加入變量聲明、邏輯運算符,更不要在模板中加入任何復雜的邏輯。
【推薦】任何數據結構的構造或初始化,都應指定大小,避免數據結構無限增長吃光內存。
容易引發誤解的一條規約
大部分Java集合類在構造時指定的大小都是初始尺寸而不是尺寸上限
只有幾種Queue除外,如ArrayBlockingQueue和LinkedBlockingQueue,其構造時可以指定隊列的max size
- 【推薦】對于“明確停止使用的代碼和配置”,如方法、變量、類、配置文件、動態配置屬性等要堅決從程序中清理出去,避免造成過多垃圾。