Java 程式設計規範 -- 易錯精簡版

2020-08-10 17:49:20

Part 1 – 易錯點

--  edit by liudeyu,If you have any adivice or suggestion, please participate in the discussion

 

命名規範方面:

1. 【強制】 POJO 類中的任何布爾型別的變數, 都不要加 is 字首,否則部分框架解析會引起序列化錯誤。
說明: 在本文 MySQL 規約中的建表約定第一條,表達是與否的值採用 is_xxx 的命名方式,所以,需要在
<resultMap>設定從 is_xxx 到 xxx 的對映關係。
反例: 定義爲基本數據型別 Boolean isDeleted 的屬性,它的方法也是 isDeleted(),框架在反向解析的時
候, 「誤以爲」 對應的屬性名稱是 deleted,導致屬性獲取不到,進而拋出異常。

 

 

2.【強制】 杜絕完全不規範的縮寫, 避免望文不知義。
反例: AbstractClass「縮寫」 命名成 AbsClass; condition「縮寫」 命名成 condi,此類隨意縮寫嚴重降低了程式碼的可閱讀性。


3.【推薦】 爲了達到程式碼自解釋的目標,任何自定義程式設計元素在命名時,使用盡量完整的單詞組合來表達。

!!! java 變數爲什麼長,就是因爲這個原因,雖然長,但是如果能望文生義,也是大有裨益。


正例:在 JDK 中,對某個物件參照的 volatile 欄位進行原子更新的類名爲:AtomicReferenceFieldUpdater。
反例: 常見的方法內變數爲 int a;的定義方式。

 

4.【推薦】 如果模組、 介面、類、方法使用了設計模式,在命名時需體現出具體模式。  
說明: 將設計模式體現在名字中,有利於閱讀者快速理解架構設計理念。
正例: public class OrderFactory;
public class LoginProxy;
public class ResourceObserver


5. 【強制】 不要使用一個常數類維護所有常數, 要按常數功能進行歸類,分開維護。

!! 這個強制一下,常數用顯而易見的命名來命名類,查詢時候,容易定位 

說明: 大而全的常數類, 雜亂無章, 使用查詢功能才能 纔能定位到修改的常數,不利於理解,也不利於維護。
正例: 快取相關常數放在類 CacheConsts 下;系統設定相關常數放在類 ConfigConsts 下。

 

程式碼方面:

 

6.【強制】 外部正在呼叫或者二方庫依賴的介面,不允許修改方法簽名,避免對介面呼叫方產生影響。介面過時必須加@Deprecated 註解,並清晰地說明採用的新介面或者新服務是什麼。

!! 開放封閉原則,不管怎樣對外暴露的api, public介面,要保證相容性,不準 不準隨便刪減介面(加可以),你刪減一個前面版本有的介面,或者變動

之前版本有的介面,你讓參照你的庫的外部程式碼,要改多少,要有良心,不隨便改已有介面,不推薦使用的,直接加@Deprecated

作爲 庫 或是sdk開發者,更應該如此,你改幾次舊介面,每次升級版本,使用者需要改動呼叫處的話,不能無縫升級的話,大家肯定是怨聲載道,對你這個庫充滿抱怨。不得已時候,進行周知。

 

7. 【強制】Object 的 equals 方法容易拋空指針異常,應使用常數或確定有值的物件來呼叫 equals。
正例: "test".equals(object);
反例: object.equals("test");
說明: 推薦使用 java.util.Objects#equals( JDK7 引入的工具類) 。


8. 【強制】 所有整型包裝類物件之間值的比較, 全部使用 equals 方法比較。
說明: 對於 Integer var = ? 在-128 至 127 之間的賦值, Integer 物件是在 IntegerCache.cache 產生,會複用已有物件,這個區間內的 Integer 值可以直接使用==進行判斷,但是這個區間之外的所有數據,

都會在堆上產生,並不會複用已有物件,這是一個大坑,推薦使用 equals 方法進行判斷。

!! 程式碼中已經有了這種寫法了,要規避,不直接 ==

 

 

9.【強制】 禁止使用構造方法 BigDecimal(double)的方式把 double 值轉化爲 BigDecimal 物件。
說明: BigDecimal(double)存在精度損失風險,在精確計算或值比較的場景中可能會導致業務邏輯異常。


如: 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);

 

10. 關於基本數據型別與包裝數據型別的使用標準如下: 


1) 【強制】 所有的 POJO 類屬性必須使用包裝數據型別。
2) 【強制】 RPC 方法的返回值和參數必須使用包裝數據型別。
3) 【推薦】 所有的區域性變數使用基本數據型別。


說明: POJO 類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何 NPE 問題,或者入庫檢查,都由使用者來保證。
正例: 數據庫的查詢結果可能是 null,因爲自動拆箱,用基本數據型別接收有 NPE 風險。

反例: 某業務的交易報表上顯示成交總額漲跌情況,即正負 x%, x 爲基本數據型別,呼叫的 RPC 服務,調
用不成功時,返回的是預設值,頁面顯示爲 0%,這是不合理的,應該顯示成中劃線-。所以包裝數據型別
的 null 值,能夠表示額外的資訊,如:遠端呼叫失敗,異常退出

 

!! 這個注意,用包裝型別,可以避免一些,接受參數或者是查庫的NPE風險,強制要求, 主要表現在對外傳輸的變數值 都用包裝型別

 

13.【強制】 定義 DO/DTO/VO 等 POJO 類時,不要設定任何屬性預設值。

反例: POJO 類的 createTime 預設值爲 new Date(), 但是這個屬性在數據提取時並沒有置入具體值,在
更新其它欄位時又附帶更新了此欄位,導致建立時間被修改成當前時間

 

14.【強制】 構造方法裏面禁止加入任何業務邏輯,如果有初始化邏輯,請放在 init 方法中。

 

15. 【強制】 關於 hashCode 和 equals 的處理,遵循如下規則:

1) 只要重寫 equals,就必須重寫 hashCode。
2) 因爲 Set 儲存的是不重複的物件,依據 hashCode 和 equals 進行判斷,所以 Set 儲存的物件必須重寫
這兩個方法。
3) 如果自定義物件作爲 Map 的鍵,那麼必須覆寫 hashCode 和 equals。

說明: String 因爲重寫了 hashCode 和 equals 方法,所以我們可以愉快地使用 String 物件作爲 key 來使
用。

 

16. 【強制】 在使用 java.util.stream.Collectors 類的 toMap()方法轉爲 Map 集合時,一定要使用含有參數型別爲 BinaryOperator, 參數名爲 mergeFunction 的方法

否則當出現相同 key值時會拋出 IllegalStateException 異常。

!! 容易忽略,注意

 

17. 【強制】 在使用 java.util.stream.Collectors 類的 toMap()方法轉爲 Map 集合時,一定要注意當 value 爲 null 時會拋 NPE 異常。

說明: 在 java.util.HashMap 的 merge 方法裡會進行如下的判斷:


if (value == null || remappingFunction == null)
       throw new NullPointerException();


反例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new Pair<>("version1", 4.22));
pairArrayList.add(new Pair<>("version2", null));
Map<String, Double> map = pairArrayList.stream().collect(
// 拋出 NullPointerException 異常
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));

解決: stream 中 使用 filter 過濾一下 爲null 的entry 

 

18. 【強制】 ArrayList 的 subList 結果不可強轉成 ArrayList

否則會拋出 ClassCastException 異常: java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。
說明: subList 返回的是 ArrayList 的內部類 SubList, 並不是 ArrayList 而是 ArrayList 的一個檢視,對
於 SubList 子列表的所有操作最終會反映到原列表上。

 

解決: 定義變數, 操作物件的時候,也是提倡用介面來操縱,能不具現化到子類就不具現化到子類,集合物件,基本都可用 定義的介面來操縱,介面中的

方法一般已足夠使用,面向介面程式設計。

 

19. 【強制】 注意不可變容器,不要進行 增刪操縱,常見場景可以從介面命名中發現

例子:

  1. 使用 Map 的方法 keySet()/values()/entrySet()返回集合物件時,不可以對其進行添
    加元素操作,否則會拋出 UnsupportedOperationException 異常。

      2.  Collections 類返回的物件,如: emptyList()/singletonList()等都是 immutable list,
不可對其進行新增或者刪除元素的操作。

      3.  subList 場景中, 高度注意對父集合元素的增加或刪除, 均會導致子列表的遍歷、
增加、刪除產生 ConcurrentModificationException 異常。

      4.  使用工具類 Arrays.asList()把陣列轉換成集合時,不能使用其修改集合相關的方法,
它的 add/remove/clear 方法會拋出 UnsupportedOperationException 異常。

 

解釋: 這些介面,都有讓人遍歷檢視元素的意圖,並不是用來讓你操縱元素的。類似的還有,迭代元素過程中,不要進行,增刪操縱,會拋同步修改異常。

這樣的寫法也不規範。

 

20【強制】 不要在 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);
    }
}

!!  注意,不要在for 迭代中,進行增刪操作

 

 

21.【推薦】 使用 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 值組合集合。

解釋: 容易無意識,寫出用keySet迭代,效率沒那麼高,推薦用 entry 迭代

 

 

 

22. 【強制】 建立執行緒或執行緒池時請指定有意義的執行緒名稱,方便出錯時回溯。

 

23. 【強制】 必須回收自定義的 ThreadLocal 變數,尤其線上程池場景下,執行緒經常會被複用
如果不清理自定義的 ThreadLocal 變數,可能會影響後續業務邏輯和造成記憶體泄露等問題。

儘量在代理中使用 try-finally 塊進行回收。

正例:
objectThreadLocal.set(userInfo);
try {
      // ...
} finally {
      objectThreadLocal.remove();
}

 

24. 【強制】 高併發時,同步呼叫應該去考量鎖的效能損耗。能用無鎖數據結構,就不要用鎖; 能鎖區塊,就不要鎖整個方法體; 能用物件鎖,就不要用類鎖。
說明: 儘可能使加鎖的程式碼塊工作量儘可能的小,避免在鎖程式碼塊中呼叫 RPC 方法。

關於,java中的無鎖容器,在juc包中有很多體現,可以瞭解下。包括常見的 List,Set,Map介面,包括原子變數 AtomicReference,AtomicInteger 等等都有涉及,

有對應需求的時候,不妨可以先看下有沒有對應的高效容器實現。

 

 

25.【強制】 對多個資源、數據庫表、物件同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。

說明: 執行緒一需要對錶 A、 B、 C 依次全部加鎖後纔可以進行更新操作,那麼執行緒二的加鎖順序也必須是 A、
B、 C,否則可能出現死鎖

 

26.【強制】 併發修改同一記錄時,避免更新丟失, 需要加鎖。 要麼在應用層加鎖,要麼在快取加
鎖,要麼在數據庫層使用樂觀鎖,使用 version 作爲更新依據。


說明: 如果每次存取衝突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於
3 次。

 

27.【強制】 多執行緒並行處理定時任務時, Timer 執行多個 TimeTask 時,只要其中之一沒有捕獲拋
出的異常,其它任務便會自動終止執行, 使用 ScheduledExecutorService 則沒有這個問題。

使用:ScheduledExecutorService

 

28.【推薦】 資金相關的金融敏感資訊,使用悲觀鎖策略。
說明: 樂觀鎖在獲得鎖的同時已經完成了更新操作,校驗邏輯容易出現漏洞,另外,樂觀鎖對衝突的解決策略有較複雜的要求,

處理不當容易造成系統壓力或數據異常,所以資金相關的金融敏感資訊不建議使用樂觀鎖更新。
正例: 悲觀鎖遵循一鎖二判三更新四釋放的原則

解釋: 在資金要求數據一定要正確的場景, 放棄效能保證正確性,孰輕孰重要懂判斷

 

 

數據庫相關:

數據庫和建表相關,統一強制的規範是必要的。這個比起前面的程式設計規範,更不能妥協。因爲建表後,有數據以後,修改表結構就是很麻煩的事情。

 

29. 【強制】 表達是與否概唸的欄位,必須使用 is_xxx 的方式命名,數據型別是 unsigned tinyint( 1 表示是, 0 表示否)。
說明: 任何欄位如果爲非負數,必須是 unsigned。
注意: POJO 類中的任何布爾型別的變數,都不要加 is 字首,所以,需要在<resultMap>設定從 is_xxx 到
Xxx 的對映關係。數據庫表示是與否的值,使用 tinyint 型別,堅持 is_xxx 的命名方式是爲了明確其取值含
義與取值範圍。
正例: 表達邏輯刪除的欄位名 is_deleted, 1 表示刪除, 0 表示未刪除。

 

30. 【強制】 表名、欄位名必須使用小寫字母或數位, 禁止出現數字開頭,禁止兩個下劃線中間只出現數字。

數據庫欄位名的修改代價很大,因爲無法進行預發佈,所以欄位名稱需要慎重考慮。


說明: MySQL 在 Windows 下不區分大小寫,但在 Linux 下預設是區分大小寫。因此,數據庫名、表名、
欄位名,都不允許出現任何大寫字母,避免節外生枝。
正例: aliyun_admin, rdc_config, level3_name
反例: AliyunAdmin, rdcConfig, level_3_name

 

31. 【強制】 禁用保留字,如 desc、 range、 match、 delayed 等, 請參考 MySQL 官方保留字。

解釋: 有些關鍵字,沒注意就衝突了,例如: desc,describe

 

32. 【強制】 主鍵索引名爲 pk_欄位名;唯一索引名爲 uk_欄位名; 普通索引名則爲 idx_欄位名。
說明: pk_ 即 primary key; uk_ 即 unique key; idx_ 即 index 的簡稱

 

33. 【強制】 小數型別爲 decimal,禁止使用 float 和 double。
說明: 在儲存的時候, float 和 double 都存在精度損失的問題,很可能在比較值的時候,得到不正確的
結果。如果儲存的數據範圍超過 decimal 的範圍,建議將數據拆成整數和小數並分開儲存。

 

34. 【強制】 表必備三欄位: id, gmt_create, gmt_modified。
說明: 其中 id 必爲主鍵,型別爲 bigint unsigned、單表時自增、步長爲 1。 gmt_create, gmt_modified的型別均爲 datetime 型別,

前者現在時表示主動式建立,後者過去分詞表示被動式更新。

解釋: 命名可改,三欄位要有,更新數據表記錄時,必須同時更新記錄對應的 gmt_modified 欄位值爲當前時間。

 

35.【推薦】 表的命名最好是遵循「業務名稱_表的作用」 。
正例: alipay_task / force_project / trade_config

 

 

36.【推薦】 欄位允許適當冗餘,以提高查詢效能,但必須考慮數據一致。冗餘欄位應遵循:
1) 不是頻繁修改的欄位。
2) 不是唯一索引的欄位。
3) 不是 varchar 超長欄位,更不能是 text 欄位。
正例: 各業務線經常冗餘儲存商品名稱,避免查詢時需要呼叫 IC 服務獲取。

 

37.【參考】 合適的字元儲存長度,不但節約數據庫表空間、節約索引儲存,更重要的是提升檢索
速度。
正例: 無符號值可以避免誤存負數, 且擴大了表示範圍。

 

38. 【強制】 業務上具有唯一特性的欄位,即使是組合欄位,也必須建成唯一索引。

 

39. 【強制】 超過三個表禁止 join。需要 join 的欄位,數據型別保持絕對一致; 多表關聯查詢時,
保證被關聯的欄位需要有索引。
說明: 即使雙表 join 也要注意表索引、 SQL 效能。


40. 【強制】 在 varchar 欄位上建立索引時,必須指定索引長度,沒必要對全欄位建立索引,根據
實際文字區分度決定索引長度。

說明: 索引的長度與區分度是一對矛盾體,一般對字串型別數據,長度爲 20 的索引,區分度會高達 90%
以上,可以使用 count(distinct left(列名, 索引長度))/count(*)的區分度來確定。

使用字首索引,varchar 長度比較長的時候

 

41. 【推薦】 如果有 order by 的場景,請注意利用索引的有序性。 order by 最後的欄位是組合索引的一部分,

並且放在索引組合順序的最後,避免出現 file_sort 的情況,影響查詢效能。

正例: where a=? and b=? order by c; 索引: a_b_c
反例: 索引如果存在範圍查詢, 那麼索引有序性無法利用,如: WHERE a>10 ORDER BY b; 索引 a_b 無法排序。

42. 【推薦】 利用覆蓋索引來進行查詢操作, 避免回表。

說明: 如果一本書需要知道第 11 章是什麼標題,會翻開第 11 章對應的那一頁嗎?目錄瀏覽一下就好,這
個目錄就是起到覆蓋索引的作用。

正例: 能夠建立索引的種類分爲主鍵索引、唯一索引、普通索引三種,而覆蓋索引只是一種查詢的一種效
果,用 explain 的結果, extra 列會出現: using index。


43. 【推薦】 利用延遲關聯或者子查詢優化超多分頁場景。

說明: MySQL 並不是跳過 offset 行,而是取 offset+N 行,然後返回放棄前 offset 行,返回 N 行,那當
offset 特別大的時候,效率就非常的低下,要麼控制返回的總頁數,要麼對超過特定閾值的頁數進行 SQL改寫。
正例: 先快速定位需要獲取的 id 段,然後再關聯:
SELECT a.* FROM 表 1 a, (select id from 表 1 where 條件 LIMIT 100000,20 ) b where a.id=b.id

 

44. 【強制】 使用 ISNULL()來判斷是否爲 NULL 值。

說明: NULL 與任何值的直接比較都爲 NULL。
1) NULL<>NULL 的返回結果是 NULL, 而不是 false。
2) NULL=NULL 的返回結果是 NULL, 而不是 true。
3) NULL<>1 的返回結果是 NULL,而不是 true。
反例: 在 SQL 語句中,如果在 null 前換行,影響可讀性。 select * from table where column1 is null and
column3 is not null; 而`ISNULL(column)`是一個整體,簡潔易懂。從效能數據上分析, `ISNULL(column)`
執行效率更快一些

 

45. 【強制】 不得使用外來鍵與級聯,一切外來鍵概念必須在應用層解決。
說明: (概念解釋) 學生表中的 student_id 是主鍵,那麼成績表中的 student_id 則爲外來鍵。如果更新學
生表中的 student_id,同時觸發成績表中的 student_id 更新, 即爲級聯更新。外來鍵與級聯更新適用於單機
低併發,不適合分佈式、高併發叢集;級聯更新是強阻塞,存在數據庫更新風暴的風險;外來鍵影響數據庫
的插入速度。

46. 【強制】 禁止使用儲存過程,儲存過程難以偵錯和擴充套件,更沒有移植性。


47. 【強制】 數據訂正(特別是刪除或修改記錄操作) 時,要先 select,避免出現誤刪除,確認無
誤才能 纔能執行更新語句。

 

 

48. 【強制】 對於數據庫中表記錄的查詢和變更,只要涉及多個表,都需要在列名前加表的別名(或表名)進行限定。
說明: 對多表進行查詢記錄、更新記錄、刪除記錄時,如果對操作列沒有限定表的別名(或表名),並且操作列在多個表中存在時,就會拋異常。
正例: select t1.name from table_first as t1 , table_second as t2 where t1.id=t2.id;
反例: 在某業務中,由於多表關聯查詢語句沒有加表的別名(或表名)的限制,正常執行兩年後,最近在
某個表中增加一個同名欄位,在預發佈環境做數據庫變更後,線上查詢語句出現出 1052 異常: Column
'name' in field list is ambiguous。


49. 【推薦】 不要寫一個大而全的數據更新介面。

反例: 傳入爲 POJO 類,不管是不是自己的目標更新欄位,都進行 update table set c1=value1,c2=value2,c3=value3; 這是不對的。

執行 SQL 時,不要更新無改動的欄位,一是易出錯;二是效率低;三是增加 binlog 儲存。

 

50. 【推薦】 @Transactional 事務不要濫用。事務會影響數據庫的 QPS,另外使用事務的地方需要考慮各方面的回滾方案,

包括快取回滾、搜尋引擎回滾、訊息補償、統計修正等

 

伺服器相關:

 

51. 【強制】 調大伺服器所支援的最大檔案控制代碼數( File Descriptor,簡寫爲 fd) 。

說明: 主流操作系統的設計是將 TCP/UDP 連線採用與檔案一樣的方式去管理,即一個連線對應於一個 fd。
主流的linux伺服器預設所支援最大fd數量爲1024,當併發連線數很大時很容易因爲 fd不足而出現「open Java too many files」 錯誤,導致新的連線無法建立。

建議將 linux 伺服器所支援的最大控制代碼數調高數倍(與伺服器的記憶體數量相關)。


52. 【強制】給 JVM 環境參數設定-XX:+HeapDumpOnOutOfMemoryError 參數,讓 JVM 碰到 OOM場景時輸出 dump 資訊。

說明: OOM 的發生是有概率的,甚至相隔數月纔出現一例,出錯時的堆內資訊對解決問題非常有幫助。

 

53. 【推薦】 在線上生產環境, JVM 的 Xms 和 Xmx 設定一樣大小的記憶體容量, 避免在 GC 後調整堆大小帶來的壓力。

解釋: xms 初始化堆大小, xmx 最大堆大小,至於其他jvm 參數,包括新生代老年代比例, meta spece大小,

需要根據特定場景特定分析, 和是否要new 物件的速率,和希望 minor gc 和 full gc 頻率來判斷。