《Effective Java 3rd》3分鐘速成:(57-63)General Programming1

前言:

Chapter 9. General Programming

  • 57. 最小化局部變量的作用域
  • 58. for-each 循環優於傳統 for 循環
  • 59. 瞭解並使用庫
  • 60. 若需要精確答案就應避免使用 float 和 double 類型
  • 61. 基本數據類型優於包裝類
  • 62. 當使用其他類型更合適時應避免使用字符串
  • 63. 當心字符串連接引起的性能問題

57. 最小化局部變量的作用域

這條目在性質上類似於條目 15,即“最小化類和成員的可訪問性”。通過最小化局部變量的作用域,可以提高代碼的可讀性和可維護性,並降低出錯的可能性。

使用原則:

  • 1、用於最小化局部變量作用域的最強大的技術是再首次使用的地方聲明它。
  • 2、幾乎每個局部變量聲明都應該包含一個初始化器。如果還沒有足夠的信息來合理地初始化一個變量,那麼應該推遲聲明,直到認為可以這樣做。
  • 3、這個規則的一個例外是 try-catch 語句。如果一個變量被初始化為一個表達式,該表達式的計算結果可以拋出一個已檢查的異常,那麼該變量必須在 try 塊中初始化(除非所包含的方法可以傳播異常)。如果該值必須在 try 塊之外使用,那麼它必須在 try 塊之前聲明,此時它還不能被「合理地初始化」。例如,參照條目 65 中的示例。
  • 4、循環提供了一個特殊的機會來最小化變量的作用域。傳統形式的 for 循環和 for-each 形式都允許聲明循環變量,將其作用域限制在需要它們的確切區域。 (該區域由循環體和 for 關鍵字與正文之間的括號中的代碼組成)。因此,如果循環終止後不需要循環變量的內容,那麼優先選擇 for 循環而不是 while 循環。

總結:

  • 1、最小化局部變量作用域的最終技術是保持方法小而集中。
  • 2、如果在同一方法中組合兩個行為(activities),則與一個行為相關的局部變量可能會位於執行另一個行為的代碼範圍內。
  • 3、為了防止這種情況發生,只需將方法分為兩個:每個行為對應一個方法。


58. for-each 循環優於傳統 for 循環

for 循環的缺陷:

  • 1、迭代器和索引變量都很混亂——你只需要元素而已。
  • 2、此外,它們也代表了出錯的機會。迭代器在每個循環中出現三次,索引變量出現四次,這使你有很多機會使用錯誤的變量。如果這樣做,就不能保證編譯器會發現到問題。
  • 3、最後,這兩個循環非常不同,引起了對容器類型的不必要注意,並且增加了更改該類型的小麻煩。

for 循環示例(代碼冗餘&易錯性):

<code>// 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}/<element>/<code>

for-each 循環的優勢:

  • 1、for-each 循環(官方稱為「增強的 for 語句」)解決了所有這些問題。
  • 2、它通過隱藏迭代器或索引變量來消除混亂和出錯的機會。
  • 3、由此產生的習慣用法同樣適用於集合和數組,從而簡化了將容器的實現類型從一種轉換為另一種的過程:

for-each 循環示例(for循環的優化方案):

<code>// The preferred idiom for iterating over collections and arraysfor (Element e : elements) {    ... // Do something with e}/<code>

多層嵌套&遍歷循環:

當涉及到嵌套迭代時,for-each 循環相對於傳統 for 循環的優勢甚至更大。下面是人們在進行嵌套迭代時經常犯的一個錯誤:

<code>// Can you spot the bug?enum Suit { CLUB, DIAMOND, HEART, SPADE }enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,            NINE, TEN, JACK, QUEEN, KING }...static Collection<suit> suits = Arrays.asList(Suit.values());static Collection<rank> ranks = Arrays.asList(Rank.values());List<card> deck = new ArrayList<>();for (Iterator<suit> i = suits.iterator(); i.hasNext(); )    for (Iterator<rank> j = ranks.iterator(); j.hasNext(); )        deck.add(new Card(i.next(), j.next()));/<rank>/<suit>/<card>/<rank>/<suit>/<code>

for 循環 編碼錯誤原因說明(不易發現):

  • 1、如果沒有發現這個 bug,也不必感到難過。許多專業程序員都曾犯過這樣或那樣的錯誤。
  • 2、問題是,對於外部集合(suit),next 方法在迭代器上調用了太多次。它應該從外部循環調用,因此每花色調用一次,但它是從內部循環調用的,因此每一張牌調用一次。在 suit 用完之後,循環拋出 NoSuchElementException 異常。

如果使用嵌套 for-each 循環,問題就會消失。生成的代碼也儘可能地簡潔:

<code>// Preferred idiom for nested iteration on collections and arraysfor (Suit suit : suits)    for (Rank rank : ranks)        deck.add(new Card(suit, rank));/<code>

不能使用 for-each 循環的三種常見情況:

  • 有損過濾(Destructive filtering)——如果需要遍歷集合,並刪除指定選元素,則需要使用顯式迭代器,以便可以調用其 remove 方法。 通常可以使用在 Java 8 中添加的 Collection 類中的 removeIf 方法,來避免顯式遍歷。
  • 轉換——如果需要遍歷一個列表或數組並替換其元素的部分或全部值,那麼需要列表迭代器或數組索引來替換元素的值。
  • 並行迭代——如果需要並行地遍歷多個集合,那麼需要顯式地控制迭代器或索引變量,以便所有迭代器或索引變量都可以同步進行 (正如上面錯誤的 card 和 dice 示例中無意中演示的那樣)。

Iterable 迭代器(內嵌支持for-each 循環):

for-each 循環不僅允許遍歷集合和數組,還允許遍歷實現 Iterable 接口的任何對象,該接口由單個方法組成。接口定義如下:

<code>public interface Iterable {    // Returns an iterator over the elements in this iterable    Iterator iterator();}/<code>

自定義容器和Iterable 接口:

  • 1、如果必須從頭開始編寫自己的 Iterator 實現,那麼實現 Iterable 會有點棘手,但是如果你正在編寫表示一組元素的類型,那麼你應該強烈考慮讓它實現 Iterable 接口,甚至可以選擇不讓它實現 Collection 接口。
  • 2、這允許用戶使用 for-each 循環遍歷類型,他們會永遠感激不盡的。

總結:

  • 1、for-each 循環在清晰度,靈活性和錯誤預防方面提供了超越傳統 for 循環的令人注目的優勢,而且沒有性能損失。
  • 2、儘可能使用 for-each 循環優先於 for 循環。


59. 瞭解並使用庫

使用標準庫的優勢:

  • 1、你可以利用編寫它的專家的知識和以前使用它的人的經驗。
  • 2、你不必浪費時間為那些與你的工作無關的問題編寫專門的解決方案。
  • 3、隨著時間的推移,它們的性能會不斷提高,而你無需付出任何努力。
  • 4、隨著時間的推移,它們往往會獲得新功能。如果一個庫丟失了一些東西,開發人員社區會將其公佈於眾,並且丟失的功能可能會在後續版本中添加。
  • 5、可以將代碼放在主幹中。這樣的代碼更容易被開發人員閱讀、維護和重用。

示例:庫方法 Random.nextInt(int) 的生命週期:

  • 1、你不必關心它如何工作的(儘管如果你感興趣,可以研究文檔或源代碼)。
  • 2、一位具有算法背景的高級工程師花了大量時間設計、實現和測試這種方法,然後將其展示給該領域的幾位專家,以確保它是正確的。
  • 3、然後,這個庫經過 beta 測試、發佈,並被數百萬程序員廣泛使用了近 20 年。該方法還沒有發現任何缺陷,但是如果發現了缺陷,將在下一個版本中進行修復。
  • 4、從 Java 7 開始,就不應該再使用 Random。在大多數情況下,選擇的隨機數生成器現在是 ThreadLocalRandom。 它能產生更高質量的隨機數,而且速度非常快。

程序員不能正確使用類庫的原因:

  • 1、也許他們不知道庫的存在。在每個主要版本中,都會向庫中添加許多特性,瞭解這些新增特性是值得的。
  • 2、每次發佈 Java 平臺的主要版本時,都會發佈一個描述其新特性的 web 頁面。這些頁面非常值得一讀 [Java8-feat, Java9-feat]。
  • 3、這些標準類庫太龐大了,以致於不可能學完所有的文檔 [Java9-api],但是 每個程序員都應該熟悉 java.lang、java.util 和 java.io 的基礎知識及其子包。 其他庫的知識可以根據需要獲得。
  • 4、其中有幾個庫值得一提。Collections 框架和 Streams 庫(詳見第 45 到 48 條)應該是每個程序員的基本工具包的一部分,java.util.concurrent 中的併發實用程序也應該是其中的一部分。這個包既包含高級的併發工具來簡化多線程的編程任務,還包含低級別的併發基本類型,允許專家們自己編寫更高級的併發抽象。java.util.concurrent 的高級部分,在第 80 條和第 81 條中討論。

選擇使用第三庫(次優方案):

  • 1、有時,類庫工具可能無法滿足你的需求。你的需求越特殊,發生這種情況的可能性就越大。
  • 2、雖然你的第一個思路應該是使用這些庫,但是如果你已經瞭解了它們在某些領域提供的功能,而這些功能不能滿足你的需求,那麼可以使用另一種實現。
  • 3、任何有限的庫集所提供的功能總是存在漏洞。如果你在 Java 平臺庫中找不到你需要的東西,你的下一個選擇應該是尋找高質量的第三方庫,比如谷歌的優秀的開源 Guava 庫 [Guava]。
  • 4、如果你無法在任何適當的庫中找到所需的功能,你可能別無選擇,只能自己實現它。

總結:

  • 1、不要白費力氣重新發明輪子。
  • 2、如果你需要做一些看起來相當常見的事情,那麼庫中可能已經有一個工具可以做你想做的事情。如果有,使用它;如果你不知道,檢查一下。
  • 3、一般來說,庫代碼可能比你自己編寫的代碼更好,並且隨著時間的推移可能會得到改進。這並不反映你作為一個程序員的能力。
  • 4、規模經濟決定了庫代碼得到的關注要遠遠超過大多數開發人員所能承擔的相同功能。、


60. 若需要精確答案就應避免使用 float 和 double 類型

float 和 double 的用途:

  • 1、float 和 double 類型主要用於科學計算和工程計算。
  • 2、它們執行二進制浮點運算,該算法經過精心設計,能夠在很大範圍內快速提供精確的近似值。
  • 3、但是,它們不能提供準確的結果,也不應該在需要精確結果的地方使用。float 和 double 類型特別不適合進行貨幣計算,因為不可能將 0.1(或 10 的任意負次冪)精確地表示為 float 或 double。

錯誤代碼示例:

System.out.println(1.03 - 0.42);

輸出: 0.6100000000000001 (非預期result)

System.out.println(1.00 - 9 * 0.10);

輸出:0.0999999999999999998 美元(非預期result)

問題解決方案:

解決這個問題的正確方法是 使用 BigDecimal、int 或 long 進行貨幣計算。


但是,使用 BigDecimal 有兩個缺點:

  • 1、它與原始算術類型相比很不方便,而且速度要慢得多。
  • 2、如果你只解決一個簡單的問題,後一種缺點是無關緊要的,但前者可能會讓你煩惱。

總結:

  • 1、對於任何需要精確答案的計算,不要使用 float 或 double 類型。
  • 2、如果希望系統來處理十進制小數點,並且不介意不使用基本類型帶來的不便和成本,請使用 BigDecimal。
  • 3、使用 BigDecimal 的另一個好處是,它可以完全控制舍入,當執行需要舍入的操作時,可以從八種舍入模式中進行選擇。如果你使用合法的舍入行為執行業務計算,這將非常方便。
  • 4、如果性能是最重要的,那麼你不介意自己處理十進制小數點,而且數值不是太大,可以使用 int 或 long。如果數值不超過 9 位小數,可以使用 int;如果不超過 18 位,可以使用 long。如果數量可能超過 18 位,則使用 BigDecimal。


61. 基本數據類型優於包裝類

Java 有一個由兩部分組成的類型系統,包括:

  • 1、基本類型(如 int、double 和 boolean)
  • 2、引用類型(如 String 和 List)。
  • 3、每個基本類型都有一個對應的引用類型,稱為包裝類型。與 int、double 和 boolean 對應的包裝類是 Integer、Double 和 Boolean。

基本類型和包裝類型之間有三個主要區別:

  • 1、首先,基本類型只有它們的值,而包裝類型具有與其值不同的標識。換句話說,兩個包裝類型實例可以具有相同的值和不同的標識。
  • 2、第二,基本類型只有全功能值,而每個包裝類型除了對應的基本類型的所有功能值外,還有一個非功能值,即 null。
  • 3、最後,基本類型比包裝類型更節省時間和空間。如果你不小心的話,這三種差異都會給你帶來真正的麻煩。
<code>// Broken comparator - can you spot the flaw?Comparator<integer> naturalOrder =(i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);/<integer>/<code>

問題說明:將 == 操作符應用於包裝類型幾乎都是錯誤的。

解決方案(包裝類型->基本類型,藉由可以使用:值對比==):

Comparator<integer> naturalOrder = (iBoxed, jBoxed) -> { int i = iBoxed, j = jBoxed; // Auto-unboxing return i < j ? -1 : (i == j ? 0 : 1); };/<integer>


什麼時候應該使用包裝類型呢?它們有幾個合法的用途。

  • 1、第一個是作為集合中的元素、鍵和值。不能將基本類型放在集合中,因此必須使用包裝類型。這是一般情況下的特例。
  • 2、在參數化類型和方法(Chapter 5)中,必須使用包裝類型作為類型參數,因為 Java 不允許使用基本類型。例如,不能將變量聲明為 ThreadLocal 類型,因此必須使用 ThreadLocal<integer>。/<integer>
  • 3、最後,在進行反射方法調用時,必須使用包裝類型(詳見第 65 條)。

總結:

  • 1、只要有選擇,就應該優先使用基本類型,而不是包裝類型。基本類型更簡單、更快。
  • 2、如果必須使用包裝類型,請小心!自動裝箱減少了使用包裝類型的冗長,但沒有減少危險。
  • 3、當你的程序使用 == 操作符比較兩個包裝類型時,它會執行標識比較,這幾乎肯定不是你想要的。
  • 4、當你的程序執行包含包裝類型和基本類型的混合類型計算時,它將進行拆箱,當你的程序執行拆箱時,將拋出 NullPointerException。
  • 5、最後,當你的程序將基本類型裝箱時,可能會導致代價高昂且不必要的對象創建。


62. 當使用其他類型更合適時應避免使用字符串

字符串的場景誤用:

  • 1、字符串被設計用來表示文本,它們在這方面做得很好。
  • 2、因為字符串是如此常見,並且受到 Java 的良好支持,所以很自然地會將字符串用於其他目的,而不是它們適用的場景。

a. 字符串是其他值類型的糟糕替代品。

當一段數據從文件、網絡或鍵盤輸入到程序時,它通常是字符串形式的。有一種很自然的傾向是保持這種格式不變,但是這種傾向只有在數據本質上是文本的情況下才合理。如果是數值類型,則應將其轉換為適當的數值類型,如 int、float 或 BigInteger。

b. 字符串是枚舉類型的糟糕替代品。

正如條目 34 中所討論的,枚舉類型常量比字符串更適合於枚舉類型常量。

c. 字符串是聚合類型的糟糕替代品。

如果一個實體有多個組件,將其表示為單個字符串通常是一個壞主意。例如,下面這行代碼來自一個真實的系統標識符,它的名稱已經被更改,以免引發罪責:

<code>// Inappropriate use of string as aggregate typeString compoundKey = className + "#" + i.next();/<code>

缺點說明:

  • 1、如果用於分隔字段的字符出現在其中一個字段中,可能會導致混亂。
  • 2、要訪問各個字段,你必須解析字符串,這是緩慢的、冗長的、容易出錯的過程。
  • 3、你不能提供 equals、toString 或 compareTo 方法,但必須接受 String 提供的行為。
  • 4、更好的方法是編寫一個類來表示聚合,通常是一個私有靜態成員類(詳見第 24 條)

d. 字符串不能很好地替代 capabilities。

例如,考慮線程本地變量機制的設計。這樣的機制提供了每個線程都有自己的變量值。自 1.2 版以來,Java 庫就有了一個線程本地變量機制,但在此之前,程序員必須自己設計。

許多年前,當面臨設計這樣一個機制的任務時,有人提出了相同的設計,其中客戶端提供的字符串鍵,用於標識每個線程本地變量。

<code>// Broken - inappropriate use of string as capability!public class ThreadLocal {    private ThreadLocal() { } // Noninstantiable    // Sets the current thread's value for the named variable.    public static void set(String key, Object value);    // Returns the current thread's value for the named variable.    public static Object get(String key);}/<code>

String實現本地變量的缺陷:

  • 1、這種方法的問題在於,字符串鍵表示線程本地變量的共享全局名稱空間。為了使這種方法有效,客戶端提供的字符串鍵必須是惟一的:
  • 2、如果兩個客戶端各自決定為它們的線程本地變量使用相同的名稱,它們無意中就會共享一個變量,這通常會導致兩個客戶端都失敗。
  • 3、而且,安全性很差。惡意客戶端可以故意使用與另一個客戶端相同的字符串密鑰來非法訪問另一個客戶端的數據。

ThreadLocal 作為一個參數化的類來實現這個 API 的類型安全很簡單(詳見第 29 條):

<code>public final class ThreadLocal {    public ThreadLocal();    public void set(T value);    public T get();}/<code>

總結:

  • 1、當存在或可以編寫更好的數據類型時,應避免將字符串用來表示對象。
  • 2、如果使用不當,字符串比其他類型更麻煩、靈活性更差、速度更慢、更容易出錯。
  • 3、字符串經常被誤用的類型包括基本類型、枚舉和聚合類型。


63. 當心字符串連接引起的性能問題

String +的性能問題:

  • 1、字符串連接操作符 (+) 是將幾個字符串組合成一個字符串的簡便方法。
  • 2、對於生成單行輸出或構造一個小的、固定大小的對象的字符串表示形式,它是可以的,但是它不能伸縮。
  • 3、使用字符串 串聯運算符重複串聯 n 個字符串需要 n 的平方級時間。
  • 4、這是字符串不可變這一事實導致的結果(詳見第 17 條)。當連接兩個字符串時,將複製這兩個字符串的內容。

錯誤使用的示例:

<code>// Inappropriate use of string concatenation - Performs poorly!public String statement() {    String result = "";    for (int i = 0; i < numItems(); i++)        result += lineForItem(i); // String concatenation    return result;}/<code>

正確方法(String+ -> StringBuilder.append())

要獲得能接受的性能,請使用 StringBuilder 代替 String 來存儲正在構建的語句:

<code>public String statement() {    StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);    for (int i = 0; i < numItems(); i++)        b.append(lineForItem(i));    return b.toString();}/<code>

總結:

  • 1、不要使用字符串連接操作符合並多個字符串,除非性能無關緊要。否則使用 StringBuilder 的 append 方法。
  • 2、或者,使用字符數組,再或者一次只處理一個字符串,而不是組合它們。
《Effective Java 3rd》3分鐘速成:(57-63)General Programming1


分享到:


相關文章: