天天看點

Effective Java 第三版——57. 最小化局部變量的作用域

Tips

書中的源代碼位址:https://github.com/jbloch/effective-java-3e-source-code

注意,書中的有些代碼裡方法是基于Java 9 API中的,是以JDK 最好下載下傳 JDK 9以上的版本。

9. 通用程式設計

這一章專門讨論Java語言的具體細節。讨論了局部變量、控制結構、類庫、資料類型以及兩種Java語言之外工具:反射和本地方法。最後,讨論了優化和命名慣例。

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

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

較早的程式設計語言(如C)要求必須在代碼塊的頭部聲明局部變量,并且一些程式員繼續習慣這樣做。 這是一個值得改進的習慣。 作為提醒,Java允許你在任何合法的語句的地方聲明變量(as does C, since C99)。

用于最小化局部變量作用域的最強大的技術是再首次使用的地方聲明它。 如果變量在使用之前被聲明,那就變得更加混亂—— 這也會對試圖了解程式的讀者來講,又增加了一件分散他們注意力的事情。 到使用該變量時,讀者可能不記得變量的類型或初始值。

過早地聲明局部變量可能導緻其作用域不僅過早開始而且結束太晚。 局部變量的作用域從聲明它的位置延伸到封閉塊的末尾。 如果變量在使用它的封閉塊之外聲明,則在程式退出該封閉塊後它仍然可見。如果在其預定用途區域之前或之後意外使用變量,則後果可能是災難性的。

幾乎每個局部變量聲明都應該包含一個初始化器。如果還沒有足夠的資訊來合理地初始化一個變量,那麼應該推遲聲明,直到認為可以這樣做。這個規則的一個例外是try-catch語句。如果一個變量被初始化為一個表達式,該表達式的計算結果可以抛出一個已檢查的異常,那麼該變量必須在try塊中初始化(除非所包含的方法可以傳播異常)。如果該值必須在try塊之外使用,那麼它必須在try塊之前聲明,此時它還不能被“合理地初始化”。例如,參照條目 65中的示例。

循環提供了一個特殊的機會來最小化變量的作用域。傳統形式的for循環和for-each形式都允許聲明循環變量,将其作用域限制在需要它們的确切區域。 (該區域由循環體和for關鍵字與正文之間的括号中的代碼組成)。是以,如果循環終止後不需要循環變量的内容,那麼優先選擇for循環而不是while循環。

例如,下面是周遊集合的首選方式(條目 58):

// Preferred idiom for iterating over a collection or array
for (Element e : c) {
    ... // Do Something with e
}
           

如果需要通路疊代器,也許是為了調用它的remove方法,首選的習慣用法,使用傳統的for循環代替for-each循環:

// Idiom for iterating when you need the iterator
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
    Element e = i.next();
    ... // Do something with e and i
}
           

要了解為什麼這些for循環優于while循環,請考慮以下代碼片段,其中包含兩個while循環和一個bug:

Iterator<Element> i = c.iterator();
while (i.hasNext()) {
    doSomething(i.next());
}
...
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) {             // BUG!
    doSomethingElse(i2.next());
}
           

第二個循環包含一個複制粘貼錯誤:它初始化一個新的循環變量i2,但是使用舊的變量i,不幸的是,它仍在範圍内。 生成的代碼編譯時沒有錯誤,并且在不抛出異常的情況下運作,但它做錯了。 第二個循環不是在c2上疊代,而是立即終止,給出了c2為空的錯誤印象。 由于程式無聲地出錯,是以錯誤可能會長時間無法被檢測到。

如果将類似的複制粘貼錯誤與for循環(for-each循環或傳統循環)結合使用,則生成的代碼甚至無法編譯。第一個循環中的元素(或疊代器)變量不在第二個循環中的作用域中。下面是它與傳統for循環的示例:

for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
    Element e = i.next();
    ... // Do something with e and i
}
...

// Compile-time error - cannot find symbol i
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
    Element e2 = i2.next();
    ... // Do something with e2 and i2
}
           

此外,如果使用for循環,那麼發送這種複制粘貼錯誤的可能性要小得多,因為沒有必要在兩個循環中使用不同的變量名。 循環是完全獨立的,是以重用元素(或疊代器)變量名稱沒有壞處。 事實上,這樣做通常很流行。

for循環比while循環還有一個優點:它更短,增強了可讀性。

下面是另一種循環習慣用法,它最小化了局部變量的作用域:

for (int i = 0, n = expensiveComputation(); i < n; i++) {
    ... // Do something with i;
}
           

關于這個做法需要注意的重要一點是,它有兩個循環變量,i和n,它們都具有完全相同的作用域。第二個變量n用于存儲第一個變量的限定值,進而避免了每次疊代中備援計算的代價。作為一個規則,如果循環測試涉及一個方法調用,并且保證在每次疊代中傳回相同的結果,那麼應該使用這種用法。

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