【Java】Best coding practices every java
原文
Best coding practices every java developer should follow
目錄
引言
4. 盡可能讓變量私有化
6. 警惕冗余初始化
5. Stringbuilder替換字符串拼接【爭(zhēng)議】
15. dry和kiss
14. Solid
7. 盡可能使用增強(qiáng)for循環(huán)或者foreach
13. 日志打印規(guī)則
12. Hardcoding硬編碼
11. 注釋
10. 避免創(chuàng)建不必要對(duì)象
9. 返回空集合而不是null
8. 精度選擇
1. 項(xiàng)目結(jié)構(gòu)
2. 遵循命名規(guī)范
3. 不要吞異常
16. 使用枚舉替代靜態(tài)常量【建議】
17. 按作用域劃分成員變量
18. 在數(shù)字字段中使用下劃線【建議】
總結(jié)
參考
引言
把標(biāo)題翻譯成中文在國(guó)內(nèi)也是一個(gè)老生常談的問題:編程習(xí)慣和編碼規(guī)范。
這篇文章大部分觀點(diǎn)和國(guó)內(nèi)的規(guī)范習(xí)慣類似,令我好奇的是外國(guó)人是如何理解這些內(nèi)容的?
注意本文的Tips排序是打亂的,個(gè)人把感興趣放到了前面來了。這篇文章的評(píng)論區(qū)非常精彩,這里一并整合了評(píng)論區(qū)的讀者觀點(diǎn)。
不管是學(xué)習(xí)還是了解老外的思考方式,或是在評(píng)論區(qū)當(dāng)中看讀者的討論和糾正錯(cuò)誤,閱讀本文都是值得推薦的。
抱著半信半疑的態(tài)度學(xué)習(xí)這些內(nèi)容,你會(huì)得到比單純的了解和接受建議更多的收獲。
4. 盡可能讓變量私有化
如果變量不需要意對(duì)外訪問,那就建議使用私有描述對(duì)于參數(shù)進(jìn)行描述。
評(píng)論區(qū)的讀者對(duì)于這一個(gè)小節(jié)做了補(bǔ)充:
如果是dto并且是final的,擁有公用數(shù)據(jù)的時(shí)候可以不私有化.。
比大量的setter更好建議是更合理的利用設(shè)計(jì)模式構(gòu)建對(duì)象(比如建造者),或者利用委托第三方對(duì)象生產(chǎn)合適的對(duì)象(靜態(tài)工廠替代構(gòu)造器)。
盡可能讓對(duì)象不可變。
6. 警惕冗余初始化
Therefore, a java best practice is to be aware of the default initialization values of member variables and avoid initializing the variables explicitly.
Java 最佳實(shí)踐是了解成員變量的默認(rèn)初始化值并避免顯式初始化變量,Java語言很多變量存在默認(rèn)值,在自己編寫初始化的時(shí)候不建議使用Java的默認(rèn)值。
另一方面,有時(shí)候可以利用Java的初始化做數(shù)據(jù)庫(kù)字段的優(yōu)化,比如開關(guān)狀態(tài)建議把0設(shè)置為開放1設(shè)置為關(guān)閉。
下面的代碼的的對(duì)象初始化代碼是毫無意義的:
public?class?Person?{
????private?String?name;
????private?int?age;
????private?boolean;
?
????public?Person()?{
????????String?name?=?null;
????????int?age?=?0;
????????boolean?isGenius?=?false;
????}
}
Although it is very common practice, it is not encouraged to initialize member variables with the values: like 0, false and null. These values are already the default initialization values of member variables in Java. Therefore, a java best practice is to be aware of the default initialization values of member variables and avoid initializing the variables explicitly.
盡管這是非常常見的做法,但不鼓勵(lì)使用以下值初始化成員變量:如 0、false 和 null。這些值已經(jīng)是 Java 中成員變量的默認(rèn)初始化值。因此,Java 最佳實(shí)踐是了解成員變量的缺省初始化值,并避免顯式初始化變量。
See more here: Java default Initialization of Instance Variables and Initialization Blocks[1]。
5. Stringbuilder 替換字符串拼接【爭(zhēng)議】
實(shí)際上多數(shù)情況下“大可不必”,只有for循環(huán)的情況才考慮是否使用Stringbuilder替換。日常情況下字符拼接操作是完全沒有問問題的,javac編譯之后會(huì)把字符串自動(dòng)用StringBuilder替換,真正應(yīng)該手動(dòng)創(chuàng)建該對(duì)象的場(chǎng)景是在for循環(huán)當(dāng)中的大量的字符串拼接,內(nèi)部會(huì)每次迭代新建Stringbuilder。
隨著JDK版本的升級(jí),到JDK9版本for循環(huán)新建StringBuilder的情況已經(jīng)被改善了(但個(gè)人未從OpenJDK中找到對(duì)應(yīng)源碼驗(yàn)證)。
JDK13 從Python那邊把多行文本的語法搬過來了,定義多行文本可以類似下面這樣:
String?htmlContent?=?“““<html>
???????????????????????????<head>
???????????????????????????</head>
???????????????????????????<body>
??????????????????????????????<p>Hello?world!</p>
???????????????????????????</body>
?????????????????????????</html>""";
最后需要注意的是使用Stringbuilder,僅僅應(yīng)該用在多線程循環(huán)操作字符串當(dāng)中,如果在同步方法里面效率會(huì)非常低并且很慢。
介紹
As a general rule, optimize at an architectural level, and write code for readability. Later on it’s easier to optimize a neat function, than to look for bugs in an optimized one.
上面意思大致是說通用規(guī)則是優(yōu)先寫出具備高可讀性的代碼,然后再去進(jìn)行優(yōu)化這些簡(jiǎn)單的可優(yōu)化點(diǎn),這比在優(yōu)化代碼找錯(cuò)誤要簡(jiǎn)單很多。
15. dry和kiss
DRY stands for “Don’s Repeat Yourself
如果代碼可以公用就盡量公用,不要復(fù)制粘貼代碼。
kiss:KISS stands for “Keep It Simple, Stupid”.
KISS就是保持簡(jiǎn)單,愚蠢。這個(gè)愚蠢指的是方法最好只知道干一件事情。這兩點(diǎn)和Unix最初的設(shè)計(jì)的理念是一樣的,簡(jiǎn)單好用即是美。
14. Solid
Single Responsibility Principle 單一職責(zé):每個(gè)類和接口有明確目標(biāo)。
Open-Closed Principle 開閉原則:開放擴(kuò)展,封閉修改。
Liskov Substitution Principle 里式替代:盡可能讓代碼多態(tài)。
Interface Segregation Principle 接口隔離:實(shí)現(xiàn)接口替代類繼承。
Dependency Inversion Principle 依賴倒轉(zhuǎn)原則:依賴抽象而不是依賴實(shí)現(xiàn)。
7. 盡可能使用增強(qiáng)for循環(huán)或者foreach
建議使用增強(qiáng)for循環(huán)(JDK5)和JDK8的foreach,當(dāng)然最好的建議是活學(xué)活用stream的API。為什么會(huì)有這樣的建議?好像for-index也不是啥壞寫法。
It’s because the index variable is error-prone, as we may alter it incidentally in the loop’s body, or we may starts the index from 1 instead of 0.
這是因?yàn)樗饕兞咳菀壮鲥e(cuò),因?yàn)槲覀兛赡軙?huì)在循環(huán)的主體中偶然改變它,或者我們可能從1而不是0開始索引。下面是Stream API帶來便利的簡(jiǎn)單例子:
public?Integer?findSmallesPositiveNumber(List<Integer>?numbers)?{
???Integer?smallestPositiveNumber=?null;
???for?(Integer?number:?numbers)?{
??????if?(number?>?0)?{
??????if?(smallestPositiveNumber?==?null
????????????||?number?<?smallestPositiveNumber)?{
?????????smallestPositiveNumber?=?number;
??????}
???}
???return?smallestPositiveNumber;
}
使用Stream APi精簡(jiǎn)之后:
public?Optional<Integer>?findSmallesPositiveNumber(
??????List<Integer>?numbers)?{
???return?numbers.stream()
??????.filter(number?->?number?>?0)
??????.min(Integer::compare);
}
13. 日志打印規(guī)則
作者的下面幾條規(guī)則有待商榷,我個(gè)人建議是避免下面的做法:
Developer should add logger on method entry and exit.
在進(jìn)出重要方法打印入?yún)⒑统鰠ⅰ?/p>
謹(jǐn)慎使用,建議涉及金額的操作情況打印。
Developer should add logger in “if” and “else” case to track which condition is true in case of any error.
在if和else的條件判斷處打印參數(shù)。
Developer should also log exception to track the issue
對(duì)于異常信息必須打印。
更為合理的做法是像下面這樣:
Definitely don’t log every if-else statement!
不要在if/else分支中打印,更為合理的建議是記錄響應(yīng)以及錯(cuò)誤。
Don’t log every method entry and exit!
不要在每個(gè)方法的入?yún)⒑统鰠⒋蛴 .?dāng)然這并不是鐵律,比如三方接口調(diào)用就必須要在“入口”和“出口”中打印以便快速定位問題和甩鍋。
**Log exceptions that are unexpected. **
記錄“意外”的異常。比如多線程有可能的interrupt處理,文件讀寫有可能的IO異常。
從褒義和貶義兩個(gè)層面看,日志都是非常具備“價(jià)值”的,好的日志可以幫你快速定位問題,不好的日志就像是垃圾一樣迅速占用服務(wù)器的磁盤。只有非常核心的日志才需要跟蹤每一步的行為,絕大多數(shù)情況下日志只需要在影響業(yè)務(wù)的位置進(jìn)行打印。在打印日志段時(shí)候多思考一下日志等級(jí)的選擇,這意味著你生產(chǎn)能否快速定位問題。
12. Hardcoding硬編碼
硬編碼回會(huì)導(dǎo)致程序難以理解。使用硬編碼會(huì)增加理解難度,通常使用枚舉替代是不錯(cuò)建議。根據(jù)dry的原則,在定義硬編碼的時(shí)候,如果魔法值在JDK中存在類似定義或者存在現(xiàn)實(shí)意義,應(yīng)該果斷通過下面的方式進(jìn)行糾正,比如下面的例子:
private?int?storeClosureDay?=?7;
應(yīng)該被替換為下面這樣:
private?static?final?int?SUNDAY?=?7;??
[…]??
private?int?storeClosureDay?=?SUNDAY;
不要這樣使用,更為合適的處理方式是使用DayOfWeek[2] 的API:
private?DayOfWeek?storeClosureDay?=?DayOfWeek.SUNDAY;
避免硬編碼是非常好的編程習(xí)慣,更好的習(xí)慣是使用易懂的硬編碼。
11. 注釋
關(guān)鍵:注釋只有在可讀性較差并且需要注釋描述關(guān)鍵意圖的時(shí)候才使用。
As your code will be read by various people with varying knowledge of Java, Proper comments should be used to give overviews of your code and provide additional information that cannot be perceived from the code itself. Comments are supposed to describe the working of your code to be read by Quality assurance engineer, reviewer, or maintenance staff .
由于代碼將被具有不同 Java 知識(shí)的各種人閱讀,因此應(yīng)該使用適當(dāng)?shù)淖⑨寔砀攀龃a,并提供無法從代碼本身感知的其他信息。注釋應(yīng)該描述代碼的工作方式,以供質(zhì)量保證工程師、審閱者或維護(hù)人員閱讀。
注釋關(guān)鍵點(diǎn)應(yīng)該具備可讀性,有時(shí)候沒有任何注釋的代碼確實(shí)讓人痛苦,更可怕的是因?yàn)榇a更新但是注釋不更新,這樣的事情也時(shí)有發(fā)生=-=。最好的注釋應(yīng)該是代碼本身會(huì)告知作者意圖。
PS:個(gè)人看過的沒啥注釋也能完全看懂,并且看完覺得寫的非常美的代碼目前只有Redission和Spring。
10. 避免創(chuàng)建不必要對(duì)象
對(duì)象創(chuàng)建是最消耗內(nèi)存的操作之一,這就是為什么最好的 Java 實(shí)踐是避免創(chuàng)建任何不必要的對(duì)象,并且只應(yīng)在需要時(shí)創(chuàng)建它們。
9. 返回空集合而不是null
NPE is the most frequent exception in production with absolute leadership, do not give it a chance.
更進(jìn)一步說是讓整個(gè)系統(tǒng)不要出現(xiàn)空指針異常,不應(yīng)該因?yàn)轫?xiàng)目代碼妥協(xié)老舊的編程風(fēng)格。一定不要讓空指針有可乘之機(jī)。除了返回空集合以外,還可以利用Optional的工具類包裝有可能為Null的對(duì)象,顯式的告訴調(diào)用者對(duì)象有可能為Null。
An empty collection might have a different meaning than no collection at all.
有時(shí)候null 也可能是“有意義”的,使用的時(shí)候也需要考慮具體情況,當(dāng)然返回空集合肯定是沒有任何問題的。
Collections
空集合相關(guān)的輪子在Collections可以找到答案,但是需要注意需要合理使用,JDK5之后出現(xiàn)了emptyXXX等集合類,但是這些類“暗藏殺機(jī)”,因?yàn)樗鼈兊男Ч?strong>List.subList() 一樣是immutable 的。
雖然JDK的工具類可以減少不必要的對(duì)象創(chuàng)建,但是假設(shè)接收方需要在收到空集合之后卻還要往里面設(shè)置數(shù)據(jù),這時(shí)候毫無疑問就會(huì)拋異常了。
???/**
?????*?Returns?an?empty?list?(immutable).??This?list?is?serializable.
?????*
?????*?<p>This?example?illustrates?the?type-safe?way?to?obtain?an?empty?list:
?????*?<pre>
?????*?????List<String>?s?=?Collections.emptyList();
?????*?</pre>
?????*
?????*?@implNote
?????*?Implementations?of?this?method?need?not?create?a?separate?<tt>List</tt>
?????*?object?for?each?call.???Using?this?method?is?likely?to?have?comparable
?????*?cost?to?using?the?like-named?field.??(Unlike?this?method,?the?field?does
?????*?not?provide?type?safety.)
?????*
?????*?@param?<T>?type?of?elements,?if?there?were?any,?in?the?list
?????*?@return?an?empty?immutable?list
?????*
?????*?@see?#EMPTY_LIST
?????*?@since?1.5
?????*/
???? ("unchecked")
????public?static?final?<T>?List<T>?emptyList()?{
????????return?(List<T>)?EMPTY_LIST;
????}
我認(rèn)為emptyList()
更為合理的方法名稱是 immutableEmptyList()
和 mutableEmptyList()
,當(dāng)然mutableEmptyList()
就是new新的ArrayList()
這樣的方法,看起來是毫無意義的,也許JDK設(shè)計(jì)的時(shí)候也是考慮這種情況?
8. 精度選擇
Most processors take almost same time in processing the operations on float and double but double offers way more precision then float that is why it is best practice to use double when precision is important otherwise you can go with float as it requires half space than double.。
大型處理器在處理double和float的時(shí)候計(jì)算速度是類似的。但是在Java中大部分情況只要是涉及浮點(diǎn)數(shù)計(jì)算都是閉著眼睛用BigDecimal。如果精度很重要直接無腦使用BigDecimal,double和float都會(huì)騙人。
When precision is important, use BigDecimal. Double and Float aren’t accurate. They will betray you when you least expect it. 1 + 1 can be 1.99999999999.
當(dāng)精度很重要時(shí)請(qǐng)使用BigDecimal,Double和Float并不精確。它們會(huì)在你最不期望的時(shí)候背叛你。1+1可能是1.99999999999。
1. 項(xiàng)目結(jié)構(gòu)
有點(diǎn)過于基礎(chǔ)了。下面是Maven的經(jīng)典結(jié)構(gòu),如果是Maven新手可以看看項(xiàng)目的基本布局
src/main/java: For source files
src/main/resources: For resource files, like properties
src/test/java: For test source files
src/test/resources: For test resource files, like properties
2. 遵循命名規(guī)范
沒啥好講的,程序員的基礎(chǔ)素質(zhì)。最好的命名規(guī)范不是參考某一個(gè)標(biāo)準(zhǔn),而是能統(tǒng)一風(fēng)格布局代碼。
3. 不要吞異常
在異常處理時(shí)在 catch 塊中編寫適當(dāng)且有意義的消息是精英 java 開發(fā)人員首選的 java 最佳實(shí)踐。新手常常會(huì)把異常捕獲之后不做任何處理,吞異常是非常危險(xiǎn)的行為。
得益于IDE的幫助,catch之后不打印任何信息的情況不是很多見,但是打印堆棧其實(shí)也是非常消耗資源的操作,同時(shí)因?yàn)槭谴蛴≡诳刂婆_(tái),如果不調(diào)用日志保存關(guān)鍵信息也有可能導(dǎo)致關(guān)鍵信息丟失。
16. 使用枚舉替代靜態(tài)常量【建議】
在Java推出枚舉之前,定義一個(gè)常量基本只能使用下面的接口方式。在很多優(yōu)秀框架的最早期版本中經(jīng)常能看到這樣的寫法,并且到現(xiàn)在使用這種寫法的不在少數(shù) 。
public?interface?Color?{
????public?static?final?int?RED?=?0xff0000;
????public?static?final?int?BLACK?=?0x000000;
????public?static?final?int?WHITE?=?0xffffff;
}
It’s because the purpose of interfaces is for inheritance and polymorphism, not for static stuffs like that. So the best practice recommends us to use an enum instead.
這是因?yàn)榻涌诘哪康氖怯糜诶^承和多態(tài)性,而不是用于類似的靜態(tài)東西。所以最佳實(shí)踐建議我們使用枚舉來代替。
public?enum?Color?{
?
????BLACK(0x000000),
????WHITE(0xffffff),
????RED(0xff0000);
?
????private?int?code;
?
????Color(int?code)?{
????????this.code?=?code;
????}
?
????public?int?getCode()?{
????????return?this.code;
????}
}
使用枚舉和靜態(tài)常量的對(duì)比:
枚舉更具備描述性
Keep It Simple, Stupid。接口可以用于繼承和多態(tài),枚舉能干的事情基本是常量定義。
接口的靜態(tài)常量往往有其他的用途。
很少有人會(huì)在枚舉設(shè)計(jì)復(fù)雜的邏輯,因?yàn)槊杜e的可擴(kuò)展性很差并且理解和學(xué)習(xí)成本較高
17. 按作用域劃分成員變量
The best practice to organize member variables of a class by their scopes from most restrictive to least restrictive.
最好的做法是按其作用域從最嚴(yán)格到最不嚴(yán)格來組織一個(gè)類的成員變量。
public?class?StudentManager?{
????protected?List<Student>?listStudents;
????public?int?numberOfStudents;
????private?String?errorMessage;
????float?rowHeight;
????float?columnWidth;
????protected?String[]?columnNames;
????private?int?numberOfRows;
????private?int?numberOfColumns;
????public?String?title;
?
}
上面的成員變量定義眼花繚亂,通過作用域劃分如下:
public?class?StudentManager?{
????private?String?errorMessage;
????private?int?numberOfColumns;
????private?int?numberOfRows;
?
?
????float?columnWidth;
????float?rowHeight;
?
????protected?String[]?columnNames;
????protected?List<Student>?listStudents;
?
????public?int?numberOfStudents;
????public?String?title;
?
}
18. 在數(shù)字字段中使用下劃線【建議】
來自Java 7的小更新可以幫助我們把冗長(zhǎng)的數(shù)字字段寫得更易讀。
int?maxUploadSize?=?20971520;
long?accountBalance?=?1000000000000L;
float?pi?=?3.141592653589F;
和下面的代碼作比較:
int?maxUploadSize?=?20_971_520;
long?accountBalance?=?1_000_000_000_000L;
float?pi?=?3.141_592_653_589F。
哪一個(gè)更易懂?記得在數(shù)字字面中使用下劃線,以提高代碼的可讀性。
個(gè)人:尷尬,學(xué)了這么多年Java居然不知道有這種寫法.....
總結(jié)
這里羅列一下存在爭(zhēng)議以及印象比較深的部分:
StringBuilder替換字符串拼接。如果單純拼接幾個(gè)變量或者方法的結(jié)果,"+"號(hào)是完全沒有問題的。
雖然JDK 9 對(duì)于每次for循環(huán)new StringBuilder的BUG做了修復(fù),但國(guó)內(nèi)廣泛使用JDK8的場(chǎng)景下依然需要避免for循環(huán)大量的字符串拼接。
這種優(yōu)化不是關(guān)鍵,請(qǐng)更加關(guān)注合理的架構(gòu)設(shè)計(jì),因?yàn)檫@一點(diǎn)性能損耗對(duì)于現(xiàn)代處理器影響真的不大。
如果不需要考慮多線程的情況,更建議使用StringBuffer。
精度選擇,實(shí)際上不管有沒有精度要求,碰到需要小數(shù)的運(yùn)算建議使用BigDecimal,因?yàn)閐ouble和float的數(shù)學(xué)運(yùn)算真的很容易出問題。
注釋
訓(xùn)練寫出易懂的代碼而不是易懂的注釋
注釋應(yīng)該遵從KISS?!坝薮馈钡拇a最好理解。
在大型的數(shù)字參數(shù)值中使用下劃線,這是一個(gè)很容易忽略的Java小技巧,對(duì)于和金額有關(guān)的處理,這種寫法非常有幫助。
參考
另一位讀者:Best coding practices every Java developer should follow?
10 Java Core Best Practices Every Java Programmer Should Know
參考資料
[1]
Java default Initialization of Instance Variables and Initialization Blocks: https://www.codejava.net/java-core/the-java-language/java-default-initialization-of-instance-variables-and-initialization-blocks
[2]DayOfWeek: https://docs.oracle.com/javase/8/docs/api/java/time/DayOfWeek.html