VIP編程規範

VIP編程規範

格式規約/OOP規約

Rule 4. 【推薦】類內方法定義的順序,不要“總是在類的最後添加新方法”

  • 類內方法定義的順序依次是:構造函數 > (公有方法 > 保護方法 > 私有方法) > getter/setter 方法。
    如果公有方法可以分成幾組,私有方法也緊跟公有方法的分組。

  • 當一個類有多個構造方法,或者多個同名的重載方法,這些方法應該放置在一起。其中參數較多的方法在後面。

  • 作爲調用者的方法,儘量放在被調用的方法前面。

方法設計

Rule 1.【推薦】方法的長度不要超過100行,如果超過需要重構

Rule 5.【推薦】方法參數最好不要超過4個,超過4個時需要抽取參數類

解決方案

  • 如果多個參數同屬於一個對象,直接傳遞對象。
    例外: 你不希望依賴整個對象,傳播了類之間的依賴性。
  • 將多個參數合併爲一個新創建的邏輯對象。
    例外: 多個參數之間毫無邏輯關聯。
  • 將函數拆分成多個函數,讓每個函數所需的參數減少。

Rule 6.【推薦】下列情形,需要進行參數校驗

  1. 調用頻次低的方法。
  2. 執行時間開銷很大的方法。此情形中,參數校驗時間幾乎可以忽略不計,但如果因爲參數錯誤導致中間執行回退,或者錯誤,代價更大。
  3. 需要極高穩定性和可用性的方法。
  4. 對外提供的開放接口,不管是RPC/HTTP/公共類庫的API接口。
    如果使用Apache Validate 或 Guava Precondition進行校驗,並附加錯誤提示信息時,注意不要每次校驗都做一次字符串拼接。

Rule 7.【推薦】下列情形,不需要進行參數校驗

  1. 極有可能被循環調用的方法。
  2. 底層調用頻度比較高的方法。畢竟是像純淨水過濾的最後一道,參數錯 誤不太可能到底層纔會暴露問題。
    比如,一般DAO層與Service層都在同一個應用中,所以DAO層的參數校 驗,可以省略。
  3. 被聲明成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。

內部類分匿名內部類,內部類,靜態內部類三種。

  1. 匿名內部類 與 內部類,按需使用:

在性能上沒有區別;當內部類會被多個地方調用,或匿名內部類的長度太長,已影響對調用它的方法的閱讀時,定義有名字的內部類。

  1. 靜態內部類 與 內部類,優先使用靜態內部類:

非靜態內部類持有外部類的引用,能訪問外類的實例方法與屬性。構造時多傳入一個引用對性能沒有太大影響,更關鍵的是向閱讀者傳遞自己的意圖,內部類會否訪問外部類。

非靜態內部類裏不能定義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()

  1. 以對象做爲Map的KEY時;
  2. 將對象存入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. 【強制】長生命週期的集合,裏面內容需要及時清理,避免內存泄漏

長生命週期集合包括下面情況,都要小心處理。

  1. 靜態屬性定義;
  2. 長生命週期對象的屬性;
  3. 保存在ThreadLocal中的集合。

如無法保證集合的大小是有限的,使用合適的緩存方案代替直接使用 HashMap。
另外,如果使用WeakHashMap保存對象,當對象本身失效時,就不會因爲它在集合中存在引用而阻止回收。但JDK的WeakHashMap並不支持併發版本,如果需要併發可使用Guava Cache的實現。

Rule 8. 【強制】集合如果存在併發修改的場景,需要使用線程安全的版本

  1. 著名的反例,HashMap擴容時,遇到併發修改可能造成100%CPU佔用。
    推薦使用 java.util.concurrent(JUC) 工具包中的併發版集合,如 ConcurrentHashMap等,優於使用Collections.synchronizedXXX()系列函數 進行同步化封裝(等價於在每個方法都加上synchronized關鍵字)。
    例外:ArrayList所對應的CopyOnWriteArrayList,每次更新時都會複製整個 數組,只適合於讀多寫很少的場景。如果頻繁寫入,可能退化爲使用 Collections.synchronizedList(list)。
  2. 即使線程安全類仍然要注意函數的正確使用。
    例如:即使用了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);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章