JAVA程式碼程式設計規範
一、程式設計規範
(一)命名規約
1【強制】POJO 類中布林型別的變數,都不要加is
,否則部分框架解析會引起序列化錯誤。
- 反例:定義為基本資料型別
boolean isSuccess;
的屬性,它的方法也是isSuccess()
,RPC 框架在反向解析的時候,“以為”對應的屬性名稱是success
,導致屬性獲取不到,進而丟擲異常。
2【推薦】介面類中的方法和屬性不要加任何修飾符號(public 也不要加),保持程式碼的簡潔性,並加上有效的 Javadoc 註釋。儘量不要在接口裡定義變數,如果一定要定義變數,肯定是與介面方法有關,並且是整個應用的基礎常量。
- 正例:介面方法簽名
void f();
String COMPANY = "alibaba";
- 反例:介面方法定義為
public abstract void f();
- 說明:JDK8 中介面允許有預設實現,那麼這個
default
方法,是對所有實現類都有價值的預設實現。
(二)常量定義
1【強制】不允許出現任何魔法值(即未經定義的常量)直接出現在程式碼中。
- 反例:
String key = "Id#taobao_" + tradeId;
cache.put(key, value);
2【強制】long
或者Long
初始賦值時,必須使用大寫的L
,不能是小寫的l
,小寫容易跟數字1
混淆,造成誤解。
- 說明:
Long a = 2l;
21
,還是Long
型的2
?
(三)格式規約
1【強制】任何運算子左右必須加一個空格。
- 說明:運算子包括賦值運算子
=
、邏輯運算子&&
、加減乘除符號、三目運算子等。
2【強制】方法引數在定義和傳入時,多個引數逗號後邊必須加空格。
- 正例:
method("a", "b", "c");
無論是引數a
還是引數b
後面皆有空格。
(四)OOP規約
1【強制】避免通過一個類的物件引用訪問此類的靜態變數或靜態方法,無謂增加編譯器解析成本,直接用類名來訪問即可。
2【強制】Object
的equals
方法容易拋空指標異常,應使用常量或確定有值的物件來呼叫equals
。
- 正例:
"test".equals(object);
- 反例:
object.equals("test");
- 說明:推薦使用
java.util.Objects#equals
(JDK7 引入的工具類)
3【強制】所有的相同型別的包裝類物件之間值得比較,全部使用equals
方法比較。
- 說明:對於
Integer var = ?
在-128
至127
之間的賦值,Integer
物件是在IntegerCache.cache
產生,會複用已有物件,這個區間內的Integer
值可以直接使用==
進行判斷,但是這個區間之外的所有資料,都會在堆上產生,並不會複用已有物件,這是一個大坑,推薦使用equals
方法進行判斷。
4【強制】關於基本資料型別與包裝資料型別的使用標準為:所有的 POJO 類屬性必須使用包裝資料型別;RPC 方法的返回值和引數必須使用包裝資料型別;所有的區域性變數推薦使用基本資料型別。
- 說明:POJO 類屬性沒有初值是提醒使用者在需要使用時,必須自己顯示地進行賦值,任何 NPE 問題,或者入庫檢查,都是使用者來保證。
- 正例:資料庫的查詢結果可能是
null
,因為自動拆箱,用基本資料型別接收有 NPE 風險。 - 反例:比如顯示成交總額漲跌情況,即正負
x%
,x
為基本資料型別,呼叫的 RPC 服務,呼叫不成功時,返回的是預設值,頁面顯示0%
,這是不合理的,應該顯示成中劃線-
,所以包裝資料型別的null
值,能夠表示額外的資訊,如遠端呼叫失敗,異常退出。
5【強制】序列化類新增屬性時,請不要修改serialVersionUID
欄位,避免反序列失敗;如果完全不相容升級,避免反序列化混亂,那麼請修改serialVersionUID
值。
- 說明:注意
serialVersionUID
不一致會丟擲序列化執行時異常。
6【推薦】迴圈體內,字串的聯接方式,使用StringBuilder
的append
方法進行擴充套件。
- 反例:
String str = "start";
for(int i = 0; i < 100; i++){
str = str + "hello";
}
- 說明:反編譯出的位元組碼檔案顯示每次迴圈都會
new
出一個StringBuilder
。物件,然後進行sppend
操作,最後通過toString
方法返回String
物件,造成記憶體資源浪費。
7【推薦】慎用Object
的clone
方法來拷貝物件。
- 說明:物件的
clone
方法預設是淺拷貝,若想實現深拷貝需要重寫clone
方法實現屬性物件的拷貝。
(五)集合處理
1【強制】關於hashCode
和equals
的處理,遵循規則:只要重寫equals
,就必須重寫hashCode
;因為Set
儲存的是不重複的物件,依據hashCode
和equals
進行判斷,所以Set
儲存的物件必須重寫這兩個方法;如果自定義物件作為Map
的鍵,那麼必須重寫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【強制】使用工具類Arrays.asList()
把陣列轉換成集合時,不能使用其修改集合相關的方法,它的add/remove/clear
方法會丟擲UnsupportedOperationException
異常。
- 說明:
asList
的返回物件是一個Arrays
內部類,並沒有實現集合的修改方法。Arrays.asList
體現的是介面卡模式,只是轉換介面,後臺的資料仍然是陣列。
4【推薦】使用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
值組合集合。
5【推薦】高度注意Map
類集合K/V
能不能儲存null
值的情況,如下表格:
集合類 | Key | Value | Supper | 說明 |
---|---|---|---|---|
Hashtable | 不允許為null |
不允許為null |
Dictionary | 執行緒安全 |
ConcurrentHashMap | 不允許為null |
不允許為null |
AbstractMap | 分段鎖技術 |
TreeMap | 不允許為null |
允許為null |
AbstractMap | 執行緒不安全 |
HashMap | 允許為null |
允許為null |
AbstractMap | 執行緒不安全 |
- 反例:由於
HashMap
的干擾,很多人認為ConcurrentHashMap
是可以置入null
值,注意儲存null
值時會丟擲 NPE 異常。
(六)併發處理
1【強制】執行緒資源必須通過執行緒池提供,不允許在應用中自行顯示建立執行緒。
- 正例:使用執行緒池的好處是減少在建立和銷燬執行緒上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用執行緒池,有可能系統建立大量同類執行緒而導致消耗完記憶體或者“過度切換”的問題。
2【強制】執行緒池不允許使用Executors
去建立,而是通過ThreadPoolExecutor
的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。
- 說明:
Executors
返回的執行緒池物件的弊端如下, FixedThreadPool
和SingleThreadPool
,允許的請求佇列長度為Integer.MAX_VALUS
,可能會堆積大量的請求,從而導致 OOM。CachedThreadPool
和ScheduledThreadPool
,允許的建立執行緒數量為Integer.MAX_VALUS
,可能會建立大量的執行緒,從而導致 OOM。
3【強制】高併發時,同步呼叫應該去考量鎖的效能損耗。能用無鎖資料結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用物件鎖,就不要用類鎖。
4【強制】併發修改同一記錄時,避免更新丟失,要麼在應用層加鎖,要麼在快取加鎖,要麼在資料庫層使用樂觀鎖,使用version
作為更新依據。
- 說明:如果每次訪問衝突概率小於
20%
,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於3
次。
5【強制】多執行緒並行處理定時任務時,Timer
執行多個TimerTask
時,只要其中之一沒有捕獲丟擲異常,其他任務便會自動終止執行,使用ScheduledExecutorService
則沒有這個問題。
(七)控制語句
1【推薦】儘量少用else
,if-eles
的方式可以改寫成:
if(condition){
...
return obj;
}
// 接著寫 else 的業務邏輯程式碼
- 說明:如果非得使用
if()...else if()...else...
方式表達邏輯,請勿超過 3 層,超過請使用狀態設計模式。 - 正例:邏輯上超過 3 層的
if-eles
程式碼可以使用衛語句,或者狀態模式來實現。
2【推薦】除常用方法(如getXxx/isXxx
)等外,不用在條件判斷中執行其它複雜的語句,將複雜邏輯判斷的結果賦值給一個有意義的布林變數名,以提高可讀性。
- 說明:很多
if
語句內的邏輯相當複雜,閱讀者需要分析條件表示式的最終結果,才能確定什麼樣的條件執行什麼樣的語句,那麼,如果閱讀者分析邏輯表示式錯誤呢? - 正例:
// 虛擬碼如下
boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if(existed){
...
}
- 反例:
if((file.open(fileName, "w") != null) && (...) || (...)){
...
}
(八)註釋規約
1【參考】註釋掉的程式碼儘快要配合說明,而不是簡單的註釋掉。
- 說明:程式碼被註釋掉有兩種可能性,
1)
後續會恢復此段程式碼邏輯;2)
永久不用。前者如果沒有備註資訊,難以知悉註釋動機;後者建議直接刪掉(程式碼倉庫保持了歷史程式碼)。
2【參考】特殊註釋標記,請註明標記人與標記時間。注意及時處理這些標記,通過標記掃描,經常清理此類標記。線上故障有時候就是來源於這些標記處的程式碼。
- 說明 1:待辦事宜
(TODO):(標記人,標記時間,[預處理時間])
,表示需要實現,但目前還未實現的功能。這實際上是一個 Javadoc 的標籤,目前的 Javadoc 還沒有實現,但已經被廣泛使用。只能應用於類,介面和方法,原因就在於它是一個 Javadoc 標籤。 - 說明 2:錯誤,不能工作
(FIXME):(標記人,標記時間,[預處理時間])
,在註釋中用FIXME
標記某程式碼是錯誤的,而且不能工作,需要及時糾正的情況。
(九)其他
1【強制】後臺輸送給頁面的變數必須加$!{var}
——中間的感嘆號。
- 說明:如果
var = null
或者不存在,那麼${var}
會直接顯示在頁面上。
2【強制】注意Math.random()
這個方法返回時double
型別, 注意取值的範圍0 ≤ x < 1
(能夠取到零值,注意除零異常),如果想獲取整數型別的隨機數,不要將x
放大10
的若干倍然後取整,直接使用Random
物件的nextInt
或者nextLong
方法。
3【強制】獲取當前毫秒數System.currentTimeMillis();
而不是new Date().getTime();
- 說明:如果想獲取更加精確的納秒時間值,用
System.nanoTime()
。在 JDK8 中,針對統計時間等場景,推薦使用Instant
類。
二、異常日誌
(一)異常處理
1【強制】不能在finally
塊中使用return
,finally
塊中的return
返回後方法結束執行,不會再執行try
塊中的return
語句。
2【推薦】防止 NPE 是程式設計師的基本修養,注意 NPE 產生的場景:
- 返回型別為包裝資料型別,有可能是
null
,返回int
值時注意判空; - 資料庫的查詢語句可能為
null
; - 集合裡的元素即使
isNotEmpty
,取出的資料元素可能為null
; - 遠端呼叫返回物件,一律要求進行 NPE 判斷;
- 對於 Session 中獲取的資料,建議 NPE 檢查,避免空指標。
3【參考】避免出現重複的程式碼(Dont’t Repeat Yourself),即 DRY 原則。
- 說明:隨意複製和貼上程式碼,必然會導致程式碼的重複,在以後需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是共用模組。
- 正例:一個類中有多個
public
方法,都需要進行數行相同的引數校驗操作,這個時候請抽取, private boolean checkParam(DTO dto){...}
(二)日誌規約
1【強制】應用中不可直接使用日誌系統(Log4j、Logback)中的 API,而應依賴使用日誌框架 SLF4J 中的 API,使用門面模式的日誌框架,有利於維護和各個類的日誌處理方式統一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
2【強制】對trace/debug/info
級別的日誌輸出,必須使用條件輸出形式或者使用佔位符的方式。
- 說明:
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
如果日誌級別是warn
,上述日誌不會列印,但是會執行字串拼接操作,如果symbol
是物件,會執行toString()
方法,浪費了系統資源,執行了上述操作,最終日誌卻沒有列印。 - 正例:(條件)
if(logger.isDebugEnabled()){
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
}
- 正例:(佔位符)
logger.debug("Processing trade with id: {} symbol: {}", id, symbol);
- 1
3【推薦】謹慎地記錄日誌。生產環節禁止輸出debug
日誌;有選擇地輸出info
日誌;如果使用warn
來記錄剛上線時的業務行為資訊,一定要注意日誌輸出量的問題,避免把伺服器磁碟撐爆,並記得及時刪除這些觀察日誌。
- 說明:這些日誌真的有人看嗎?看到這條日誌你能做什麼?能不能給問題排查帶來好處?
三、MySQL 規約
(一)建表規約
1【強制】表名、欄位名必須使用小寫字母或數字;禁止出現數字開頭,禁止兩個下劃線中間只出現數字。資料庫欄位名的修改代價很大,因為無法進行預釋出,所以欄位名稱需要慎重考慮。
- 正例:
getter_admin, task_config, level3_name
- 反例:
GetterAdmin, taskConfig, level_3_name
2【強制】唯一索引名為uk_欄位名
;普通索引名則為idx_欄位名
。
- 說明:
uk_
即unique key
;idx_
即index
的簡稱。
3【強制】小數型別為decimal
,禁止使用float
和double
。
- 說明:
float
和double
在儲存的時候,存在精度損失的問題,很可能在值的比較時,得到不正確的結果。如果儲存的資料範圍超過decimal
的範圍,建議將資料拆成整數和小數分開儲存。
4【強制】varchar
是可變長字串,不預先分配儲存空間,長度不要超過 5000,如果儲存長度大於此值,定義欄位型別為text
,獨立出來一張表,用主鍵來對應,避免影響其他欄位索引效率。
5【強制】表必備三欄位:id
,gmt_create
,gmt_modified
。
- 說明:其中
id
必為主鍵,型別為unsigned bigint
、單表時自增、步長為1
;gmt_create
和gmt_modified
的型別均為date_time
型別。
(二)索引規約
1【強制】業務上具有唯一特性的欄位,即使是組合欄位,也必須建成唯一索引。
- 說明:不用以為唯一索引影響了
insert
速度,這個速度損耗可以忽略,但提高查詢速度是明顯的;另外,即使在應用層做了非常完善的校驗和控制,只要沒有唯一索引,根據墨菲定律,必然有髒資料產生。
2【強制】超過三個表禁止join
。需要join
的欄位,資料型別保持絕對一致;多表關聯查詢時,保證被關聯的欄位需要有索引。
- 說明:即使雙表
join
也要注意表索引、SQL 效能。
3【強制】在varchar
欄位上建立索引時,必須指定索引長度,沒必要對全欄位建立索引,根據實際文字區分度決定索引長度。
- 說明:索引的長度與區分度是一對矛盾體,一般對字串型別資料,長度為 20 的索引,區分度會高達 90% 以上,可以使用
count(distinct left(列名,索引長度))/count(*)
的區分度來確定。
(三)SQL規約
1【強制】不要使用count(列名)
或count(常量)
來替代count(*)
,count(*)
就是 SQL92 定義的標準統計行數的語法,跟資料庫無關,跟 NULL 和非 NULL 無關。
- 說明:
count(*)
會統計值為 NULL 的行,而count(列名)
不會統計此列為 NULL 值的行。
2【強制】count(distinct col)
計算該列除 NULL 之外的不會重複數量。注意count(distinct col1, col2)
如果其中一列全為 NULL,那麼即使另一列有不同的值,也返回為0
。
(四)ORM規約
1【強制】在表查詢中,一律不要使用*
作為查詢的欄位列表,需要哪些欄位必須明確寫明。
- 說明:1)增加查詢分析器解析成本;2)增減欄位容易與
resultMap
配置不一致。
2【強制】不要用resultClass
當返回引數,即使所有類屬性名與資料庫欄位一一對應,也需要定義;反過來,每一個表也必然有一個與之對應。
- 說明:配置對映關係,使欄位與 DO 類解耦,方便維護。
3【強制】xml
配置中引數注意使用:#{}
,#param#
不要使用${}
,此種方式容易出現 SQL 注入。
四、其他
1【強制】依賴於一個二方庫群時,必須定義一個統一版本變數,避免版本號不一致。
- 說明:依賴
springframework-core, -context, -beans
,它們都是同一個版本,可以定義一個變數來保持版本:${spring.version}
,定義依賴的時候,引用該版本。
2【強制】禁止在子專案的POM
依賴中出現相同的GroupId
,相同的ArtifiactId
,但是不同的Version
。
- 說明:在本地除錯時會使用各子專案指定的版本號,但是合併成一個
war
,只能有一個版本號出現在最後的lib
目錄中。曾經出現過線下除錯是正確的,釋出到線上出故障的先例。
3【強制】使用者輸入的 SQL 引數嚴格使用引數繫結或者 METADATA 欄位值限定,防止 SQL 注入。