Java最佳實(shí)踐


計(jì)算機(jī)編程中,最佳實(shí)踐是許多開發(fā)人員遵循的一組非正式規(guī)則,以提高軟件質(zhì)量、可讀性和可維護(hù)性。在應(yīng)用程序長(zhǎng)時(shí)間保持使用的情況下,最佳實(shí)踐尤其有益,這樣它最初是由一個(gè)團(tuán)隊(duì)開發(fā)的,然后由不同的人組成的維護(hù)團(tuán)隊(duì)進(jìn)行維護(hù)。
本教程將提供Java最佳實(shí)踐的概述,以及每個(gè)條目的解釋,包括Java編程的頂級(jí)最佳實(shí)踐列表中的每一項(xiàng)。
Java編程最佳實(shí)踐概覽
雖然Java最佳實(shí)踐的完整列表可能很長(zhǎng),但對(duì)于那些正在努力提高代碼質(zhì)量的編碼人員來說,有幾個(gè)被認(rèn)為是一個(gè)很好的起點(diǎn),包括使用適當(dāng)?shù)拿?guī)范、使類成員私有化、避免使用空的catch塊、避免內(nèi)存泄漏以及正確地注釋代碼塊:
使用適當(dāng)?shù)拿?guī)范
將類成員設(shè)置為私有
在長(zhǎng)數(shù)字文字中使用下劃線
避免空的catch塊
使用StringBuilder或StringBuffer進(jìn)行字符串連接
避免冗余初始化
使用增強(qiáng)型for循環(huán)代替帶計(jì)數(shù)器的for循環(huán)
正確處理空指針異常
Float或Double:哪一個(gè)是正確的選擇?
使用單引號(hào)和雙引號(hào)
避免內(nèi)存泄漏
返回空集合而不是返回Null元素
高效使用字符串
避免創(chuàng)建不必要的對(duì)象
正確注釋代碼
Java中的類成員應(yīng)該是私有的
在Java中,類的成員越不可訪問,越好!第一步是使用private訪問修飾符。目標(biāo)是促進(jìn)理想的封裝,這是面向?qū)ο缶幊蹋∣OP)的基本概念之一。太多時(shí)候,新的開發(fā)人員沒有正確地為類分配訪問修飾符,或者傾向于將它們?cè)O(shè)置為public以使事情更容易。
考慮以下字段被設(shè)置為public的類:
public class BandMember {
??public String name;
??public String instrument;
}
在這里,類的封裝性被破壞了,因?yàn)槿魏稳硕伎梢灾苯痈倪@些值,如下所示:
BandMember billy = new BandMember();
billy.name = "George";
billy.instrument = "drums";
使用private訪問修飾符與類成員一起可以將字段隱藏起來,防止用戶通過setter方法之外的方式更改數(shù)據(jù):
public class BandMember {
??private String name;
??private String instrument;
??
??public void setName(String name) {
????this.name = name;
??}
??public void setInstrument(String instrument)
????this.instrument = instrument;
??}
}
在setter方法中也是放置驗(yàn)證代碼和/或管理任務(wù)(如增加計(jì)數(shù)器)的理想位置。
在長(zhǎng)數(shù)字文字中使用下劃線
得益于Java 7的更新,開發(fā)人員現(xiàn)在可以在長(zhǎng)數(shù)字字面量中使用下劃線(_),以提高可讀性。以下是在允許下劃線之前一些長(zhǎng)數(shù)字字面量的示例:
int minUploadSize = 05437326;
long debitBalance = 5000000000000000L;
float pi = 3.141592653589F;
我想您會(huì)同意下劃線使值更易讀:
int minUploadSize = 05_437_326;
long debitBalance = 5_000_000_000_000_000L;
float pi = 3.141_592_653_589F;
避免空的Catch塊
在Java中,把catch塊留空是非常不好的習(xí)慣,有兩個(gè)原因:一是它可能導(dǎo)致程序默默地失敗,二是程序可能會(huì)繼續(xù)運(yùn)行而不會(huì)發(fā)生任何異常。這兩種結(jié)果都會(huì)使調(diào)試變得非常困難
考慮以下程序,它從命令行參數(shù)中計(jì)算兩個(gè)數(shù)字的和:
public class Sum {
??public static void main(String[] args) {
????int a = 0;
????int b = 0;
????
????try {
??????a = Integer.parseInt(args[0]);
??????b = Integer.parseInt(args[1]);
????} catch (NumberFormatException ex) {
????}
????
????int sum = a + b;
????
????System.out.println(a + " + " + b + " = " + sum);
??}
}
Java的parseInt()方法會(huì)拋出NumberFormatException異常,需要開發(fā)人員在其調(diào)用周圍使用try/catch塊來捕獲異常。不幸的是,這位開發(fā)人員選擇忽略拋出的異常!因此,傳入無效參數(shù),例如“45y”,將導(dǎo)致關(guān)聯(lián)的變量被賦為其類型的默認(rèn)值,對(duì)于int類型來說是0。

通常,在捕獲異常時(shí),程序員應(yīng)采取以下三個(gè)行動(dòng)中的一個(gè)或多個(gè):
開發(fā)人員最起碼應(yīng)該通知用戶異常情況,要么讓他們重新輸入無效的值,要么讓他們知道程序必須提前終止。
使用?JDK Logging 或?Log4J 記錄異常日志。
將異常封裝并作為一個(gè)新的、更適合應(yīng)用程序的異常重新拋出。
以下是重新編寫后的Sum應(yīng)用程序,用于通知用戶輸入無效并因此終止程序:
public class Sum {
??public static void main(String[] args) {
????int a = 0;
????int b = 0;
????
????try {
??????a = Integer.parseInt(args[0]);
????} catch (NumberFormatException ex) {
??????System.out.println(args[0] + " is not a number. Aborting...");
??????return;
????}
????
????try {
??????b = Integer.parseInt(args[1]);
????} catch (NumberFormatException ex) {
??????System.out.println(args[1] + " is not a number. Aborting...");
??????return;
????}
????
????int sum = a + b;
????
????System.out.println(a + " + " + b + " = " + sum);
??}
}
以下是我們觀察到的結(jié)果:

使用StringBuilder或StringBuffer進(jìn)行字符串拼接
“+” 運(yùn)算符是在 Java 中快速和簡(jiǎn)便地組合字符串的方法。在 Hibernate 和 JPA 時(shí)代之前,對(duì)象是手動(dòng)持久化的,通過從頭開始構(gòu)建 SQL INSERT 語句來實(shí)現(xiàn)!以下是一個(gè)存儲(chǔ)一些用戶數(shù)據(jù)的示例:
String sql = "Insert Into Users (name, age)";
???????sql += " values ('" + user.getName();
???????sql += "', '" + user.getage();
???????sql += "')";
很遺憾,當(dāng)像上面那樣連接多個(gè)字符串時(shí),Java編譯器必須創(chuàng)建多個(gè)中間字符串對(duì)象,然后將它們合并成最終連接的字符串。
相反,我們應(yīng)該使用StringBuilder或StringBuffer類。兩者都包含函數(shù),可以連接字符串而無需創(chuàng)建中間String對(duì)象,從而節(jié)省處理時(shí)間和不必要的內(nèi)存使用。
以前的代碼可以使用 StringBuilder 重寫,如下所示:
StringBuilder sqlSb = new StringBuilder("Insert Into Users (name, age)");
sqlSb.append(" values ('").append(user.getName());
sqlSb.append("', '").append(user.getage());
sqlSb.append("')");
String sqlSb = sqlSb.toString();
這對(duì)開發(fā)者來說可能要費(fèi)點(diǎn)事兒,但是這樣做非常值得!
StringBuffer 與 StringBuilder
雖然StringBuffer和StringBuilder類都比“+”運(yùn)算符更可取,但它們并不相同。StringBuilder比StringBuffer更快,但不是線程安全的。因此,在非多線程環(huán)境中進(jìn)行字符串操作時(shí),應(yīng)使用StringBuilder;否則,請(qǐng)使用StringBuffer類。
避免冗余初始化
盡管某些語言如TypeScript強(qiáng)烈建議在聲明時(shí)初始化變量,但在Java中并非總是必要的,因?yàn)樗诼暶鲿r(shí)將默認(rèn)初始化值(如0、false和null)分配給變量。
因此,Java的最佳實(shí)踐是要知道成員變量的默認(rèn)初始化值,除非您想將它們?cè)O(shè)置為除默認(rèn)值以外的其他值,否則不要顯式初始化變量。
以下是一個(gè)計(jì)算從1到1000的自然數(shù)之和的短程序。請(qǐng)注意,只有部分變量被初始化:
class VariableInitializationExample {
??public static void main(String[] args) {
????
????// automatically set to 0
????int sum;
????final numberOfIterations = 1000;
????// Set the loop counter to 1
????for (int i = 1; i &= numberOfIterations; ++i) {
??????sum += i;
????}
???????
????System.out.println("Sum = " + sum);
??}
}
使用增強(qiáng)型for循環(huán)代替需要計(jì)數(shù)器的for循環(huán)
盡管 for 循環(huán)在某些情況下很有用,但是計(jì)數(shù)器變量可能會(huì)引起錯(cuò)誤。例如,計(jì)數(shù)器變量可能會(huì)在稍后的代碼中被無意中更改。即使從 1 而不是從 0 開始索引,也可能導(dǎo)致意外行為。出于這些原因,for-each 循環(huán)(也稱為增強(qiáng)型 for 循環(huán))可能是更好的選擇。
考慮以下代碼:
String[] names = {"Rob", "John", "George", "Steve"};
for (int i = 0; i < names.length; i++) {
??System.out.println(names[i]);
}
在這里,變量i既是循環(huán)計(jì)數(shù)器,又是數(shù)組names的索引。盡管這個(gè)循環(huán)只是打印每個(gè)名稱,但如果下面有修改i的代碼,就會(huì)變得棘手。我們可以通過使用下面所示的for-each循環(huán)輕松避開整個(gè)問題:
for (String name : names) {
??System.out.println(name);
}
使用增強(qiáng)的for循環(huán),出錯(cuò)的機(jī)會(huì)要少得多!
?合理處理空指針異常
空指針異常在Java中是一個(gè)非常常見的問題,可能是由于其面向?qū)ο蟮脑O(shè)計(jì)所致。當(dāng)您試圖在 Null 對(duì)象引用上調(diào)用方法時(shí),就會(huì)發(fā)生 Null Pointer 異常。這通常發(fā)生在在類實(shí)例化之前調(diào)用實(shí)例方法的情況下,如下例所示:
Office office;
// later in the code...
Employee[] employees = office.getEmployees();
雖然你無法完全消除 Null Pointer Exceptions,但有方法可以將其最小化。一種方法是在調(diào)用對(duì)象的方法之前檢查對(duì)象是否為 Null。以下是使用三元運(yùn)算符的示例:
Office office;
// later in the code...
Employee[] employees = office == null ? 0 : office.getEmployees();
你可能還想拋出自己的異常:
Office office;
Employee[] employees;
// later in the code...
if (office == null) {
??throw new CustomApplicationException("Office can't be null!");
} else {
??employees = office.getEmployees();
}
Float或Double:應(yīng)該使用哪個(gè)?
?浮點(diǎn)數(shù)和雙精度數(shù)是相似的類型,因此許多開發(fā)人員不確定該選擇哪種類型。兩者都處理浮點(diǎn)數(shù),但具有非常不同的特性。例如,float的大小為32位,而double分配了64位的內(nèi)存空間,因此double可以處理比float大得多的小數(shù)。然后有一個(gè)精度問題:float只能容納7位精度。極小的指數(shù)大小意味著一些位是不可避免的丟失的。相比之下,double為指數(shù)分配了更多的位數(shù),允許它處理高達(dá)15位精度。
因此,當(dāng)速度比準(zhǔn)確性更重要時(shí),通常建議使用float。盡管大多數(shù)程序不涉及大量計(jì)算,但在數(shù)學(xué)密集型應(yīng)用中,精度差異可能非常顯著。當(dāng)需要的小數(shù)位數(shù)已知時(shí),float也是一個(gè)不錯(cuò)的選擇。當(dāng)精度非常重要時(shí),double應(yīng)該是你的首選。只需記住,Java強(qiáng)制使用double作為處理浮點(diǎn)數(shù)的默認(rèn)數(shù)據(jù)類型,因此您可能需要附加字母"f"來明確表示float,例如,1.2f。
單引號(hào)和雙引號(hào)在字符串連接中的使用
在Java中,雙引號(hào)(“)用于保存字符串,單引號(hào)用于字符(由char類型表示)。當(dāng)我們嘗試使用+連接運(yùn)算符連接字符時(shí),可能會(huì)出現(xiàn)問題。問題在于使用+連接字符會(huì)將char的值轉(zhuǎn)換為ascii,從而產(chǎn)生數(shù)字輸出。以下是一些示例代碼,以說明這一點(diǎn):
char a, b, c;
a = 'a';
b = 'b';
c = 'c';
str = a + b + c; // not "abc", but 294!
就像字符串連接最好使用StringBuilder或StringBuffer類一樣,字符連接也是如此!在上面的代碼中,變量a、b和c可以通過以下方式組合成一個(gè)字符串:
new StringBuilder().append(a).append(b).append(c).toString()
我們可以在下面觀察到期望的結(jié)果:

避免Java中的內(nèi)存泄漏
在Java中,開發(fā)人員并沒有太多關(guān)于內(nèi)存管理的控制權(quán),因?yàn)镴ava通過垃圾回收自動(dòng)地進(jìn)行內(nèi)存管理。盡管如此,有一些Java最佳實(shí)踐可以幫助開發(fā)人員避免內(nèi)存泄漏,例如:
避免創(chuàng)建不必要的對(duì)象。
避免使用"+"運(yùn)算符進(jìn)行字符串連接。
避免在會(huì)話中存儲(chǔ)大量數(shù)據(jù)。
在會(huì)話不再使用時(shí),及時(shí)讓會(huì)話超時(shí)。
避免使用靜態(tài)對(duì)象,因?yàn)樗鼈冊(cè)谡麄€(gè)應(yīng)用程序的生命周期內(nèi)存在。
在與數(shù)據(jù)庫交互時(shí),不要忘記在finally塊中關(guān)閉ResultSet、Statements和Connection對(duì)象。
返回空集合而不是null引用。
你知道嗎,null引用經(jīng)常被稱為軟件開發(fā)中最嚴(yán)重的錯(cuò)誤嗎?1965年,Tony Hoare在設(shè)計(jì)面向?qū)ο笳Z言(OOP)的引用的第一個(gè)全面類型系統(tǒng)時(shí)發(fā)明了null引用。后來在2009年的一次會(huì)議上,Hoare為自己的發(fā)明道歉,承認(rèn)他的創(chuàng)造“導(dǎo)致了無數(shù)的錯(cuò)誤、漏洞和系統(tǒng)崩潰,在過去四十年中可能造成了數(shù)十億美元的痛苦和損失?!?/p>
在Java中,通常最好返回空值而不是null,特別是當(dāng)返回集合、可枚舉對(duì)象或?qū)ο髸r(shí)更為重要。盡管你自己的代碼可能會(huì)處理返回的null值,但其他開發(fā)人員可能會(huì)忘記編寫空值檢查,甚至沒有意識(shí)到null是可能的返回值!
以下是一些Java代碼,它以ArrayList的形式獲取庫存中的書籍列表。但是,如果列表為空,則返回一個(gè)空列表:
private final List<Book> booksInStock = ...
public List<Book> getInStockBooks() {
??return booksInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(booksInStock);
}
這使得方法的調(diào)用者可以在不必首先檢查null引用的情況下迭代列表:
(Book book: getInStockBooks()) {
??// do something with books
}
Java 中字符串的高效使用
我們已經(jīng)討論了使用 + 連接操作符可能產(chǎn)生的副作用,但還有其他一些方法可以更有效地使用字符串,以避免浪費(fèi)內(nèi)存和處理器周期。例如,在實(shí)例化 String 對(duì)象時(shí),通常最好直接創(chuàng)建 String 而不是使用構(gòu)造函數(shù)。原因是什么?使用直接創(chuàng)建 String 比使用構(gòu)造函數(shù)更快(更不用說更少的代碼!)。
這里是在Java中創(chuàng)建字符串的兩種等效方式:直接創(chuàng)建和使用構(gòu)造函數(shù):
// directly
String str = "abc";
?????
// using a constructor
char data[] = {'a', 'b', 'c'};
String str = new String(data);
雖然兩種方法都是等效的,但直接創(chuàng)建字符串的方式更好。
Java中不必要的對(duì)象創(chuàng)建
你知道嗎?在Java中,對(duì)象的創(chuàng)建是消耗內(nèi)存最多的操作之一。因此,在沒有充分理由的情況下,最好避免創(chuàng)建對(duì)象,并僅在絕對(duì)必要時(shí)才這樣做。
那么,如何將這個(gè)實(shí)踐起來呢?具有諷刺意味的是,像我們上面看到的直接創(chuàng)建字符串就是避免不必要?jiǎng)?chuàng)建對(duì)象的一種方式!
以下是一個(gè)更復(fù)雜的示例:
以下是一個(gè)Person類的例子,它包括一個(gè)isBabyBoomer()方法,用于判斷此人是否屬于“嬰兒潮”年齡段,出生于1946年至1964年之間:
public class Person {
??private final Date birthDate;
??
??public boolean isBabyBoomer() {
????// Unnecessary allocation of expensive object!
????Calendar gmtCal =
????????Calendar.getInstance(TimeZone.getTimeZone("GMT"));
????gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
????Date boomStart = gmtCal.getTime();
????gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
????Date boomEnd = gmtCal.getTime();
????
????return birthDate.compareTo(boomStart) >= 0 &&
???????????birthDate.compareTo(boomEnd)???<??0;
??}
}
?isBabyBoomer()方法每次被調(diào)用都會(huì)創(chuàng)建一個(gè)新的Calendar、TimeZone和兩個(gè)Date實(shí)例,這是不必要的。糾正這種低效的方法之一是使用靜態(tài)初始化器,以便只在初始化時(shí)創(chuàng)建Calendar、TimeZone和Date對(duì)象,而不是每次調(diào)用isBabyBoomer()方法。
class Person {
??private final Date birthDate;
??
??// The starting and ending dates of the baby boom.
??private static final Date BOOM_START;
??private static final Date BOOM_END;
??
??static {
????Calendar gmtCal =
??????Calendar.getInstance(TimeZone.getTimeZone("GMT"));
????gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
????BOOM_START = gmtCal.getTime();
????gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
????BOOM_END = gmtCal.getTime();
??}
??
??public boolean isBabyBoomer() {
????return birthDate.compareTo(BOOM_START) >= 0 &&
???????birthDate.compareTo(BOOM_END)???????<??0;
??}
}
Java中適當(dāng)?shù)淖⑨?/span>
清晰簡(jiǎn)潔的注釋在閱讀其他開發(fā)人員的代碼時(shí)非常有用。以下是寫出高質(zhì)量注釋的幾個(gè)指南:
注釋不應(yīng)該重復(fù)代碼。
好的注釋不能彌補(bǔ)代碼不清晰的問題。
?如果您無法編寫清晰的注釋,則代碼可能存在問題。
在注釋中解釋不符合慣用方式的代碼。
在最有用的地方包含指向外部參考文獻(xiàn)的鏈接。
在修復(fù)錯(cuò)誤時(shí)添加注釋。
使用注釋標(biāo)記未完成的實(shí)現(xiàn),通常使用標(biāo)記“TODO:”開頭。
總結(jié)
在本文中,我們了解了15個(gè)Java最佳實(shí)踐,并探討了類成員封裝、在冗長(zhǎng)的數(shù)字字面值中使用下劃線、避免空catch塊、正確完成字符串連接、如何避免冗余初始化以及使用增強(qiáng)的for循環(huán)。
【注】本文譯自: Java Best Practices | Developer.com (https://www.developer.com/languages/javascript/java-best-practices/)