寫在前面
《Effective Java》原書國內的翻譯只出版到第二版入客,書籍的編寫日期距今已有十年之久。這期間夭咬,Java已經更新?lián)Q代好幾次铆隘,有些實踐經驗已經不再適用。去年底膀钠,作者結合Java7肿嘲、8、9的最新特性雳窟,編著了第三版(參考https://blog.csdn.net/u014717036/article/details/80588806)封救。當前只有英文版本拇涤,可以在互聯(lián)網搜索到PDF原書誉结。本讀書筆記都是基于原書的理解。
以下是正文部分
通用編程(General Programming)
本章包括:
- 實踐57 最小化局部變量作用域(Minimize the scope of local variables)
- 實踐58 使用
foreach
循環(huán)替代傳統(tǒng)循環(huán)(Prefer for-each loops to traditional for loops) - 實踐59 知道并使用庫(Know and use the libraries)
- 實踐60 不要在需要精確結果的場景使用浮點數(shù)(Avoid float and double if exact answers are required)
- 實踐61 使用基礎類型替代封裝基礎類型(Prefer primitive types to boxed primitives)
- 實踐62 盡量用更恰當?shù)念愋吞鎿Q
String
(Avoid strings where other types are more appropriate) - 實踐63 拼接
String
是低效的(Beware the performance of string concatenation) - 實踐64 使用對象的接口來指示對象(Refer to objects by their interfaces)
- 實踐65 使用接口替代反射(Prefer interfaces to reflection)
- 實踐66 審慎地使用原生方法(Use native methods judiciously)
- 實踐67 審慎地優(yōu)化代碼(Optimize judiciously)
- 實踐68 堅持使用公認的命名規(guī)范(Adhere to generally accepted naming conventions)
實踐57 最小化局部變量作用域(Minimize the scope of local variables)
C語言的一個約定是將變量聲明在函數(shù)最前面掉盅,實際上旭贬,這完全可以改變稀轨。Java 中,一個最小化局部變量作用域的好辦法是奋刽,在使用變量時才去聲明它。這樣還能使得代碼閱讀起來更清晰肚吏。
- 除了
try..catch...
中的變量外狭魂,其他變量都應該在聲明時初始化党觅。 - 相比while循環(huán)杯瞻,for循環(huán)能夠幫助我們定義更加局部的變量炫掐,因此應多使用for循環(huán)。
- 函數(shù)的功能應當盡量小且聚焦旗唁,這樣避免定義過多的變量痹束,作用域相互干擾、混淆电谣。
實踐58 使用 foreach
循環(huán)替代傳統(tǒng) for
循環(huán)(Prefer for-each loops to traditional for loops)
先看看傳統(tǒng) for
循環(huán)的示例:
// Not the best way to iterate over a collection!
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e
}
// Not the best way to iterate over an array!
for (int i = 0; i < a.length; i++) {
... // Do something with a[i]
}
for-each
方法抹蚀,更加簡潔企垦,可以避免越界訪問問題钞诡。
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
在嵌套循環(huán)中,for-each
的優(yōu)勢更加明顯荧降。在傳統(tǒng)循環(huán)中朵诫,為了維持第一層循環(huán)的某個變量值,我們需要單獨定義一個變量剪返;而 for-each
不需要這樣做脱盲。
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
Suit suit = i.next();
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(suit, j.next()));
}
for (Suit suit : suits)
for (Rank rank : ranks)
deck.add(new Card(suit, rank));
但是,要注意掖看,有幾個場景是不應該使用 for-each
的:
- 破壞性過濾(Destructive filtering): 例如遍歷時需要刪除集合中的元素,則應當顯式使用迭代器毅待。
- 元素變換(Transforming): 需要修改元素值時耳峦。
- 并行遍歷(Parallel iteration): 同時遍歷多個容器。
for-each
支持遍歷所有實現(xiàn)了 Iterable
接口的對象驶乾。
實踐59 知道并使用庫(Know and use the libraries)
在使用庫生成隨機數(shù)時循签,大多數(shù)人會這么寫:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
- 如果n是2的平方切值較小,則不久就會形成序列循環(huán)
- 如果n不是2的平方风科,則返回某些數(shù)的概率會大于另一些數(shù)
- 極端情況下乞旦,返回的值甚至可能超出限定范圍,這是處理正負值時可能引起的
實質上故痊,完全可以使用庫里面的 Random.nextInt(int)
來實現(xiàn)上述功能玖姑。庫函數(shù)由專家編寫,經歷成千上萬使用者的考驗戴甩,其安全性闪彼、功能性都有保障。有 bug 也會及時修復课蔬。我們也可以從實現(xiàn)-測試-迭代中抽脫出來郊尝。另外,使用庫函數(shù)也會讓你的代碼融入主流扎即,更容易閱讀、理解各拷。
Java強大的社區(qū)支持闷营,使得每個發(fā)布版本都會對庫函數(shù)有很多的更新迭代。我們要盡量多得掌握速蕊、了解每個版本的關鍵庫娘赴,包括以下幾個:
- java.lang
- java.util
- java.io
實踐60 不要在需要精確結果的場景使用浮點數(shù)(Avoid float and double if exact answers are required)
Java中浮點數(shù)的存在主要是為了科學計算與工程計算诽表。在需要精確結果的場景,尤其是涉及到錢款的運算竿奏,不要使用浮點數(shù)泛啸,使用 int
, long
, BigDecimal
。
錯誤地使用浮點數(shù)示例代碼:
public static void main(String[] args) {
double funds = 1.00;
int itemsBought = 0;
for (double price = 0.10; funds >= price; price += 0.10) {
funds -= price;
itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Change: $" + funds);
}
//OUTPUT:
//3 items bought.
//Change: $0.3999999999999999
使用 BigDecimal
修正后的代碼:
public static void main(String[] args) {
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)) {
funds = funds.subtract(price);
itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds);
}
//OUTPUT:
//4 items bought.
//Money left over: $0.00
實踐61 使用基礎類型替代封裝基礎類型(Prefer primitive types to boxed primitives)
基礎類型(例如 int
)與封裝基礎類型(例如 Integer
)有三個主要區(qū)別:
- 基礎類型只有值,封裝基礎類型還有對象信息
- 封裝基礎類型的值可能為
null
- 基礎類型在時間效率和空間效率更優(yōu)
主要的坑是宗雇,使用 ==
去比較兩個封裝對象并不是比較其值莹规,相同值得封裝基礎對象也會返回 false
。
在基礎類型與封裝基礎類型混用的場景舞虱,封裝基礎類型會自動“解封”母市,如果此時對象為 null
,將拋出異常椅寺。
注意,有幾種情況必須使用封裝基礎類型桐玻,除此之外荆萤,應盡量使用基礎類型。
- 集合的對象不能使用基礎類型
- 泛型參數(shù)不能使用基礎類型
- 反射調用不能使用基礎類型
實踐62 盡量用更恰當?shù)念愋吞鎿Q String
(Avoid strings where other types are more appropriate)
String
類型不應當作為值類型偏竟、枚舉類型梧油、聚合對象類型、線程關鍵key來使用褪子。
實踐63 拼接 String
是低效的(Beware the performance of string concatenation)
使用 +
來拼接 String
非常方便骗村,但是由于 String
對象的不可變性胚股,這種拼接操作是耗時的,它需要拷貝兩個對象到一個新的對象琅拌。如果在 for 循環(huán)中使用进宝,效率會呈指數(shù)降低。
在拼接操作比較頻繁是谭胚,我們就應該選用 StringBuilder
類來進行未玻。注意,最好在一開始定義好 StringBuilder
的長度旁趟。示例代碼如下:
public String statement() {
StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
for (int i = 0; i < numItems(); i++)
b.append(lineForItem(i));
return b.toString();
}
實踐64 使用對象的接口來指示對象(Refer to objects by their interfaces)
在使用對象時庇绽,如果對象是某個適當接口的實現(xiàn)癣猾,那么我們應該使用接口來指示該對象纷宇。通常蛾方,確實有必要使用類信息的只有一處——使用構造函數(shù)創(chuàng)建對象時。其他地方包括代碼的如下關鍵點拓春,都最好使用接口亚隅。當然,如果對象要用到接口實現(xiàn)中的某些特定方法懂鸵,那么還是該使用具體的對象類來標識對象行疏。
- 對象作為參數(shù)
- 返回值類型
- 變量
- 類屬性
// 推薦用法
Set<Son> sonSet = new LinkedHashSet<>();
// 不推薦用法
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
這樣做可以使得程序擴展性更好酿联。例如上述代碼想改用 HashSet
,則只需要換掉 LinkedHashSet
就可以了周崭。
實踐65 使用接口替代反射(Prefer interfaces to reflection)
使用 java.lang.reflect
可以基于類名喳张,去訪問到構造函數(shù)、方法、屬性人柿。這帶來了一些便利性凫岖,但是同時,也引入了幾個問題:
- 由于編譯時無法進行檢測使用是否合法哥放,反射出的東西是否可用,給程序增加了額外的風險踩身。
- 為了實現(xiàn)反射挟阻,程序寫起來麻煩,可讀性也低
- 性能下降
很少很少有程序是必須使用反射來實現(xiàn)的附鸽,哪怕是目前主要的兩中應用:依賴注入框架坷备、代碼分析工具,也在逐漸減少反射的使用赌蔑。
public static void main(String[] args) {
// Translate the class name into a Class object
Class<? extends Set<String>> cl = null;
try {
cl =
(Class<? extends Set<String>>)
// Unchecked cast!
Class.forName(args[0]);
} catch (ClassNotFoundException e) {
fatalError("Class not found.");
}
// Get the constructor
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
fatalError("No parameterless constructor");
}
// Instantiate the set
Set<String> s = null;
try {
s = cons.newInstance();
} catch (IllegalAccessException e) {
fatalError("Constructor not accessible");
} catch (InstantiationException e) {
fatalError("Class not instantiable.");
} catch (InvocationTargetException e) {
fatalError("Constructor threw " + e.getCause());
} catch (ClassCastException e) {
fatalError("Class doesn't implement Set");
}
// Exercise the set
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
private static void fatalError(String msg) {
System.err.println(msg);
System.exit(1);
}
實踐66 審慎地使用原生方法(Use native methods judiciously)
在 Java 中惯雳,使用JNI可以引入原生方法鸿摇,這些方法可以是使用 C,C++ 語言編寫的潮孽。早期筷黔,原生方法有三個作用:
- 能夠訪問寄存器等平臺特有的模塊
- 能夠訪問原生方法的庫和數(shù)據(jù)
- 特殊部分的性能優(yōu)化
但是,隨著 Java 的發(fā)展椎例,現(xiàn)在幾乎用不上了请祖∷敛叮總之,能不用則不用。
實踐67 審慎地優(yōu)化代碼(Optimize judiciously)
對于代碼優(yōu)化喻奥,有三條名言捏悬。
- 在計算機領域以優(yōu)化之名所犯的罪比其他所有原因加起來還要多(并且還不一定達到優(yōu)化的目的)邮破。[More computing sins are committed in the name of efficiency (without necessarily achieving it) than for any other single reason—including blind stupidity.]
- 過早的優(yōu)化是萬惡的根源。[We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.]
- 關于優(yōu)化的建議是:1矫渔、不要優(yōu)化摧莽。2、在你成為專家油够,在你有了清晰完美的方案之前征懈,不要優(yōu)化卖哎。[We follow two rules in the matter of optimization: Rule 1. Don’t do it. Rule 2 (for experts only). Don’t do it yet—that is, not until you have a perfectly clear and unoptimized solution.]
我們應致力于編寫能用的、好的程序焕窝,而不是快的的程序维贺。好的程序都是解耦的,在內部完成邏輯虐秋,后續(xù)優(yōu)化大有空間垃沦。另外,不優(yōu)化的意思并不是說在寫代碼的時候就不考慮性能問題起愈,我們應在初次盡量搭好架構译仗,不要做大手術纵菌。
我們應避免引入明顯降低性能的代碼塊。
實踐68 堅持使用公認的命名規(guī)范(Adhere to generally accepted naming conventions)
(完)