《唯品會Java開發手冊》1.0.2版閱讀
1. 概述
《阿里巴巴Java開發手冊》,是首個對外公佈的企業級Java開發手冊,對整個業界都有重要的意義。
我們結合唯品會的內部經驗,參考《Clean Code》、《Effective Java》等重磅資料,增補了一些條目,也做了些精簡。
感謝阿里授權我們定製和再發布。
2. 規範正文
-
命名規
注意: 如需全文pdf版,請下載原始碼,在docs/standard/目錄執行merge.sh生成。
3. 規範落地
規則落地主要依靠程式碼格式模版與Sonar程式碼規則檢查。
其中Sonar規則不如人意的地方,我們進行了定製。
4. 參考資料
5. 定製記錄
(一) 命名規約
Rule 1. 【強制】禁止拼音縮寫,避免閱讀者費勁猜測;儘量不用拼音,除非中國式業務詞彙沒有通用易懂的英文對應。
禁止: DZ[打折] / getPFByName() [評分]
儘量避免:Dazhe / DaZhePrice
Rule 2. 【強制】禁止使用非標準的英文縮寫
反例: AbstractClass 縮寫成 AbsClass;condition 縮寫成 condi。
Rule 3. 【強制】禁用其他程式語言風格的字首和字尾
在其它程式語言中使用的特殊字首或字尾,如_name
, name_
, mName
, i_name
,在Java中都不建議使用。
Rule 4. 【推薦】命名的好壞,在於其“模糊度”
1)如果上下文很清晰,區域性變數可以使用 list
這種簡略命名, 否則應該使用 userList
這種更清晰的命名。
2)禁止 a1, a2, a3
這種帶編號的沒誠意的命名方式。
3)方法的引數名叫 bookList
,方法裡的區域性變數名叫 theBookList
也是很沒誠意。
4)如果一個應用裡同時存在 Account、AccountInfo、AccountData
類,或者一個類裡同時有 getAccountInfo()、getAccountData()
, save()、 store()
的函式,閱讀者將非常困惑。
5) callerId
與 calleeId
, mydearfriendswitha
與 mydearfriendswithb
這種拼寫極度接近,考驗閱讀者眼力的。
Rule 5. 【推薦】包名全部小寫。點分隔符之間儘量只有一個英語單詞,即使有多個單詞也不使用下劃線或大小寫分隔
正例: com.vip.javatool
反例: com.vip.java_tool, com.vip.javaTool
Rule 6. 【強制】類名與介面名使用UpperCamelCase風格,遵從駝峰形式
Tcp, Xml等縮寫也遵循駝峰形式,可約定例外如:DTO/ VO等。
正例:UserId / XmlService / TcpUdpDeal / UserVO
反例:UserID / XMLService / TCPUDPDeal / UserVo
- Sonar-101:Class names should comply with a naming convention
- Sonar-114:Interface names should comply with a naming convention
Rule 7. 【強制】方法名、引數名、成員變數、區域性變數使用lowerCamelCase風格,遵從駝峰形式
正例: localValue / getHttpMessage();
- Sonar-100:Method names should comply with a naming convention
- Sonar-116:Field names should comply with a naming convention
- Sonar-117:Local variable and method parameter names should comply with a naming convention
Rule 8. 【強制】常量命名全大寫,單詞間用下劃線隔開。力求語義表達完整清楚,不要嫌名字長
正例: MAX_STOCK_COUNT
反例: MAX_COUNT
例外:當一個static final欄位不是一個真正常量,比如不是基本型別時,不需要使用大寫命名。
private static final Logger logger = Logger.getLogger(MyClass.class);
例外:列舉常量推薦全大寫,但如果歷史原因未遵循也是允許的,所以我們修改了Sonar的規則。
- Sonar-115:Constant names should comply with a naming convention
- Sonar-308:Static non-final field names should comply with a naming convention
Rule 9. 【推薦】如果使用到了通用的設計模式,在類名中體現,有利於閱讀者快速理解設計思想
正例:OrderFactory, LoginProxy ,ResourceObserver
Rule 10. 【推薦】列舉類名以Enum結尾; 抽象類使用Abstract或Base開頭;異常類使用Exception結尾;測試類以它要測試的類名開始,以Test結尾
正例:DealStatusEnum, AbstractView,BaseView, TimeoutException,UserServiceTest
- Sonar-2166:Classes named like "Exception" should extend "Exception" or a subclass
- Sonar-3577:Test classes should comply with a naming convention
Rule 11. 【推薦】實現類儘量用Impl的字尾與介面關聯,除了形容能力的介面
正例:CacheServiceImpl 實現 CacheService介面。
正例: Foo 實現 Translatable介面。
Rule 12. 【強制】POJO類中布林型別的變數名,不要加is字首,否則部分框架解析會引起序列化錯誤
反例:Boolean isSuccess的成員變數,它的GET方法也是isSuccess(),部分框架在反射解析的時候,“以為”對應的成員變數名稱是success,導致出錯。
Rule 13. 【強制】避免成員變數,方法引數,區域性變數的重名複寫,引起混淆
- 類的私有成員變數名,不與父類的成員變數重名
- 方法的引數名/區域性變數名,不與類的成員變數重名 (getter/setter例外)
下面錯誤的地方,Java在編譯時很坑人的都是合法的,但給閱讀者帶來極大的障礙。
public class A {
int foo;
}
public class B extends A {
int foo; //WRONG
int bar;
public void hello(int bar) { //WRONG
int foo = 0; //WRONG
}
public void setBar(int bar) { //OK
this.bar = bar;
}
}
- Sonar-2387: Child class fields should not shadow parent class fields
- Sonar: Local variables should not shadow class fields
(二) 格式規約
Rule 1. 【強制】使用專案組統一的程式碼格式模板,基於IDE自動的格式化
1)IDE的預設程式碼格式模板,能簡化絕大部分關於格式規範(如空格,括號)的描述。
2)統一的模板,並在接手舊專案先進行一次全面格式化,可以避免, 不同開發者之間,因為格式不統一產生程式碼合併衝突,或者程式碼變更日誌中因為格式不同引起的變更,掩蓋了真正的邏輯變更。
3)設定專案組統一的行寬,建議120。
4)設定專案組統一的縮排方式(Tab或二空格,四空格均可),基於IDE自動轉換。
Rule 2. 【強制】IDE的text file encoding設定為UTF-8; IDE中檔案的換行符使用Unix格式,不要使用Windows格式
Rule 3. 【推薦】 用小括號來限定運算優先順序
我們沒有理由假設讀者能記住整個Java運算子優先順序表。除非作者和Reviewer都認為去掉小括號也不會使程式碼被誤解,甚至更易於閱讀。
if ((a == b) && (c == d))
- Sonar-1068:Limited dependence should be placed on operator precedence rules in expressions,我們修改了三目運算子
foo!=null?foo:""
不需要加括號。
Rule 4. 【推薦】類內方法定義的順序,不要“總是在類的最後新增新方法”
一個類就是一篇文章,想象一個閱讀者的存在,合理安排方法的佈局。
1)順序依次是:建構函式 > (公有方法>保護方法>私有方法) > getter/setter方法。
如果公有方法可以分成幾組,私有方法也緊跟公有方法的分組。
2)當一個類有多個構造方法,或者多個同名的過載方法,這些方法應該放置在一起。其中引數較多的方法在後面。
public Foo(int a) {...}
public Foo(int a, String b) {...}
public void foo(int a) {...}
public void foo(int a, String b) {...}
3)作為呼叫者的方法,儘量放在被呼叫的方法前面。
public void foo() {
bar();
}
public void bar() {...}
Rule 5. 【推薦】通過空行進行邏輯分段
一段程式碼也是一段文章,需要合理的分段而不是一口氣讀到尾。
不同組的變數之間,不同業務邏輯的程式碼行之間,插入一個空行,起邏輯分段的作用。
而聯絡緊密的變數之間、語句之間,則儘量不要插入空行。
int width;
int height;
String name;
Rule 6.【推薦】避免IDE格式化
對於一些特殊場景(如使用大量的字串拼接成一段文字,或者想把大量的列舉值排成一列),為了避免IDE自動格式化,土辦法是把註釋符號//加在每一行的末尾,但這有視覺的干擾,可以使用@formatter:off和@formatter:on來包裝這段程式碼,讓IDE跳過它。
// @formatter:off
...
// @formatter:on
(三) 註釋規約
Rule 1.【推薦】基本的註釋要求
完全沒有註釋的大段程式碼對於閱讀者形同天書,註釋是給自己看的,即使隔很長時間,也能清晰理解當時的思路;註釋也是給繼任者看的,使其能夠快速接替自己的工作。
程式碼將被大量後續維護,註釋如果對閱讀者有幫助,不要吝嗇在註釋上花費的時間。(但也綜合參見規則2,3)
第一、能夠準確反應設計思想和程式碼邏輯;第二、能夠描述業務含義,使別的程式設計師能夠迅速瞭解到程式碼背後的資訊。
除了特別清晰的類,都儘量編寫類級別註釋,說明類的目的和使用方法。
除了特別清晰的方法,對外提供的公有方法,抽象類的方法,同樣儘量清晰的描述:期待的輸入,對應的輸出,錯誤的處理和返回碼,以及可能丟擲的異常。
Rule 2. 【推薦】通過更清晰的程式碼來避免註釋
在編寫註釋前,考慮是否可以通過更好的命名,更清晰的程式碼結構,更好的函式和變數的抽取,讓程式碼不言自明,此時不需要額外的註釋。
Rule 3. 【推薦】刪除空註釋,無意義註釋
《Clean Code》建議,如果沒有想說的,不要留著IDE自動生成的,空的@param,@return,@throws 標記,讓程式碼更簡潔。
反例:方法名為put,加上兩個有意義的變數名elephant和fridge,已經說明了這是在幹什麼,不需要任何額外的註釋。
/**
* put elephant into fridge.
*
* @param elephant
* @param fridge
* @return
*/
public void put(Elephant elephant, Fridge fridge);
Rule 4.【推薦】避免建立人,建立日期,及更新日誌的註釋
程式碼後續還會有多人多次維護,而建立人可能會離職,讓我們相信原始碼版本控制系統對更新記錄能做得更好。
Rule 5. 【強制】程式碼修改的同時,註釋也要進行相應的修改。尤其是引數、返回值、異常、核心邏輯等的修改
Rule 6. 【強制】類、類的公有成員、方法的註釋必須使用Javadoc規範,使用/* xxx */格式,不得使用//xxx
方式*
正確的JavaDoc格式可以在IDE中,檢視呼叫方法時,不進入方法即可懸浮提示方法、引數、返回值的意義,提高閱讀效率。
Rule 7. 【推薦】JavaDoc中不要為了HTML格式化而大量使用HTML標籤和轉義字元
如果為了Html版JavaDoc的顯示,大量使用
這樣的html標籤,以及<
"
這樣的html轉義字元,嚴重影響了直接閱讀程式碼時的直觀性,而直接閱讀程式碼的機率其實比看Html版的JavaDoc大得多。
另外IDE對JavaDoc的格式化也要求``之類的標籤來換行,可以配置讓IDE不對JavaDoc的格式化。
Rule 8. 【推薦】註釋不要為了英文而英文
如果沒有國際化要求,中文能表達得更清晰時還是用中文。
Rule 9. 【推薦】TODO標記,清晰說明代辦事項和處理人
清晰描述待修改的事項,保證過幾個月後仍然能夠清楚要做什麼修改。
如果近期會處理的事項,寫明處理人。如果遠期的,寫明提出人。
通過IDE和Sonar的標記掃描,經常清理此類標記,線上故障經常來源於這些標記但未處理的程式碼。
正例:
//TODO:calvin use xxx to replace yyy.
反例:
//TODO: refactor it
Rule 10. 【推薦】合理處理註釋掉的程式碼
如果後續會恢復此段程式碼,在目的碼上方用///
說明註釋動機,而不是簡單的註釋掉程式碼。
如果很大概率不再使用,則直接刪除(版本管理工具儲存了歷史程式碼)。
(四) 方法設計
Rule 1. 【推薦】方法的長度度量
方法儘量不要超過100行,或其他團隊共同商定的行數。
另外,方法長度超過8000個位元組碼時,將不會被JIT編譯成二進位制碼。
- Sonar-107: Methods should not have too many lines,預設值改為100
- Facebook-Contrib:Performance - This method is too long to be compiled by the JIT
Rule 2. 【推薦】方法的語句在同一個抽象層級上
反例:一個方法裡,前20行程式碼在進行很複雜的基本價格計算,然後呼叫一個折扣計算函式,再呼叫一個贈品計算函式。
此時可將前20行也封裝成一個價格計算函式,使整個方法在同一抽象層級上。
Rule 3. 【推薦】為了幫助閱讀及方法內聯,將小概率發生的異常處理及其他極小概率進入的程式碼路徑,封裝成獨立的方法
if(seldomHappenCase) {
hanldMethod();
}
try {
...
} catch(SeldomHappenException e) {
handleException();
}
Rule 4. 【推薦】儘量減少重複的程式碼,抽取方法
超過5行以上重複的程式碼,都可以考慮抽取公用的方法。
Rule 5. 【推薦】方法引數最好不超過3個,最多不超過7個
1)如果多個引數同屬於一個物件,直接傳遞物件。
例外: 你不希望依賴整個物件,傳播了類之間的依賴性。
2)將多個引數合併為一個新建立的邏輯物件。
例外: 多個引數之間毫無邏輯關聯。
3)將函式拆分成多個函式,讓每個函式所需的引數減少。
Rule 6.【推薦】下列情形,需要進行引數校驗
1) 呼叫頻次低的方法。
2) 執行時間開銷很大的方法。此情形中,引數校驗時間幾乎可以忽略不計,但如果因為引數錯誤導致中間執行回退,或者錯誤,代價更大。
3) 需要極高穩定性和可用性的方法。
4) 對外提供的開放介面,不管是RPC/HTTP/公共類庫的API介面。
如果使用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);
Rule 7.【推薦】下列情形,不需要進行引數校驗
1) 極有可能被迴圈呼叫的方法。
2) 底層呼叫頻度比較高的方法。畢竟是像純淨水過濾的最後一道,引數錯誤不太可能到底層才會暴露問題。
比如,一般DAO層與Service層都在同一個應用中,所以DAO層的引數校驗,可以省略。
3) 被宣告成private,或其他只會被自己程式碼所呼叫的方法,如果能夠確定在呼叫方已經做過檢查,或者肯定不會有問題則可省略。
即使忽略檢查,也儘量在方法說明裡註明引數的要求,比如vjkit中的@NotNull,@Nullable標識。
Rule 8.【推薦】禁用assert做引數校驗
assert斷言僅用於測試環境除錯,無需在生產環境時進行的校驗。因為它需要增加-ea啟動引數才會被執行。而且校驗失敗會丟擲一個AssertionError(屬於Error,需要捕獲Throwable)
因此在生產環境進行的校驗,需要使用Apache Commons Lang的Validate或Guava的Precondition。
Rule 9.【推薦】返回值可以為Null,可以考慮使用JDK8的Optional類
不強制返回空集合,或者空物件。但需要新增註釋充分說明什麼情況下會返回null值。
本手冊明確防止NPE是呼叫者的責任
。即使被呼叫方法返回空集合或者空物件,對呼叫者來說,也並非高枕無憂,必須考慮到遠端呼叫失敗、序列化失敗、執行時異常等場景返回null的情況。
JDK8的Optional類的使用這裡不展開。
Rule 10.【推薦】返回值可以為內部陣列和集合
如果覺得被外部修改的可能性不大,或沒有影響時,不強制在返回前包裹成Immutable集合,或進行陣列克隆。
Rule 11.【推薦】不能使用有繼承關係的引數型別來過載方法
因為方法過載的引數型別是根據編譯時表面型別匹配的,不根據執行時的實際型別匹配。
class A {
void hello(List list);
void hello(ArrayList arrayList);
}
List arrayList = new ArrayList();
// 下句呼叫的是hello(List list),因為arrayList的定義型別是List
a.hello(arrayList);
Rule 12.【強制】正被外部呼叫的介面,不允許修改方法簽名,避免對介面的呼叫方產生影響
只能新增新介面,並對已過時介面加@Deprecated註解,並清晰地說明新介面是什麼。
Rule 13.【推薦】不使用@Deprecated
的類或方法
介面提供方既然明確是過時介面並提供新介面,那麼作為呼叫方來說,有義務去考證過時方法的新實現是什麼。
比如java.net.URLDecoder 中的方法decode(String encodeStr) 這個方法已經過時,應該使用雙引數decode(String source, String encode)。
Rule 14.【推薦】不使用不穩定方法,如com.sun.*包下的類,底層類庫中internal包下的類
com.sun.*
,sun.*
包下的類,或者底層類庫中名稱為internal的包下的類,都是不對外暴露的,可隨時被改變的不穩定類。
(五) 類設計
Rule 1. 【推薦】類成員與方法的可見性最小化
任何類、方法、引數、變數,嚴控訪問範圍。過於寬泛的訪問範圍,不利於模組解耦。思考:如果是一個private的方法,想刪除就刪除,可是一個public的service方法,或者一個public的成員變數,刪除一下,不得手心冒點汗嗎?
例外:為了單元測試,有時也可能將訪問範圍擴大,此時需要加上JavaDoc說明或vjkit中的@VisibleForTesting
註解。
Rule 2.【推薦】 減少類之間的依賴
比如如果A類只依賴B類的某個屬性,在建構函式和方法引數中,只傳入該屬性。讓閱讀者知道,A類只依賴了B類的這個屬性,而不依賴其他屬性,也不會呼叫B類的任何方法。
a.foo(b); //WRONG
a.foo(b.bar); //RIGHT
Rule 3.【推薦】 定義變數與方法引數時,儘量使用介面而不是具體類
使用介面可以保持一定的靈活性,也能向讀者更清晰的表達你的需求:變數和引數只是要求有一個Map,而不是特定要求一個HashMap。
例外:如果變數和引數要求某種特殊型別的特性,則需要清晰定義該引數型別,同樣是為了向讀者表達你的需求。
Rule 4. 【推薦】類的長度度量
類儘量不要超過300行,或其他團隊共同商定的行數。
對過大的類進行分拆時,可考慮其內聚性,即類的屬性與類的方法的關聯程度,如果有些屬性沒有被大部分的方法使用,其內聚性是低的。
Rule 5.【推薦】 建構函式如果有很多引數,且有多種引數組合時,建議使用Builder模式
Executor executor = new ThreadPoolBuilder().coreThread(10).queueLenth(100).build();
即使仍然使用建構函式,也建議使用chain constructor模式,逐層加入預設值傳遞呼叫,僅在引數最多的建構函式裡實現構造邏輯。
public A(){
A(DEFAULT_TIMEOUT);
}
public A(int timeout) {
...
}
Rule 6.【推薦】建構函式要簡單,尤其是存在繼承關係的時候
可以將複雜邏輯,尤其是業務邏輯,抽取到獨立函式,如init(),start(),讓使用者顯式呼叫。
Foo foo = new Foo();
foo.init();
Rule 7.【強制】所有的子類覆寫方法,必須加@Override
註解
比如有時候子類的覆寫方法的拼寫有誤,或方法簽名有誤,導致沒能真正覆寫,加@Override
可以準確判斷是否覆寫成功。
而且,如果在父類中對方法簽名進行了修改,子類會馬上編譯報錯。
另外,也能提醒閱讀者這是個覆寫方法。
最後,建議在IDE的Save Action中配置自動新增@Override
註解,如果無意間錯誤同名覆寫了父類方法也能被發現。
Rule 8.【強制】靜態方法不能被子類覆寫。
因為它只會根據表面型別來決定呼叫的方法。
Base base = new Children();
// 下句實際呼叫的是父類的靜態方法,雖然物件例項是子類的。
base.staticMethod();
Rule 9.靜態方法訪問的原則
9.1【推薦】避免通過一個類的物件引用訪問此類的靜態變數或靜態方法,直接用類名來訪問即可
目的是向讀者更清晰傳達呼叫的是靜態方法。可在IDE的Save Action中配置自動轉換。
int i = objectA.staticMethod(); // WRONG
int i = ClassA.staticMethod(); // RIGHT
- Sonar-2209: "static" members should be accessed statically
- Sonar-2440: Classes with only "static" methods should not be instantiated
9.2 【推薦】除測試用例,不要static import 靜態方法
靜態匯入後忽略掉的類名,給閱讀者造成障礙。
例外:測試環境中的assert語句,大家都太熟悉了。
- Sonar-3030: Classes should not have too many "static" imports 但IDEA經常自動轉換static import,所以暫不作為規則。
9.3【推薦】儘量避免在非靜態方法中修改靜態成員變數的值
// WRONG
public void foo() {
ClassA.staticFiled = 1;
}
- Sonar-2696: Instance methods should not write to "static" fields
- Sonar-3010: Static fields should not be updated in constructors
Rule 10.【推薦】 內部類的定義原則
當一個類與另一個類關聯非常緊密,處於從屬的關係,特別是只有該類會訪問它時,可定義成私有內部類以提高封裝性。
另外,內部類也常用作回撥函式類,在JDK8下建議寫成Lambda。
內部類分匿名內部類,內部類,靜態內部類三種。
- 匿名內部類 與 內部類,按需使用:
在效能上沒有區別;當內部類會被多個地方呼叫,或匿名內部類的長度太長,已影響對呼叫它的方法的閱讀時,定義有名字的內部類。
- 靜態內部類 與 內部類,優先使用靜態內部類:
- 非靜態內部類持有外部類的引用,能訪問外類的例項方法與屬性。構造時多傳入一個引用對效能沒有太大影響,更關鍵的是向閱讀者傳遞自己的意圖,內部類會否訪問外部類。
- 非靜態內部類裡不能定義static的屬性與方法。
- Sonar-2694: Inner classes which do not reference their owning classes should be "static"
- Sonar-1604: Anonymous inner classes containing only one method should become lambdas
Rule 11.【推薦】使用getter/setter方法,還是直接public成員變數的原則。
除非因為特殊原因方法內聯失敗,否則使用getter方法與直接訪問成員變數的效能是一樣的。
使用getter/setter,好處是可以進一步的處理:
- 通過隱藏setter方法使得成員變數只讀
- 增加簡單的校驗邏輯
- 增加簡單的值處理,值型別轉換等
建議通過IDE生成getter/setter。
但getter/seter中不應有複雜的業務處理,建議另外封裝函式,並且不要以getXX/setXX命名。
如果是內部類,以及無邏輯的POJO/VO類,使用getter/setter除了讓一些純OO論者感覺舒服,沒有任何的好處,建議直接使用public成員變數。
例外:有些序列化框架只能從getter/setter反射,不能直接反射public成員變數。
Rule 12.【強制】POJO類必須覆寫toString方法。
便於記錄日誌,排查問題時呼叫POJO的toString方法列印其屬性值。否則預設的Object.toString()只列印類名@數字
的無效資訊。
Rule 13. hashCode和equals方法的處理,遵循如下規則:
13.1【強制】只要重寫equals,就必須重寫hashCode。 而且選取相同的屬性進行運算。
13.2【推薦】只選取真正能決定物件是否一致的屬性,而不是所有屬性,可以改善效能。
13.3【推薦】對不可變物件,可以快取hashCode值改善效能(比如String就是例子)。
13.4【強制】類的屬性增加時,及時重新生成toString,hashCode和equals方法。
Rule 14.【強制】使用IDE生成toString,hashCode和equals方法。
使用IDE生成而不是手寫,能保證toString有統一的格式,equals和hashCode則避免不正確的Null值處理。
子類生成toString() 時,還需要勾選父類的屬性。
Rule 15. 【強制】Object的equals方法容易拋空指標異常,應使用常量或確定非空的物件來呼叫equals
推薦使用java.util.Objects#equals(JDK7引入的工具類)
"test".equals(object); //RIGHT
Objects.equals(object, "test"); //RIGHT
Rule 16.【強制】除了保持相容性的情況,總是移除無用屬性、方法與引數
特別是private的屬性、方法、內部類,private方法上的引數,一旦無用立刻移除。信任程式碼版本管理系統。
- Sonar-3985: Unused "private" classes should be removed
- Sonar-1068: Unused "private" fields should be removed
- Sonar: Unused "private" methods should be removed
- Sonar-1481: Unused local variables should be removed
- Sonar-1172: Unused method parameters should be removed Sonar-VJ版只對private方法的無用引數告警。
Rule 17.【推薦】final關鍵字與效能無關,僅用於下列不可修改的場景
1) 定義類及方法時,類不可繼承,方法不可覆寫;
2) 定義基本型別的函式引數和變數,不可重新賦值;
3) 定義物件型的函式引數和變數,僅表示變數所指向的物件不可修改,而物件自身的屬性是可以修改的。
Rule 18.【推薦】得墨忒耳法則,不要和陌生人說話
以下呼叫,一是導致了對A物件的內部結構(B,C)的緊耦合,二是連串的呼叫很容易產生NPE,因此鏈式呼叫盡量不要過長。
obj.getA().getB().getC().hello();
(六) 控制語句
Rule 1. 【強制】if, else, for, do, while語句必須使用大括號,即使只有單條語句
曾經試過合併程式碼時,因為沒加括號,單條語句合併成兩條語句後,仍然認為只有單條語句,另一條語句在迴圈外執行。
其他增加除錯語句等情況也經常引起同樣錯誤。
可在IDE的Save Action中配置自動新增。
if (a == b) {
...
}
例外:一般由IDE生成的equals()函式
- Sonar-121: Control structures should use curly braces Sonar-VJ版豁免了equals()函式
Rule 2.【推薦】少用if-else方式,多用哨兵語句式以減少巢狀層次
if (condition) {
...
return obj;
}
// 接著寫else的業務邏輯程式碼;
- Facebook-Contrib: Style - Method buries logic to the right (indented) more than it needs to be
Rule 3.【推薦】限定方法的巢狀層次
所有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;
}
Rule 4.【推薦】布林表示式中的布林運算子(&&,||)的個數不超過4個,將複雜邏輯判斷的結果賦值給一個有意義的布林變數名,以提高可讀性
//WRONG
if ((file.open(fileName, "w") != null) && (...) || (...)|| (...)) {
...
}
//RIGHT
boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed || (...)) {
...
}
Rule 5.【推薦】簡單邏輯,善用三目運算子,減少if-else語句的編寫
s != null ? s : "";
Rule 6.【推薦】減少使用取反的邏輯
不使用取反的邏輯,有利於快速理解。且大部分情況,取反邏輯存在對應的正向邏輯寫法。
//WRONG
if (!(x >= 268) { ... }
//RIGHT
if (x < 268) { ... }
Rule 7.【推薦】表示式中,能造成短路概率較大的邏輯儘量放前面,使得後面的判斷可以免於執行
if (maybeTrue() || maybeFalse()) { ... }
if (maybeFalse() && maybeTrue()) { ... }
Rule 8.【強制】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;
}
Rule 9.【推薦】迴圈體中的語句要考量效能,操作儘量移至迴圈體外處理
1)不必要的耗時較大的物件構造;
2)不必要的try-catch(除非出錯時需要迴圈下去)。
Rule 10.【推薦】能用while迴圈實現的程式碼,就不用do-while迴圈
while語句能在迴圈開始的時候就看到迴圈條件,便於幫助理解迴圈內的程式碼;
do-while語句要在迴圈最後才看到迴圈條件,不利於程式碼維護,程式碼邏輯容易出錯。
(七) 基本型別與字串
Rule 1. 原子資料型別(int等)與包裝型別(Integer等)的使用原則
1.1 【推薦】需要序列化的POJO類屬性使用包裝資料型別
1.2 【推薦】RPC方法的返回值和引數使用包裝資料型別
1.3 【推薦】區域性變數儘量使用基本資料型別
包裝型別的壞處:
1)Integer 24位元組,而原子型別 int 4位元組。
2)包裝型別每次賦值還需要額外建立物件,如Integer var = 200, 除非數值在快取區間內(見Integer.IntegerCache與Long.LongCache)才會複用已快取物件。預設快取區間為-128到127,其中Integer的快取區間還受啟動引數的影響,如-XX:AutoBoxCacheMax=20000。
3)包裝型別還有==比較的陷阱(見規則3)
包裝型別的好處:
1)包裝型別能表達Null的語義。
比如資料庫的查詢結果可能是null,如果用基本資料型別有NPE風險。又比如顯示成交總額漲跌情況,如果呼叫的RPC服務不成功時,應該返回null,顯示成-%,而不是0%。
2)集合需要包裝型別,除非使用陣列,或者特殊的原子型別集合。
3)泛型需要包裝型別,如Result
。
Rule 2.原子資料型別與包裝型別的轉換原則
2.1【推薦】自動轉換(AutoBoxing)有一定成本,呼叫者與被呼叫函式間儘量使用同一型別,減少預設轉換
//WRONG, sum 型別為Long, i型別為long,每次相加都需要AutoBoxing。
Long sum=0L;
for( long i = 0; i < 10000; i++) {
sum+=i;
}
//RIGHT, 準確使用API返回正確的型別
Integer i = Integer.valueOf(str);
int i = Integer.parseInt(str);
2.2 【推薦】自動拆箱有可能產生NPE,要注意處理
//如果intObject為null,產生NPE
int i = intObject;
Rule 3. 數值equals比較的原則
3.1【強制】 所有包裝類物件之間值的比較,全部使用equals方法比較
判斷物件是否同一個。Integer var = ?在快取區間的賦值(見規則1),會複用已有物件,因此這個區間內的Integer使用進行判斷可通過,但是區間之外的所有資料,則會在堆上新產生,不會通過。因此如果用== 來比較數值,很可能在小的測試資料中通過,而到了生產環境才出問題。
3.2【強制】 BigDecimal需要使用compareTo()
因為BigDecimal的equals()還會比對精度,2.0與2.00不一致。
- Facebook-Contrib: Correctness - Method calls BigDecimal.equals()
3.3【強制】 Atomic 系列,不能使用equals方法*
因為 Atomic* 系列沒有覆寫equals方法。
//RIGHT
if (counter1.get() == counter2.get()){...}
3.4【強制】 double及float的比較,要特殊處理
因為精度問題,浮點數間的equals非常不可靠,在vjkit的NumberUtil中有對應的封裝函式。
float f1 = 0.15f;
float f2 = 0.45f/3; //實際等於0.14999999
//WRONG
if (f1 == f2) {...}
if (Double.compare(f1,f2)==0)
//RIGHT
static final float EPSILON = 0.00001f;
if (Math.abs(f1-f2)<EPSILON) {...}
Rule 4. 數字型別的計算原則
4.1【強制】數字運算表示式,因為先進行等式右邊的運算,再賦值給等式左邊的變數,所以等式兩邊的型別要一致
例子1: int與int相除後,哪怕被賦值給float或double,結果仍然是四捨五入取整的int。
需要強制將除數或被除數轉換為float或double。
double d = 24/7; //結果是3.0
double d = (double)24/7; //結果是正確的3.42857
例子2: int與int相乘,哪怕被賦值給long,仍然會溢位。
需要強制將乘數的一方轉換為long。
long l = Integer.MAX_VALUE * 2; // 結果是溢位的-2
long l = Integer.MAX_VALUE * 2L; //結果是正確的4294967294
另外,int的最大值約21億,留意可能溢位的情況。
4.2【強制】數字取模的結果不一定是正數,負數取模的結果仍然負數
取模做陣列下標時,如果不處理負數的情況,很容易ArrayIndexOutOfBoundException。
另外,Integer.MIN_VALUE取絕對值也仍然是負數。因此,vjkit的MathUtil對上述情況做了安全的封裝。
-4 % 3 = -1;
Math.abs(Integer.MIN_VALUE) = -2147483648;
- Findbugs: Style - Remainder of hashCode could be negative
4.3【推薦】 double 或 float 計算時有不可避免的精度問題
float f = 0.45f/3; //結果是0.14999999
double d1 = 0.45d/3; //結果是正確的0.15
double d2 = 1.03d - 0.42d; //結果是0.6100000000000001
儘量用double而不用float,但如果是金融貨幣的計算,則必須使用如下選擇:
選項1, 使用效能較差的BigDecimal。BigDecimal還能精確控制四捨五入或是其他取捨的方式。
選項2, 在預知小數精度的情況下,將浮點運算放大為整數計數,比如貨幣以"分"而不是以"元"計算。
Rule 5. 【推薦】如果變數值僅有有限的可選值,用列舉類來定義常量
尤其是變數還希望帶有名稱之外的延伸屬性時,如下例:
//WRONG
public String MONDAY = "MONDAY";
public int MONDAY_SEQ = 1;
//RIGHT
public enum SeasonEnum {
SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);
int seq;
SeasonEnum(int seq) { this.seq = seq; }
}
業務程式碼中不要依賴ordinary()函式進行業務運算,而是自定義數字屬性,以免列舉值的增減調序造成影響。 例外:永遠不會有變化的列舉,比如上例的一年四季。
Rule 6. 字串拼接的原則
6.1 【推薦】 當字串拼接不在一個命令列內寫完,而是存在多次拼接時(比如迴圈),使用StringBuilder的append()
String s = "hello" + str1 + str2; //Almost OK,除非初始長度有問題,見第3點.
String s = "hello"; //WRONG
if (condition) {
s += str1;
}
String str = "start"; //WRONG
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
反編譯出的位元組碼檔案顯示,其實每條用+
進行字元拼接的語句,都會new出一個StringBuilder物件,然後進行append操作,最後通過toString方法返回String物件。所以上面兩個錯誤例子,會重複構造StringBuilder,重複toString()造成資源浪費。
6.2 【強制】 字串拼接物件時,不要顯式呼叫物件的toString()
如上,+
實際是StringBuilder,本身會呼叫物件的toString(),且能很好的處理null的情況。
//WRONG
str = "result:" + myObject.toString(); // myObject為Null時,拋NPE
//RIGHT
str = "result:" + myObject; // myObject為Null時,輸出 result:null
6.3【強制】使用StringBuilder,而不是有所有方法都有同步修飾符的StringBuffer
因為內聯不成功,逃逸分析並不能抹除StringBuffer上的同步修飾符
6.4 【推薦】當拼接後字串的長度遠大於16時,指定StringBuilder的大概長度,避免容量不足時的成倍擴充套件
6.5 【推薦】如果字串長度很大且頻繁拼接,可考慮ThreadLocal重用StringBuilder物件
參考BigDecimal的toString()實現,及vjkit中的StringBuilderHolder。
Rule 7. 【推薦】字元操作時,優先使用字元引數,而不是字串,能提升效能
//WRONG
str.indexOf("e");
//RIGHT
stringBuilder.append('a');
str.indexOf('e');
str.replace('m','z');
其他包括split等方法,在JDK String中未提供針對字元引數的方法,可考慮使用Apache Commons StringUtils 或Guava的Splitter。
Rule 8. 【推薦】利用好正規表示式的預編譯功能,可以有效加快正則匹配速度
反例:
//直接使用String的matches()方法
result = "abc".matches("[a-zA-z]");
//每次重新構造Pattern
Pattern pattern = Pattern.compile("[a-zA-z]");
result = pattern.matcher("abc").matches();
正例:
//在某個地方預先編譯Pattern,比如類的靜態變數
private static Pattern pattern = Pattern.compile("[a-zA-z]");
...
//真正使用Pattern的地方
result = pattern.matcher("abc").matches();
(八) 集合處理
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 2. 【推薦】儘量使用新式的foreach語法遍歷Collection與陣列
foreach是語法糖,遍歷集合的實際位元組碼等價於基於Iterator的迴圈。
foreach程式碼一來程式碼簡潔,二來有效避免了有多個迴圈或巢狀迴圈時,因為不小心的複製貼上,用錯了iterator或迴圈計數器(i,j)的情況。
Rule 3. 【強制】不要在foreach迴圈裡進行元素的remove/add操作,remove元素可使用Iterator方式
//WRONG
for (String str : list) {
if (condition) {
list.remove(str);
}
}
//RIGHT
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String str = it.next();
if (condition) {
it.remove();
}
}
- Facebook-Contrib: Correctness - Method modifies collection element while iterating
- Facebook-Contrib: Correctness - Method deletes collection element while iterating
Rule 4. 【強制】使用entrySet遍歷Map類集合Key/Value,而不是keySet 方式進行遍歷
keySet遍歷的方式,增加了N次用key獲取value的查詢。
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 | Nullable |
由於HashMap的干擾,很多人認為ConcurrentHashMap是可以置入null值。同理,Set中的value實際是Map中的key。
Rule 7. 【強制】長生命週期的集合,裡面內容需要及時清理,避免記憶體洩漏
長生命週期集合包括下面情況,都要小心處理。
1) 靜態屬性定義;
2) 長生命週期物件的屬性;
3) 儲存在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 9. 【推薦】正確使用集合泛型的萬用字元
List
並不是List
的子類,如果希望泛型的集合能向上向下相容轉型,而不僅僅適配唯一類,則需定義萬用字元,可以按需要extends 和 super的字面意義,也可以遵循PECS(Producer Extends Consumer Super)
原則:
- 如果集合要被讀取,定義成``
Class Stack<E>{
public void pushAll(Iterable<? extends E> src){
for (E e: src)
push(e);
}
}
Stack<Number> stack = new Stack<Number>();
Iterable<Integer> integers = ...;
stack.pushAll(integers);
- 如果集合要被寫入,定義成``
Class Stack<E>{
public void popAll(Collection<? super E> dist){
while(!isEmpty())
dist.add(pop);
}
}
Stack<Number> stack = new Stack<Number>();
Collection<Object> objects = ...;
stack.popAll(objects);
Rule 10. 【推薦】List
, List
與 List
的選擇
定義成List
,會被IDE提示需要定義泛型。 如果實在無法確定泛型,就倉促定義成List
來矇混過關的話,該list只能讀,不能增改。定義成List
呢,如規則9所述,List
並不是List
的子類,除非函式定義使用了萬用字元。
因此實在無法明確其泛型時,使用List
也是可以的。
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());
//原始型別陣列,JDK7則要自己寫個迴圈來加入了
Arrays.asList(array),如果array是原始型別陣列如int[],會把整個array當作List的一個元素,String[] 或 Foo[]則無此問題。 Collections.addAll()實際是迴圈加入元素,效能相對較低,同樣會把int[]認作一個元素。
- Facebook-Contrib: Correctness - Impossible downcast of toArray() result
- Facebook-Contrib: Correctness - Method calls Array.asList on an array of primitive values
(九) 併發處理
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 2. 【推薦】儘量使用執行緒池來建立執行緒
除特殊情況,儘量不要自行建立執行緒,更好的保護執行緒資源。
//WRONG
Thread thread = new Thread(...);
thread.start();
同理,定時器也不要使用Timer,而應該使用ScheduledExecutorService。
因為Timer只有單執行緒,不能併發的執行多個在其中定義的任務,而且如果其中一個任務丟擲異常,整個Timer也會掛掉,而ScheduledExecutorService只有那個沒捕獲到異常的任務不再定時執行,其他任務不受影響。
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 5. 【強制】編寫可停止的Runnable
執行Thread.interrupt()時,如果執行緒處於sleep(), wait(), join(), lock.lockInterruptibly()等blocking狀態,會丟擲InterruptedException,如果執行緒未處於上述狀態,則將執行緒狀態設為interrupted。
因此,如下的程式碼無法中斷執行緒:
public void run() {
while (true) { //WRONG,無判斷執行緒狀態。
sleep();
}
public void sleep() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.warn("Interrupted!", e); //WRONG,吃掉了異常,interrupt狀態未再傳遞
}
}
}
5.1 正確處理InterruptException
因為InterruptException異常是個必須處理的Checked Exception,所以run()所呼叫的子函式很容易吃掉異常並簡單的處理成列印日誌,但這等於停止了中斷的傳遞,外層函式將收不到中斷請求,繼續原有迴圈或進入下一個堵塞。
正確處理是呼叫Thread.currentThread().interrupt();
將中斷往外傳遞。
//RIGHT
public void myMethod() {
try {
...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
5.2 主迴圈及進入阻塞狀態前要判斷執行緒狀態
//RIGHT
public void run() {
try {
while (!Thread.isInterrupted()) {
// do stuff
}
} catch (InterruptedException e) {
logger.warn("Interrupted!", e);
}
}
其他如Thread.sleep()的程式碼,在正式sleep前也會判斷執行緒狀態。
Rule 6. 【強制】Runnable中必須捕獲一切異常
如果Runnable中沒有捕獲RuntimeException而向外丟擲,會發生下列情況:
- ScheduledExecutorService執行定時任務,任務會被中斷,該任務將不再定時排程,但執行緒池裡的執行緒還能用於其他任務。
- ExecutorService執行任務,當前執行緒會中斷,執行緒池需要建立新的執行緒來響應後續任務。
- 如果沒有在ThreadFactory設定自定義的UncaughtExceptionHanlder,則異常最終只列印在System.err,而不會列印在專案的日誌中。
因此建議自寫的Runnable都要保證捕獲異常; 如果是第三方的Runnable,可以將其再包裹一層vjkit中的SafeRunnable。
executor.execute(ThreadPoolUtil.safeRunner(runner));
Rule 7. 【強制】全域性的非執行緒安全的物件可考慮使用ThreadLocal存放
全域性變數包括單例物件,static成員變數。
著名的非執行緒安全類包括SimpleDateFormat,MD5/SHA1的Digest。
對這些類,需要每次使用時建立。
但如果建立有一定成本,可以使用ThreadLocal存放並重用。
ThreadLocal變數需要定義成static,並在每次使用前重置。
private static final ThreadLocal<MessageDigest> SHA1_DIGEST = new ThreadLocal<MessageDigest>() {
@Override
protected MessageDigest initialValue() {
try {
return MessageDigest.getInstance("SHA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("...", e);
}
}
};
public void digest(byte[] input) {
MessageDigest digest = SHA1_DIGEST.get();
digest.reset();
return digest.digest(input);
}
- Sonar-2885: Non-thread-safe fields should not be static
- Facebook-Contrib: Correctness - Field is an instance based ThreadLocal variable
Rule 8. 【推薦】縮短鎖
1) 能鎖區塊,就不要鎖整個方法體;
//鎖整個方法,等價於整個方法體內synchronized(this)
public synchronized boolean foo(){};
//鎖區塊方法,僅對需要保護的原子操作的連續程式碼塊進行加鎖。
public boolean foo() {
synchronized(this) {
...
...
}
//other stuff
}
2)能用物件鎖,就不要用類鎖。
//物件鎖,隻影響使用同一個物件加鎖的執行緒
synchronized(this) {
...
}
//類鎖,使用類物件作為鎖物件,影響所有執行緒。
synchronized(A.class) {
...
}
Rule 10. 【推薦】選擇分離鎖,分散鎖甚至無鎖的資料結構
- 分離鎖:
1) 讀寫分離鎖ReentrantReadWriteLock,讀讀之間不加鎖,僅在寫讀和寫寫之間加鎖;
2) Array Base的queue一般是全域性一把鎖,而Linked Base的queue一般是隊頭隊尾兩把鎖。
- 分散鎖(又稱分段鎖):
1)如JDK7的ConcurrentHashMap,分散成16把鎖;
2)對於經常寫,少量讀的計數器,推薦使用JDK8或vjkit封裝的LongAdder物件效能更好(內部分散成多個counter,減少樂觀鎖的使用,取值時再相加所有counter)
- 無鎖的資料結構:
1)完全無鎖無等待的結構,如JDK8的ConcurrentHashMap;
2)基於CAS的無鎖有等待的資料結構,如AtomicXXX系列。
Rule 11. 【推薦】基於ThreadLocal來避免鎖
比如Random例項雖然是執行緒安全的,但其實它的seed的訪問是有鎖保護的。因此建議使用JDK7的ThreadLocalRandom,通過在每個執行緒裡放一個seed來避免了加鎖。
Rule 12. 【推薦】規避死鎖風險
對多個資源多個物件的加鎖順序要一致。
如果無法確定完全避免死鎖,可以使用帶超時控制的tryLock語句加鎖。
Rule 13. 【推薦】volatile修飾符,AtomicXX系列的正確使用
多執行緒共享的物件,在單一執行緒內的修改並不保證對所有執行緒可見。使用volatile定義變數可以解決(解決了可見性)。
但是如果多條執行緒併發進行基於當前值的修改,如併發的counter++,volatile則無能為力(解決不了原子性)。
此時可使用Atomic*系列:
AtomicInteger count = new AtomicInteger();
count.addAndGet(2);
但如果需要原子地同時對多個AtomicXXX的Counter進行操作,則仍然需要使用synchronized將改動程式碼塊加鎖。
Rule 14. 【推薦】延時初始化的正確寫法
通過雙重檢查鎖(double-checked locking)實現延遲初始化存在隱患,需要將目標屬性宣告為volatile型,為了更高的效能,還要把volatile屬性賦予給臨時變數,寫法複雜。
所以如果只是想簡單的延遲初始化,可用下面的靜態類的做法,利用JDK本身的class載入機制保證唯一初始化。
private static class LazyObjectHolder {
static final LazyObject instance = new LazyObject();
}
public void myMethod() {
LazyObjectHolder.instance.doSomething();
}
(十) 異常處理
Rule 1. 【強制】建立異常的消耗大,只用在真正異常的場景
構造異常時,需要獲得整個呼叫棧,有一定消耗。
不要用來做流程控制,條件控制,因為異常的處理效率比條件判斷低。
發生概率較高的條件,應該先進行檢查規避,比如:IndexOutOfBoundsException,NullPointerException等,所以如果程式碼裡捕獲這些異常通常是個壞味道。
//WRONG
try {
return obj.method();
} catch (NullPointerException e) {
return false;
}
//RIGHT
if (obj == null) {
return false;
}
Rule 2. 【推薦】在特定場景,避免每次構造異常
如上,異常的建構函式需要獲得整個呼叫棧。
如果異常頻繁發生,且不需要列印完整的呼叫棧時,可以考慮繞過異常的建構函式。
1) 如果異常的message不變,將異常定義為靜態成員變數;
下例定義靜態異常,並簡單定義一層的StackTrace。ExceptionUtil
見vjkit。
private static RuntimeException TIMEOUT_EXCEPTION = ExceptionUtil.setStackTrace(new RuntimeException("Timeout"),
MyClass.class, "mymethod");
...
throw TIMEOUT_EXCEPTION;
2) 如果異常的message會變化,則對靜態的異常例項進行clone()再修改message。
Exception預設不是Cloneable的,CloneableException
見vjkit。
private static CloneableException TIMEOUT_EXCEPTION = new CloneableException("Timeout") .setStackTrace(My.class,
"hello");
...
throw TIMEOUT_EXCEPTION.clone("Timeout for 40ms");
3)自定義異常,也可以考慮過載fillStackTrace()為空函式,但相對沒那麼靈活,比如無法按場景指定一層的StackTrace。
Rule 3. 【推薦】自定義異常,建議繼承RuntimeException
詳見《Clean Code》,爭論已經結束,不再推薦原本初衷很好的CheckedException。
因為CheckedException需要在丟擲異常的地方,與捕獲處理異常的地方之間,層層定義throws XXX來傳遞Exception,如果底層程式碼改動,將影響所有上層函式的簽名,導致編譯出錯,對封裝的破壞嚴重。對CheckedException的處理也給上層程式設計師帶來了額外的負擔。因此其他語言都沒有CheckedException的設計。
Rule 4. 【推薦】異常日誌應包含排查問題的足夠資訊
異常資訊應包含排查問題時足夠的上下文資訊。
捕獲異常並記錄異常日誌的地方,同樣需要記錄沒有包含在異常資訊中,而排查問題需要的資訊,比如捕獲處的上下文資訊。
//WRONG
new TimeoutException("timeout");
logger.error(e.getMessage(), e);
//RIGHT
new TimeoutException("timeout:" + eclapsedTime + ", configuration:" + configTime);
logger.error("user[" + userId + "] expired:" + e.getMessage(), e);
- Facebook-Contrib: Style - Method throws exception with static message string
Rule 5. 異常丟擲的原則
5.1 【推薦】儘量使用JDK標準異常,專案標準異常
儘量使用JDK標準的Runtime異常如IllegalArgumentException
,IllegalStateException
,UnsupportedOperationException
,專案定義的Exception如ServiceException
。
5.2 【推薦】根據呼叫者的需要來定義異常類,直接使用RuntimeException
是允許的
是否定義獨立的異常類,關鍵是呼叫者會如何處理這個異常,如果沒有需要特別的處理,直接丟擲RuntimeException也是允許的。
Rule 6. 異常捕獲的原則
6.1 【推薦】按需要捕獲異常,捕獲Exception
或Throwable
是允許的
如果無特殊處理邏輯,統一捕獲Exception統一處理是允許的。
捕獲Throwable是為了捕獲Error類異常,包括其實無法處理的OOM
StackOverflow
ThreadDeath
,以及類載入,反射時可能丟擲的NoSuchMethodError
NoClassDefFoundError
等。
6.2【推薦】多個異常的處理邏輯一致時,使用JDK7的語法避免重複程式碼
try {
...
} catch (AException | BException | CException ex) {
handleException(ex);
}
Rule 7.異常處理的原則
7.1 【強制】捕獲異常一定要處理;如果故意捕獲並忽略異常,須要註釋寫明原因
方便後面的閱讀者知道,此處不是漏了處理。
//WRONG
try {
} catch(Exception e) {
}
//RIGHT
try {
} catch(Exception ignoredExcetpion) {
//continue the loop
}
7.2 【強制】異常處理不能吞掉原異常,要麼在日誌列印,要麼在重新丟擲的異常裡包含原異常
//WRONG
throw new MyException("message");
//RIGHT 記錄日誌後丟擲新異常,向上次呼叫者遮蔽底層異常
logger.error("message", ex);
throw new MyException("message");
//RIGHT 傳遞底層異常
throw new MyException("message", ex);
- Sonar-1166: Exception handlers should preserve the original exceptions,其中預設包含了InterruptedException, NumberFormatException,NoSuchMethodException等若干例外
7.3 【強制】如果不想處理異常,可以不進行捕獲。但最外層的業務使用者,必須處理異常,將其轉化為使用者可以理解的內容
Rule 8. finally塊的處理原則
8.1 【強制】必須對資源物件、流物件進行關閉,或使用語法try-with-resource
關閉動作必需放在finally塊,不能放在try塊 或 catch塊,這是經典的錯誤。
更加推薦直接使用JDK7的try-with-resource語法自動關閉Closeable的資源,無需在finally塊處理,避免潛在問題。
try (Writer writer = ...) {
writer.append(content);
}
8.2 【強制】如果處理過程中有丟擲異常的可能,也要做try-catch,否則finally塊中丟擲的異常,將代替try塊中丟擲的異常
//WRONG
try {
...
throw new TimeoutException();
} finally {
file.close();//如果file.close()丟擲IOException, 將代替TimeoutException
}
//RIGHT, 在finally塊中try-catch
try {
...
throw new TimeoutException();
} finally {
IOUtil.closeQuietly(file); //該方法中對所有異常進行了捕獲
}
8.3 【強制】不能在finally塊中使用return,finally塊中的return將代替try塊中的return及throw Exception
//WRONG
try {
...
return 1;
} finally {
return 2; //實際return 2 而不是1
}
try {
...
throw TimeoutException();
} finally {
return 2; //實際return 2 而不是TimeoutException
}
(十一) 日誌規約
Rule 1. 【強制】應用中不可直接使用日誌庫(Log4j、Logback)中的API,而應使用日誌框架SLF4J中的API
使用門面模式的日誌框架,有利於維護各個類的日誌處理方式統一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static Logger logger = LoggerFactory.getLogger(Foo.class);
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 4. 【推薦】儘量使用非同步日誌
低延時的應用,使用非同步輸出的形式(以AsyncAppender串接真正的Appender),可減少IO造成的停頓。
需要正確配置非同步佇列長度及佇列滿的行為,是丟棄還是等待可用,業務上允許丟棄的儘量選丟棄。
Rule 5. 【強制】禁止使用效能很低的System.out()列印日誌資訊
同理也禁止e.printStackTrace();
例外: 應用啟動和關閉時,擔心日誌框架還未初始化或已關閉。
- Sonar-106: Standard outputs should not be used directly to log anything
- Sonar-1148: Throwable.printStackTrace(...) should not be called
Rule 6. 【強制】禁止配置日誌框架輸出日誌列印處的類名,方法名及行號的資訊
日誌框架在每次列印時,通過主動獲得當前執行緒的StackTrace來獲取上述資訊的消耗非常大,儘量通過Logger名本身給出足夠資訊。
Rule 7. 【推薦】謹慎地記錄日誌,避免大量輸出無效日誌,資訊不全的日誌
大量地輸出無效日誌,不利於系統效能,也不利於快速定位錯誤點。
記錄日誌時請思考:這些日誌真的有人看嗎?看到這條日誌你能做什麼?能不能給問題排查帶來好處?
Rule 8. 【推薦】使用warn級別而不是error級別,記錄外部輸入引數錯誤的情況
如非必要,請不在此場景列印error級別日誌,避免頻繁報警。
error級別只記錄系統邏輯出錯、異常或重要的錯誤資訊。
(十二) 其他規約
Rule 1. 【參考】儘量不要讓魔法值(即未經定義的數字或字串常量)直接出現在程式碼中
//WRONG
String key = "Id#taobao_"+tradeId;
cache.put(key, value);
例外:-1,0,1,2,3 不認為是魔法數
- Sonar-109: Magic numbers should not be used 但現實中所謂魔法數還是太多,該規則不能被真正執行。
Rule 2. 【推薦】時間獲取的原則
1)獲取當前毫秒數System.currentTimeMillis() 而不是new Date().getTime(),後者的消耗要大得多。
2)如果要獲得更精確的,且不受NTP時間調整影響的流逝時間,使用System.nanoTime()獲得機器從啟動到現在流逝的納秒數。
3)如果希望在測試用例中控制當前時間的值,則使用vjkit的Clock類封裝,在測試和生產環境中使用不同的實現。
Rule 3. 【推薦】變數宣告儘量靠近使用的分支
不要在一個程式碼塊的開頭把區域性變數一次性都宣告瞭(這是c語言的做法),而是在第一次需要使用它時才宣告。
否則如果方法已經退出或進入其他分支,就白白初始化了變數。
//WRONG
Foo foo = new Foo();
if(ok){
return;
}
foo.bar();
Rule 4. 【推薦】不要像C那樣一行裡做多件事情
//WRONG
fooBar.fChar = barFoo.lchar = 'c';
argv++; argc--;
int level, size;
Rule 5. 【推薦】不要為了效能而使用JNI本地方法
Java在JIT後並不比C程式碼慢,JNI方法因為要反覆跨越JNI與Java的邊界反而有額外的效能損耗。
因此JNI方法僅建議用於呼叫"JDK所沒有包括的, 對特定作業系統的系統呼叫"
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);
Rule 7.【推薦】可降低優先順序的常見程式碼檢查規則
- 介面內容的定義中,去除所有modifier,如public等。 (多個public也沒啥,反正大家都看慣了)
- 工具類,定義private建構函式使其不能被例項化。(命名清晰的工具類,也沒人會去例項化它,對靜態方法通過類來訪問也能避免例項化)
《阿里Java開發手冊》定製紀錄
只記錄較大的改動,對更多條目內容的重新組織與擴寫,則未一一冗述。
(一) 命名規約
對應 阿里規範《命名風格》一章
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
13. 變數、引數重名覆蓋 | 新增規則 | |
1. 禁止拼音縮寫 | 2. 嚴禁使用拼音與英文混合的方式 | 改寫規則 |
3. 禁用其他程式語言風格的字首和字尾 | 1. 程式碼中的命名均不能以下劃線或美元符號開始 | 擴寫規則,把其他語言的囉嗦都禁止掉 |
4. 命名的好壞,在於其“模糊度” | 11. 為了達到程式碼自解釋的目標 | 擴寫規則,參考《Clean Code》的更多例子 |
6. 常量命名全部大寫 | 5.常量名大寫 | 擴寫規則 |
7. 型別與中括號緊挨相連來定義陣列 | 刪除規則,非命名風格,也不重要 | |
13. 介面類中的方法和成員變數不要加任何修飾符號 | 移動規則,非命名風格,移到類設計 | |
16. 各層命名規約 | 刪除規則,各公司有自己的習慣 |
(二) 格式規約
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
1. 專案組統一的程式碼格式模板 | 規則1-8 | 用IDE模版代替逐條描述 同時對Tab/空格不做硬性規定 |
3. 用小括號來限定運算優先順序 | 新增規則 | |
4. 類內方法定義的順序 | 新增規則 | |
5. 通過空行進行邏輯分段 | 11. 不同邏輯、不同語義 | 改寫規則 |
6. 避免IDE格式化 | 新增規則 | |
10.單個方法行數不超過80行 | 刪除規則,非格式規約,移動方法設計 | |
11.沒有必要增加若干空格來對齊 | 刪除規則,現在很少人這麼做 |
(三) 註釋規約
對應 阿里規範《註釋規約》一章
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
2. 刪除空註釋,無意義註釋 | 增加規則 | |
7. JavaDoc中不要大量使用HTML標籤和轉義字元 | 增加規則 | |
1. 註釋的基本要求 | 9. 對於註釋的要求 | 擴寫規則 |
4.避免建立人的註釋 | 3.所有的類都必須新增建立者 | 衝突規則 |
2.所有的抽象方法必須用Javadoc註釋 | 刪除規則,因為規則2不強制,併入規則1 | |
4.方法內部單行註釋,使用//註釋 | 刪除規則,區別不大不強求 | |
5. 所有的列舉型別欄位必須要有註釋 | 刪除規則,因為規則2不強制 |
(四) 方法設計
- 規則 6,7,12,13 從阿里規範《控制語句》一章 移入
- 規則 9 從阿里規範《異常處理》一章 移入
- 規則1,2,3,4,5,8,10,11,14為新建規則
(五) 類設計
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
2.減少類之間的依賴 | 增加規則 | |
3.定義變數與方法引數時,儘量使用介面 | 增加規則 | |
4.類的長度度量 | 增加規則 | |
5.Builder模式 | 增加規則 | |
8.靜態方法不能被覆寫 | 增加規則 | |
9.靜態方法的訪問原則 | 擴寫規則 | |
10.內部類原則 | 增加規則 | |
12-14.hashCode,equals,toString的規則 | 增加規則 | |
16.總是移除無用屬性、方法與引數 | 增加規則 | |
18.【推薦】得墨忒耳法則 | 增加規則 | |
3. 提倡同學們儘量不用可變引數程式設計 | 刪除規則 | |
9. 定義DO/DTO/VO等POJO類時,不要設定任何屬性預設值 | 刪除規則 | |
10. 序列化類新增屬性時,請不要修改serialVersionUID欄位 | 刪除規則 | |
13. 使用索引訪問用String的split方法時 | 刪除規則 | |
19. 慎用Object的clone方法來拷貝物件 | 刪除規則 | |
規則4,5 | 移到《方法規約》 | |
規則6 | 移到《通用設計》 | |
規則7,8,17 | 移到《基礎型別》 | |
規則14,15 | 移到《格式規約》 |
(六) 控制語句
對應 阿里規範《控制語句》一章
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
4.布林表示式中的運算子個數不超過4個 | 擴寫規則 | |
5.善用三目運算子 | 增加規則 | |
6.能造成短路概率較大的邏輯放前面 | 增加規則 | |
10.能用while迴圈實現的程式碼,就不用do-while迴圈 | 增加規則 | |
3. 在高併發場景中,避免使用 ”等於”作為條件 | 刪除規則 | |
8. 介面入參保護 | 刪除規則 | |
9. 下列情形,需要進行引數校驗 | 移到《方法規約》 | |
10. 下列情形,不需要進行引數校 | 移到《方法規約》 |
(七) 基本型別與字串
(八) 集合處理
對應 阿里規範《集合處理》一章
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
2. foreach語法遍歷 | 增加規則 | |
7. 長生命週期的集合 | 增加規則 | |
8. 併發集合 | 增加規則 | |
9. 泛型的萬用字元 | 增加規則 | |
10. List, List 與 List 的選擇 |
增加規則 | |
11. EnumMap | 增加規則 | |
2. ArrayList的subList結果 | 刪除規則 | |
6. 泛型萬用字元 | 刪除規則 | |
12.合理利用好集合的有序性 | 刪除規則 | |
13.利用Set元素唯一的特性 | 刪除規則 | |
12.Array 與 List互轉的 | 使用集合轉陣列的方法,必須使用集合的toArray | 某位老大的測試,new String[0]也不錯 |
(九) 併發處理: 併發與多執行緒
對應 阿里規範《併發處理》一章
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
1. 指定執行緒名 | 擴寫規則 | |
4. 正確停止執行緒 | 擴寫規則 | |
5. 編寫可中斷的Runnable | 增加規則 | |
6. Runnable中必須捕獲一切異常 | 9.多執行緒並行處理定時任務 | 擴寫規則 |
7. 全域性變數的執行緒安全 | 擴寫規則 | |
10. 選擇分離鎖, 分散鎖甚至無鎖的資料結構 | 增加規則 | |
13. volatile修飾符,AtomicXX系列的正確使用 | 擴寫規則 | |
8.併發修改同一記錄時,需要加鎖 | 刪除規則 | |
10.使用CountDownLatch進行非同步轉同步操作 | 刪除規則 | |
14.HashMap在容量不夠進行resize | 移到《集合規約》一章 | |
14. 延時初始化的正確寫法 | 12.雙重檢查鎖 | 衝突規則 |
(十) 異常處理
對應 阿里規範《異常處理》一章
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
2.在特定場合,避免每次構造異常 | 增加規則 | |
5.異常丟擲的原則 | 增加規則 | |
6.異常捕獲的原則 | 增加規則 | |
7.異常處理的原則 | 增加規則 | |
8.捕獲異常與拋異常,必須是完全匹配 | 刪除規則 | |
12.對於公司外的開放介面必須使用“錯誤碼” | 刪除規則 | |
13.DRY原則 | 刪除規則,為什麼會出現在這章,太著名了 | |
9.返回值可以為null | 移到《方法設計》一章 | |
10. 【推薦】防止NPE,是程式設計師的基本修養 | 拆開到各章 | |
11.避免直接丟擲RuntimeException | 規則衝突 |
(十一) 日誌規約
對應 阿里規範《日誌規約》一章
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
4.儘量使用非同步日誌 | 增加規則 | |
5.禁止使用System.out() | 增加規則 | |
6.禁止配置日誌框架列印日誌列印時的類名,行號等資訊 | 增加規則 | |
2.日誌檔案推薦至少儲存15天 | 刪除規則 | |
3.應用中的擴充套件日誌命名方式 | 刪除規則 | |
6.異常資訊應該包括兩類資訊 | 移到《異常處理》 | |
2.合理使用使用佔位符 | 4.對trace/debug/info級別的日誌使用佔位符 | 還是要判斷日誌是否必然輸出, 並強調條件判斷與佔位符之間的差別 |
(十二) 其他規約
保留 阿里規範《常量定義》一章的規則1
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
規則2-4 | 刪除規則 | |
規則5. 如果變數值僅在一個固定範圍內變化用enum型別來定義 | 移到《基本型別》 |
保留 阿里規範《其他》一章的規則7
VIP 規範 | 阿里規範 | 修改 |
---|---|---|
規則2-4,6-8 | 刪除規則 | |
規則1. 在使用正規表示式時,利用好其預編譯功 | 移到《基本型別》 |
- 規則3,4,5,6,7均為新增規則