Effective Java英文第三版讀書筆記(4) -- 通用編程最佳實踐

寫在前面

《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; 
}
  1. 如果n是2的平方切值較小,則不久就會形成序列循環(huán)
  2. 如果n不是2的平方风科,則返回某些數(shù)的概率會大于另一些數(shù)
  3. 極端情況下乞旦,返回的值甚至可能超出限定范圍,這是處理正負值時可能引起的

實質上故痊,完全可以使用庫里面的 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 ,將拋出異常椅寺。

注意,有幾種情況必須使用封裝基礎類型桐玻,除此之外荆萤,應盡量使用基礎類型。

  1. 集合的對象不能使用基礎類型
  2. 泛型參數(shù)不能使用基礎類型
  3. 反射調用不能使用基礎類型

實踐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)

image.png

(完)

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市序苏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌围来,老刑警劉巖匈睁,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件航唆,死亡現(xiàn)場離奇詭異,居然都是意外死亡粪狼,警方通過查閱死者的電腦和手機超营,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門演闭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人窝革,你說我怎么就攤上這事吕座。” “怎么了漆诽?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵厢拭,是天一觀的道長。 經常有香客問我畦贸,道長楞捂,這世上最難降的妖魔是什么寨闹? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮涵但,結果婚禮上帖蔓,老公的妹妹穿的比我還像新娘。我一直安慰自己澈侠,他們只是感情好埋酬,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布写妥。 她就那樣靜靜地躺著,像睡著了一般祝峻。 火紅的嫁衣襯著肌膚如雪扎筒。 梳的紋絲不亂的頭發(fā)上嗜桌,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機與錄音浮定,去河邊找鬼。 笑死雳灵,一個胖子當著我的面吹牛闸盔,可吹牛的內容都是我干的琳省。 我是一名探鬼主播针贬,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼桦他,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了快压?” 一聲冷哼從身側響起蔫劣,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤脉幢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后沪曙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萎羔,經...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了昵宇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓦哎。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡柔逼,死狀恐怖愉适,靈堂內的尸體忽然破棺而出癣漆,到底是詐尸還是另有隱情,我是刑警寧澤癌蓖,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布婚肆,位于F島的核電站,受9級特大地震影響较性,放射性物質發(fā)生泄漏用僧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一赞咙、第九天 我趴在偏房一處隱蔽的房頂上張望责循。 院中可真熱鬧,春花似錦攀操、人聲如沸沼死。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽意蛀。三九已至,卻和暖如春健芭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背慈迈。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工若贮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人痒留。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓谴麦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伸头。 傳聞我的和親對象是個殘疾皇子匾效,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內容