VIP編程規範
- 格式規約/OOP規約
- 方法設計
- Rule 1.【推薦】方法的長度不要超過100行,如果超過需要重構
- Rule 5.【推薦】方法參數最好不要超過4個,超過4個時需要抽取參數類
- Rule 6.【推薦】下列情形,需要進行參數校驗
- Rule 7.【推薦】下列情形,不需要進行參數校驗
- Rule 12.【強制】正被外部調用的接口,不允許修改方法簽名,避免對接口 的調用方產生影響
- 【推薦】參數列表中請儘量不取用true、false類參數
- 返回值
- 【推薦】下列情形,需要進行參數校驗
- 【推薦】下列情形,不需要進行參數校驗
- 【推薦】禁用assert做參數校驗
- 類設計
- 【推薦】類的長度度量
- 【推薦】 構造函數如果有很多參數,且有多種參數組合時,建議使用Builder模式
- 【推薦】構造函數要簡單,尤其是存在繼承關係的時候
- 【推薦】 內部類的定義原則
- 【強制】POJO類必須覆寫toString方法。
- 【強制/推薦】hashCode和equals方法的處理,遵循如下規則:
- 【強制】使用IDE生成toString,hashCode和equals方法。
- 【強制】Object的equals方法容易拋空指針異常,應使用常量或確定非空的對象來調用equals
- 【推薦】得墨忒耳法則,不要和陌生人說話
- 控制語句
- 【推薦】限定方法的嵌套層次
- 【推薦】簡單邏輯,善用三目運算符,減少if-else語句的編寫
- 【推薦】表達式中,能造成短路概率較大的邏輯儘量放前面,使得後面的判斷可以免於執行
- 【強制】switch的規則
- 【推薦】循環體中的語句要考量性能,操作儘量移至循環體外處理
- 基本類型與字符串
- 集合處理
- Rule 1. 【推薦】底層數據結構是數組的集合,指定集合初始大小
- Rule 5. 【強制】當對象用於集合時,下列情況需要重新實現hashCode()和 equals()
- Rule 6. 【強制】高度注意各種Map類集合Key/Value能不能存儲null值的情 況
- Rule 7. 【強制】長生命週期的集合,裏面內容需要及時清理,避免內存泄漏
- Rule 8. 【強制】集合如果存在併發修改的場景,需要使用線程安全的版本
- Rule 11. 【推薦】如果Key只有有限的可選值,先將Key封裝成Enum,並使用EnumMap
- Rule 12. 【推薦】Array 與 List互轉的正確寫法
- 併發處理
- 日誌規約
- Rule 2. 【推薦】對不確定會否輸出的日誌,採用佔位符或條件判斷
- Rule 3. 【推薦】對確定輸出,而且頻繁輸出的日誌,採用直接拼裝字符串的方式
- Rule 8. 【推薦】使用warn級別而不是error級別,記錄外部輸入參數錯誤的情況
- 其它規約
格式規約/OOP規約
Rule 4. 【推薦】類內方法定義的順序,不要“總是在類的最後添加新方法”
-
類內方法定義的順序依次是:構造函數 > (公有方法 > 保護方法 > 私有方法) > getter/setter 方法。
如果公有方法可以分成幾組,私有方法也緊跟公有方法的分組。 -
當一個類有多個構造方法,或者多個同名的重載方法,這些方法應該放置在一起。其中參數較多的方法在後面。
-
作爲調用者的方法,儘量放在被調用的方法前面。
方法設計
Rule 1.【推薦】方法的長度不要超過100行,如果超過需要重構
Rule 5.【推薦】方法參數最好不要超過4個,超過4個時需要抽取參數類
解決方案
- 如果多個參數同屬於一個對象,直接傳遞對象。
例外: 你不希望依賴整個對象,傳播了類之間的依賴性。 - 將多個參數合併爲一個新創建的邏輯對象。
例外: 多個參數之間毫無邏輯關聯。 - 將函數拆分成多個函數,讓每個函數所需的參數減少。
Rule 6.【推薦】下列情形,需要進行參數校驗
- 調用頻次低的方法。
- 執行時間開銷很大的方法。此情形中,參數校驗時間幾乎可以忽略不計,但如果因爲參數錯誤導致中間執行回退,或者錯誤,代價更大。
- 需要極高穩定性和可用性的方法。
- 對外提供的開放接口,不管是RPC/HTTP/公共類庫的API接口。
如果使用Apache Validate 或 Guava Precondition進行校驗,並附加錯誤提示信息時,注意不要每次校驗都做一次字符串拼接。
Rule 7.【推薦】下列情形,不需要進行參數校驗
- 極有可能被循環調用的方法。
- 底層調用頻度比較高的方法。畢竟是像純淨水過濾的最後一道,參數錯 誤不太可能到底層纔會暴露問題。
比如,一般DAO層與Service層都在同一個應用中,所以DAO層的參數校 驗,可以省略。 - 被聲明成private,或其他只會被自己代碼所調用的方法,如果能夠確定 在調用方已經做過檢查,或者肯定不會有問題則可省略
Rule 12.【強制】正被外部調用的接口,不允許修改方法簽名,避免對接口 的調用方產生影響
只能新增新接口,並對已過時接口加@Deprecated註解,並清晰地說明新接口是什麼。
【推薦】參數列表中請儘量不取用true、false類參數
避免因爲調用者不明確導致的問題
返回值
- 返回調用函數方應該知道的消息,慎用void
- 當函數中有很多種錯誤信息時,使用和調用方協調好的ErrorCode或Exception
- 返回空的集合時使用Collections.empty***方法,不要直接返回null(建議)
【推薦】下列情形,需要進行參數校驗
- 調用頻次低的方法。
- 執行時間開銷很大的方法。此情形中,參數校驗時間幾乎可以忽略不計,但如果因爲參數錯誤導致中間執行回退,或者錯誤,代價更大。
- 需要極高穩定性和可用性的方法。
- 對外提供的開放接口,不管是RPC/HTTP/公共類庫的API接口。
tips:如果使用Apache Validate 或 Guava Precondition進行校驗,並附加錯誤提示信息時,注意不要每次校驗都做一次字符串拼接。
//WRONG
Validate.isTrue(length > 2, "length is "+keys.length+", less than 2", length);
//RIGHT
Validate.isTrue(length > 2, "length is %d, less than 2", length);
【推薦】下列情形,不需要進行參數校驗
-
極有可能被循環調用的方法。
-
底層調用頻度比較高的方法。畢竟是像純淨水過濾的最後一道,參數錯誤不太可能到底層纔會暴露問題。
tips: 比如,一般DAO層與Service層都在同一個應用中,所以DAO層的參數校驗,可以省略。 -
被聲明成private,或其他只會被自己代碼所調用的方法,如果能夠確定在調用方已經做過檢查,或者肯定不會有問題則可省略。
tips: 即使忽略檢查,也儘量在方法說明裏註明參數的要求,比如vjkit中的@NotNull,@Nullable標識。
【推薦】禁用assert做參數校驗
-
assert斷言僅用於測試環境調試,無需在生產環境時進行的校驗。因爲它需要增加-ea啓動參數纔會被執行。而且校驗失敗會拋出一個AssertionError(屬於Error,需要捕獲Throwable)
-
因此在生產環境進行的校驗,需要使用Apache Commons Lang的Validate或Guava的Precondition。
#【推薦】返回值可以爲Null,可以考慮使用JDK8的Optional類 -
不強制返回空集合,或者空對象。但需要添加註釋充分說明什麼情況下會返回null值。
-
本手冊明確防止NPE是調用者的責任。即使被調用方法返回空集合或者空對象,對調用者來說,也並非高枕無憂,必須考慮到遠程調用失敗、序列化失敗、運行時異常等場景返回null的情況。
類設計
【推薦】類的長度度量
類儘量不要超過300行,或其他團隊共同商定的行數。
對過大的類進行分拆時,可考慮其內聚性,即類的屬性與類的方法的關聯程度,如果有些屬性沒有被大部分的方法使用,其內聚性是低的。
【推薦】 構造函數如果有很多參數,且有多種參數組合時,建議使用Builder模式
Executor executor = new ThreadPoolBuilder().coreThread(10).queueLenth(100).build();
【推薦】構造函數要簡單,尤其是存在繼承關係的時候
可以將複雜邏輯,尤其是業務邏輯,抽取到獨立函數,如init(),start(),讓使用者顯式調用。
Foo foo = new Foo();
foo.init();
【推薦】 內部類的定義原則
當一個類與另一個類關聯非常緊密,處於從屬的關係,特別是只有該類會訪問它時,可定義成私有內部類以提高封裝性。
另外,內部類也常用作回調函數類,在JDK8下建議寫成Lambda。
內部類分匿名內部類,內部類,靜態內部類三種。
- 匿名內部類 與 內部類,按需使用:
在性能上沒有區別;當內部類會被多個地方調用,或匿名內部類的長度太長,已影響對調用它的方法的閱讀時,定義有名字的內部類。
- 靜態內部類 與 內部類,優先使用靜態內部類:
非靜態內部類持有外部類的引用,能訪問外類的實例方法與屬性。構造時多傳入一個引用對性能沒有太大影響,更關鍵的是向閱讀者傳遞自己的意圖,內部類會否訪問外部類。
非靜態內部類裏不能定義static的屬性與方法。
【強制】POJO類必須覆寫toString方法。
便於記錄日誌,排查問題時調用POJO的toString方法打印其屬性值。否則默認的Object.toString()只打印類名@數字的無效信息。
【強制/推薦】hashCode和equals方法的處理,遵循如下規則:
13.1【強制】只要重寫equals,就必須重寫hashCode。 而且選取相同的屬性進行運算。
13.2【推薦】只選取真正能決定對象是否一致的屬性,而不是所有屬性,可以改善性能。
13.3【推薦】對不可變對象,可以緩存hashCode值改善性能(比如String就是例子)。
13.4【強制】類的屬性增加時,及時重新生成toString,hashCode和equals方法。
【強制】使用IDE生成toString,hashCode和equals方法。
使用IDE生成而不是手寫,能保證toString有統一的格式,equals和hashCode則避免不正確的Null值處理。
子類生成toString() 時,還需要勾選父類的屬性。
【強制】Object的equals方法容易拋空指針異常,應使用常量或確定非空的對象來調用equals
推薦使用java.util.Objects#equals(JDK7引入的工具類)
"test".equals(object); //RIGHT
Objects.equals(object, "test"); //RIGHT
【推薦】得墨忒耳法則,不要和陌生人說話
以下調用,一是導致了對A對象的內部結構(B,C)的緊耦合,二是連串的調用很容易產生NPE,因此鏈式調用盡量不要過長。
obj.getA().getB().getC().hello();
控制語句
【推薦】少用if-else方式,多用哨兵語句式以減少嵌套層次
if (condition) {
...
return obj;
}
// 接着寫else的業務邏輯代碼;
【推薦】限定方法的嵌套層次
所有if/else/for/while/try的嵌套,當層次過多時,將引起巨大的閱讀障礙,因此一般推薦嵌套層次不超過4。
通過抽取方法,或哨兵語句(見Rule 2)來減少嵌套。
public void applyDriverLicense() {
if (isTooYoung()) {
System.out.println("You are too young to apply driver license.");
return;
}
if (isTooOld()) {
System.out.println("You are too old to apply driver license.");
return;
}
System.out.println("You've applied the driver license successfully.");
return;
}
【推薦】簡單邏輯,善用三目運算符,減少if-else語句的編寫
s != null ? s : "";
【推薦】表達式中,能造成短路概率較大的邏輯儘量放前面,使得後面的判斷可以免於執行
if (maybeTrue() || maybeFalse()) { ... }
if (maybeFalse() && maybeTrue()) { ... }
【強制】switch的規則
1)在一個switch塊內,每個case要麼通過break/return等來終止,要麼註釋說明程序將繼續執行到哪一個case爲止;
2)在一個switch塊內,都必須包含一個default語句並且放在最後,即使它什麼代碼也沒有。
String animal = "tomcat";
switch (animal) {
case "cat":
System.out.println("It's a cat.");
break;
case "lion": // 執行到tiger
case "tiger":
System.out.println("It's a beast.");
break;
default:
// 什麼都不做,也要有default
break;
}
【推薦】循環體中的語句要考量性能,操作儘量移至循環體外處理
1)不必要的耗時較大的對象構造;
2)不必要的try-catch(除非出錯時需要循環下去)。
基本類型與字符串
鏈接地址
全篇需要關注
集合處理
Rule 1. 【推薦】底層數據結構是數組的集合,指定集合初始大小
底層數據結構爲數組的集合包括 ArrayList,HashMap,HashSet, ArrayDequeue等。
數組有大小限制,當超過容量時,需要進行復制式擴容,新申請一個是原來 容量150% or 200%的數組,將原來的內容複製過去,同時浪費了內存與性 能。HashMap/HashSet的擴容,還需要所有鍵值對重新落位,消耗更大。
默認構造函數使用默認的數組大小,比如ArrayList默認大小爲10,HashMap 爲16。因此建議使用ArrayList(int initialCapacity)等構造函數,明確初始化大小。
HashMap/HashSet的初始值還要考慮加載因子:
爲了降低哈希衝突的概率(Key的哈希值按數組大小取模後,如果落在同一個 數組下標上,將組成一條需要遍歷的Entry鏈),默認當HashMap中的鍵值對 達到數組大小的75%時,即會觸發擴容。因此,如果預估容量是100,即需 要設定 100/0.75 +1=135 的數組大小。vjkit的MapUtil的Map創建函數封裝了該計算。如果希望加快Key查找的時間,還可以進一步降低加載因子,加大初始大小,以降低哈希衝突的概率。
Rule 5. 【強制】當對象用於集合時,下列情況需要重新實現hashCode()和 equals()
- 以對象做爲Map的KEY時;
- 將對象存入Set時。
上述兩種情況,都需要使用hashCode和equals比較對象,默認的實現會比較是否同一個對象(對象的引用相等)。
另外,對象放入集合後,會影響hashCode(),equals()結果的屬性,將不允許修改。
Rule 6. 【強制】高度注意各種Map類集合Key/Value能不能存儲null值的情 況
Map | Key | value |
---|---|---|
HashMap | Nullable | Nullable |
ConcurrentHashMap | NotNull | Notnull |
TreeMap | NotNull | NotNull |
由於HashMap的干擾,很多人認爲ConcurrentHashMap是可以置入null值。 同理,Set中的value實際是Map中的key。
Rule 7. 【強制】長生命週期的集合,裏面內容需要及時清理,避免內存泄漏
長生命週期集合包括下面情況,都要小心處理。
- 靜態屬性定義;
- 長生命週期對象的屬性;
- 保存在ThreadLocal中的集合。
如無法保證集合的大小是有限的,使用合適的緩存方案代替直接使用 HashMap。
另外,如果使用WeakHashMap保存對象,當對象本身失效時,就不會因爲它在集合中存在引用而阻止回收。但JDK的WeakHashMap並不支持併發版本,如果需要併發可使用Guava Cache的實現。
Rule 8. 【強制】集合如果存在併發修改的場景,需要使用線程安全的版本
- 著名的反例,HashMap擴容時,遇到併發修改可能造成100%CPU佔用。
推薦使用 java.util.concurrent(JUC) 工具包中的併發版集合,如 ConcurrentHashMap等,優於使用Collections.synchronizedXXX()系列函數 進行同步化封裝(等價於在每個方法都加上synchronized關鍵字)。
例外:ArrayList所對應的CopyOnWriteArrayList,每次更新時都會複製整個 數組,只適合於讀多寫很少的場景。如果頻繁寫入,可能退化爲使用 Collections.synchronizedList(list)。 - 即使線程安全類仍然要注意函數的正確使用。
例如:即使用了ConcurrentHashMap,但直接是用get/put方法,仍然可能會 多線程間互相覆蓋。
//WRONG
E e = map.get(key);
if (e == null) {
e = new E();
map.put(key, e); //仍然能兩條線程併發執行put,互相覆蓋 }
return e;
//RIGHT
E e = map.get(key);
if (e == null) {
e = new E();
E previous = map.putIfAbsent(key, e);
if(previous != null) {
return previous;
}
}
return e;
Rule 11. 【推薦】如果Key只有有限的可選值,先將Key封裝成Enum,並使用EnumMap
EnumMap,以Enum爲Key的Map,內部存儲結構爲Object[enum.size],訪問時以value = Object[enum.ordinal()]獲取值,同時具備HashMap的清晰結構與數組的性能。
public enum COLOR {
RED, GREEN, BLUE, ORANGE;
}
EnumMap<COLOR, String> moodMap = new EnumMap<COLOR, String> (COLOR.class);
Rule 12. 【推薦】Array 與 List互轉的正確寫法
// list -> array,構造數組時不需要設定大小
String[] array = (String[])list.toArray(); //WRONG;
String[] array = list.toArray(new String[0]); //RIGHT
String[] array = list.toArray(new String[list.size()]); //RIGHT,但list.size()可用0代替。
// array -> list
//非原始類型數組,且List不能再擴展
List list = Arrays.asList(array);
//非原始類型數組, 但希望List能再擴展
List list = new ArrayList(array.length);
Collections.addAll(list, array);
//原始類型數組,JDK8
List myList = Arrays.stream(intArray).boxed().collect(Collectors.toList());
併發處理
Rule 1. 【強制】創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯
1)創建單條線程時直接指定線程名稱
Thread t = new Thread();
t.setName("cleanup-thread");
2) 線程池則使用guava或自行封裝的ThreadFactory,指定命名規則。
//guava 或自行封裝的ThreadFactory
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + "-%d").build();
ThreadPoolExecutor executor = new ThreadPoolExecutor(..., threadFactory, ...);
Rule 3. 【強制】線程池不允許使用 Executors去創建,避資源耗盡風險
Executors返回的線程池對象的弊端 :
1)FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。
應通過 new ThreadPoolExecutor(xxx,xxx,xxx,xxx)這樣的方式,更加明確線程池的運行規則,合理設置Queue及線程池的core size和max size,建議使用vjkit封裝的ThreadPoolBuilder。
Rule 4. 【強制】正確停止線程
Thread.stop()不推薦使用,強行的退出太不安全,會導致邏輯不完整,操作不原子,已被定義成Deprecate方法。
停止單條線程,執行Thread.interrupt()。
停止線程池:
-
ExecutorService.shutdown(): 不允許提交新任務,等待當前任務及隊列中的任務全部執行完畢後退出;
-
ExecutorService.shutdownNow(): 通過Thread.interrupt()試圖停止所有正在執行的線程,並不再處理還在隊列中等待的任務。
最優雅的退出方式是先執行shutdown(),再執行shutdownNow(),vjkit的ThreadPoolUtil進行了封裝。
注意,Thread.interrupt()並不保證能中斷正在運行的線程,需編寫可中斷退出的Runnable,見規則5。
日誌規約
Rule 2. 【推薦】對不確定會否輸出的日誌,採用佔位符或條件判斷
//WRONG
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
如果日誌級別是info,上述日誌不會打印,但是會執行1)字符串拼接操作,2)如果symbol是對象,還會執行toString()方法,浪費了系統資源,最終日誌卻沒有打印。
//RIGHT
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);
但如果symbol.getMessage()本身是個消耗較大的動作,佔位符在此時並沒有幫助,須要改爲條件判斷方式來完全避免它的執行。
//WRONG
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol.getMessage());
//RIGHT
if (logger.isDebugEnabled()) {
logger.debug("Processing trade with id: " + id + " symbol: " + symbol.getMessage());
}
Rule 3. 【推薦】對確定輸出,而且頻繁輸出的日誌,採用直接拼裝字符串的方式
如果這是一條WARN,ERROR級別的日誌,或者確定輸出的INFO級別的業務日誌,直接字符串拼接,比使用佔位符替換,更加高效。
Slf4j的佔位符並沒有魔術,每次輸出日誌都要進行佔位符的查找,字符串的切割與重新拼接。
//RIGHT
logger.info("I am a business log with id: " + id + " symbol: " + symbol);
//RIGHT
logger.warn("Processing trade with id: " + id + " symbol: " + symbol);
Rule 8. 【推薦】使用warn級別而不是error級別,記錄外部輸入參數錯誤的情況
如非必要,請不在此場景打印error級別日誌,避免頻繁報警。
error級別只記錄系統邏輯出錯、異常或重要的錯誤信息。
其它規約
Rule 2. 【推薦】時間獲取的原則
1)獲取當前毫秒數System.currentTimeMillis() 而不是new Date().getTime(),後者的消耗要大得多。
2)如果要獲得更精確的,且不受NTP時間調整影響的流逝時間,使用System.nanoTime()獲得機器從啓動到現在流逝的納秒數。
Rule 6. 【推薦】正確使用反射,減少性能損耗
獲取Method/Field對象的性能消耗較大, 而如果對Method與Field對象進行緩存再反覆調用,則並不會比直接調用類的方法與成員變量慢(前15次使用NativeAccessor,第15次後會生成GeneratedAccessorXXX,bytecode爲直接調用實際方法)
//用於對同一個方法多次調用
private Method method = ....
public void foo(){
method.invoke(obj, args);
}
//用於僅會對同一個方法單次調用
ReflectionUtils.invoke(obj, methodName, args);