Effective Java(七)

七、通用程序設計

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

        同“使類和成員的可訪問性最小化”一樣,將局部變量的作用域最小化,可以增強代碼的可讀性和可維護性,並降低出錯的可能性。
        Java允許在任何可以出現語句的地方聲明變量,所以,最好的方法就是在第一次使用它的地方聲明並初始化

        循環中提供了特殊的機會來將變量的作用域最小化,它們的作用域被限定在正好需要的範圍之內。如果在循環終止之後不再需要循環變量的內容,for循環就優先於while循環。

for (Element e : c) {
    doSomething(e);
}

for (Iterator i = c.iterator(); i.hasNext(); ) {
    doSomething(i.next());
}

//對比之下,for/for-each循環要優於while循環
Iterator<Element> i = c.iterator();
while(i.hasNext()) {
    doSomething(i.next());
}

        另外,與while循環相比,for循環更簡短,可讀性更強。

        還有種“使局部變量作用域最小化”的方法是:使方法小而集中。方法小而功能集中,就可以減少變量的個數,方法中的局部變量僅和當前功能有關。

2. for-each循環優先於傳統的for循環

        同傳統的for循環相比,for-each循環可以隱藏迭代器或者索引變量,避免了混亂和出錯的可能。

for (Iterator i = c.iterator(); i.hasNext(); ) {
    doSomething((Element) i.next);
}

for (int i = 0; i < a.length; i++) {
    doSomething(a[i]);
}

//隱藏了迭代器or索引變量
for (Element e : elements) {
    doSomething(e);
}

        在對多個集合進行嵌套式迭代時,for-each循環相對於傳統for循環的這種優勢更加明顯。 

        有三種常見的情況無法使用for-each循環:

(1)過濾——如果需要遍歷集合,並刪除選中的元素,就需要使用顯式的迭代器,以便可以調用它的remove方法。

(2)轉換——如果需要遍歷列表或者數組,並取代它部分或者全部的元素值,就需要列表迭代器或者數組索引,以便設定元素的值。

(3)平行迭代——如果需要平行的遍歷多個集合,就需要顯式地控制迭代器或者索引變量,以便所有迭代器或者索引變量都可以得到同步前移。

3. 瞭解和使用類庫

使用標準庫(類庫)的好處有:

  • 使用標準類庫,可以充分利用這些編寫標準庫的專家的知識,以及其他人的使用經驗。
  • 不必浪費時間爲那些與工作不太相關的問題提供特別的解決方案,把時間花在應用程序上,而不是底層的細節上。
  • 標準庫的性能往往會隨着時間的推移而不斷提高。
  • 可以使自己的代碼融入主流,更易讀、易維護、更容易被其他開發人員重用。

4. 如果需要精確的答案,請避免使用float和double

        floatdouble類型主要是爲了科學計算工程計算而設計的。它們執行二進制浮點計算,是爲了在廣泛的數值範圍上提供較爲精確的快速近似計算而精心設計的,它們並沒有提供完全精確的結果,不應該被用於需要精確結果的場合。尤其不適合用於貨幣計算。應使用BigDecimal(高精度小數)、int 後者 long 進行貨幣計算。

5. 基本類型優先於裝箱基本類型

        每個基本類型都有一個對應的引用類型,稱作裝箱基本類型。Java1.5增加了自動裝箱自動拆箱。這些特性模糊了基本類型和裝箱基本類型的區別,但它們還是有差別的。

基本類型和裝箱基本類型之間有三個主要的區別
(1)基本類型只有值,裝箱基本類型則具有與它們的值不同的同一性(兩個裝箱基本類型可以具有相同的值和不同的同一性)。
(2)基本類型只有功能完備的值,而每個裝箱基本類型除了它對應基本類型的所有功能值之外,還有個非功能值:null。
(3)基本類型通常比裝箱基本類型更節省時間和空間。

public class Unbelievable {
    static Integer i;

    public static void main(String[] args) {
        if (i == 42) {
            System.out.println("Unbelievable");
        }
    }
}

        上述程序在計算i == 42的時候將拋出NullPointerException異常。因爲i被聲明爲基本裝箱類型,它的初始值默認是null,在計算i == 42時將執行自動拆箱,null引用被自動拆箱就會拋出NullPointerException異常。

        當在一項操作中混合使用基本類型和裝箱基本類型時,裝箱基本類型就會自動拆箱

    public static void main(String[] args) {
        Long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
        System.out.println(sum);
    }

        上述代碼運行起來要比預計的慢很多,因爲它將sum聲明爲裝箱基本類型Long,而不是基本類型long。程序雖可正常執行,但變量被反覆地裝箱和拆箱,導致明顯的性能下降。 

        裝箱基本類型的用途有

  • 作爲集合中的元素、鍵、值
  • 在參數化類型中,必須使用裝箱基本類型作爲類型參數
  • 在進行反射的方法調用時,必須使用裝箱基本類型

6. 如果其他類型更合適,則儘量避免使用字符串

不應該使用字符串的情形有:
(1)字符串不適合代替其他的值類型
        當一段數據從文件、網絡、鍵盤設備進入到程序之後,它通常以字符串的形式存在,自然的傾向是讓它繼續保留這種形式,但這是很不恰當的。它應該轉化爲同應用情景更匹配的類型。如果它是數值,就應該被轉換成適當的數值類型,比如int、float或者BigInteger類型。如果它是一個“是-或-否”這種問題的答案,就應該轉換爲boolean類型。
(2) 字符串不適合代替枚舉類型
        枚舉類型比字符串更加適合用來表示枚舉類型的常量。
(3)字符串不適合代替聚集類型
        如果一個實體有多個組件,用一個字符串來表示這個實體通常是很不恰當的。

String compoundKey = className + "#" + i.next();

        這種方法有很多缺點。如果用來分割域的字符也出現在某個域中,結果就會出現混亂。爲了訪問單獨的域,必須解析該字符串,這個過程非常慢,也很繁瑣,還容易出錯。更好的做法是,簡單地編寫一個類來描述這個數據集,通常是一個私有的靜態成員類。
(4)字符串不適合代替能力表(capabilities)

        有時候,字符串被用作某種功能進行授權訪問。例如,考慮設計一個提供線程局部變量的機制。這個機制提供的變量在每個線程中都有自己的值。

public class ThreadLocal {
    private ThreadLocal() { }
    public static void set(String key, Object value);
    public static Object get(String key);
}

        這種方法的問題在於,這些字符串鍵代表了一個共享的全局命名空間。要使這種方法可行,客戶端提供的字符串鍵必須是唯一的。如果兩個客戶端各自決定爲他們的線程局部變量使用同樣的名稱,它們實際上就無意中共享了這個變量,這樣往往會導致兩個客戶端都失敗。而且安全性也很差。惡意的客戶端可能有意地使用與另一個客戶端相同的鍵,以便非法地訪問其他客戶端的數據。

        要解決這個問題,只要用一個不可僞造的鍵(有時被稱爲能力)來代替字符串即可。 

public class ThreadLocal {
    private ThreadLocal() { }
    public static void set(Key key, Object value);
    public static Object get(Key key);
    
    public static Key getKey() {
        return new Key();
    }

    public static class Key {
        Key() { }
    }
}

        雖然這種方法解決了問題,但仍然可以做得更好。實際上這裏不再需要靜態方法了,它們可以被取代爲以鍵(Key)的實例方法,這樣這個鍵不再是鍵,而是線程局部變量了。

public final class ThreadLocal {
    public ThreadLocal() { }
    public void set(Object value);
    public Object get();
} 

         當然它不是線程安全的,因爲當從線程局部變量得到它時,必須將Object轉換成它實際的值。改進的方法是將ThreadLocal類泛型化。最終這個ThreadLocal類正是java.util.ThreadLocal提供的API,與之前的基於鍵的API相比,它更快速、更優雅。

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

7. 當心字符串連接的性能

        字符串連接操作符(+)是把多個字符串合併爲一個字符串的便利途徑。要產生單獨一行的輸出,或構造一個字符串來表示一個較小的、大小固定的對象,使用連接操作符是非常適合的。字符串連接符不適合運用於大規模的場景中,因爲字符串是不可變的,當兩個字符串被連接在一起時,它們的內容都要被拷貝,會對性能帶來較大的影響。

        爲了獲得可以接受的性能,需使用StringBuilder替代String。

public String statement() {
    String result = "";
    for (int i = 0; i < numItems(); i++) {
        result += lineForItem(i);
    }
    return result;
}

//使用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();
}

8. 通過接口引用對象

        優先使用接口而不是類來引用對象。
        如果有合適的接口類型存在,對於參數、返回值、變量、域,就都應該使用接口類型進行聲明。只有當利用構造器創建某個對象的時候,才真正需要引用這個對象的類

List<Subscriber> subscribers = new Vector<Subscriber>();

//用類作爲類型進行聲明(不建議)
Vector<Subscriber> subscribers = new Vector<Subscriber>();

        面向接口編程是面向對象編程的一個很重要的設計原則,它使得程序更加靈活,耦合性更低。

        不適用接口引用對象的三種情形爲
(1)沒有合適的接口存在,如值對象String、BigInteger。
(2)對象屬於一個框架,而框架的基本類型是類,而不是接口。這時候就應該用相關的基類來引用對象,而不是實現類。如java.util.TimerTask。
(3)類實現了接口,但是它提供了接口中不存在的額外方法,如果程序依賴於這些額外的方法,這種類就應該只被用來引用它的實例。如LinkedHashMap。

9. 接口優先於反射機制

        核心反射機制java.lang.reflect提供了通過程序來訪問已裝載類的信息的能力。給定一個Class實例,你可以獲得ConstructorMethodField實例,分別代表了該Class類所表示的類的構造器、方法、域。這些對象提供了通過程序來訪問類的成員名稱、域類型、方法簽名等信息的能力

        Constructor、Method和Field實例使你能夠通過反射機制操作它們的底層對等體:通過調用Constructor、Method、Field實例上的方法,可以構造底層類的實例、調用底層類的方法,訪問底層類中的域。

        反射功能很強大,但也會帶來一些負面的影響:

  • 喪失了編譯時類型檢查的好處
  • 執行反射訪問所需要的代碼非常笨拙和冗長
  • 性能損失

        如果只是以非常有限的形式使用反射機制,雖然也要付出少許代價,但是可以獲得許多好處。對於有些程序,它們必須用到在編譯時無法獲取的類,但是在編譯時存在適當的接口或者超類,通過它們可以引用這個類。若是這種情況,就可以以反射方式創建實例,然後通過它們的接口或者超類,以正常的方式訪問這些實例

public class InterfaceReference {
    
    public static void main(String[] args) {
        Class<?> cl = null;
        
        try {
            cl = Class.forName("java.util.HashSet");
        } catch (ClassNotFoundException e) {
            System.out.println("class not found");
            System.exit(1);
        }
        
        Set<String> set = null;
        try {
            set = (Set<String>) cl.newInstance();
        } catch (InstantiationException e) {
            System.out.println("class not instantiable.");
            System.exit(1);
        } catch (IllegalAccessException e) {
            System.out.println("class not accessible");
            System.exit(1);
        }
        //以接口的方式訪問實例
        set.addAll(Arrays.asList("Java", "Kotlin", "Python"));
        System.out.println(set);
    }
}

        反射機制是一種功能強大的機制,對於特定的複雜系統編程任務,它是非常必要的,但也有一些缺點。如果你編寫的程序必須要與編譯時未知的類一起工作,如有可能,就應該僅僅使用反射機制來實例化對象,而訪問對象時則使用編譯時已知的某個接口或超類

10. 謹慎地使用本地方法

        Java Native Interface(JNI)允許Java程序可以調用本地方法。所謂本地方法是指用本地程序設計語言(C或者C++)來編寫的特殊方法。

        本地方法主要有三種用途
(1)提供了“訪問特定平臺”的能力,比如訪問註冊表和文件鎖。
(2)提供了訪問遺留代碼庫的能力,從而可以訪問遺留數據。
(3)通過本地語言,編寫應用程序中注重性能的部分,從而提高系統的性能。

        使用本地方法來訪問特定平臺的機制是合法的;使用本地方法來訪問遺留代碼也是合法的;使用本地方法來提高性能的做法不值得提倡,因爲JVM的優化功能已經做的很好了。

        使用本地方法有一些嚴重的缺點:

  • 本地語言是不安全的,使用本地方法有內存毀壞的風險。
  • 本地語言是與平臺相關的,程序不能再自由移植。
  • 使用本地方法的程序更難調試。
  • 需要“膠合代碼”的本地方法編寫起來單調乏味且難以閱讀。

11. 謹慎地進行優化

        優化的弊大於利,特別是不成熟的優化。在優化的過程中,產生的軟件可能既不快速,也不正確,而且還不容易修正。

        不能因爲性能而犧牲合理的結構。要努力編寫好的程序而不是快的程序。如果好的程序不夠快,它的結構使它可以得到優化。好的程序體現了信息隱藏的原則:只要有可能,就把設計決策集中在單個模塊中,可以改變單個決策,而不會影響到系統的其他部分。

        在設計過程中必須要考慮性能問題,因爲實現上的問題可以通過後期的優化而得到修正,但遍佈全局且限制性能的結構缺陷幾乎是不可能被改正的。

        設計的過程中要努力避免那些限制性能的設計決策,還要考慮API設計決策的性能後果

12. 遵守普遍接受的命名慣例

        通常,命名慣例分爲兩大類:字面的語法的

字面命名慣例:

名稱應該是層次狀的,用“.”分割每個部分,每個部分包括小寫字母。

類和接口的名稱(包括枚舉和註解)的名稱,都應該包括一個或多個單詞,每個單詞首字母大寫,如Timer、TimerTask,應該避免使用縮寫,除非是一些首字母縮寫和一些通用的縮寫,如max和min。

方法和域的名稱與類和接口的名稱一樣,都遵守相同的字面慣例,只不過方法和域的名稱的第一個字母應該小寫如remove、ensureCapacity。

局部變量名稱的字面慣例與成員名稱類似,只不過它允許縮寫,單個字符和短字符序列的意義取決於局部變量所在的上下文環境。如i、xref、houseNumber。

類型參數名稱通常由單個字母組成,這個字母通常是以下五種類型之一:

  • T表示任意的類型
  • E表示集合的元素類型
  • K和V表示映射的鍵和值類型
  • X表示異常
  • 任何類型序列可以是T、U、V或者T1、T2、T3

語法命名慣例:

沒有語法命名慣例。
通常用一個名詞或者名詞短語命名,如Timer、BufferedWriter、ChessPiece。
接口的命名與類相似,如Collection、Comparator、或者用一個以-able-ible結尾的形容詞來命名,如Runnable、Iterable、Accessible。
執行某個動作的方法通常用動詞或者動詞短語來命名,如append、drawImage。
返回boolean值的方法通常以is開頭,後面跟名詞或名詞短語,或者任何具有形容詞功能的單詞或短語,如isDigit、isEmpty、isEnabled。
返回一個非boolean值的方法通常用名詞、名詞短語、或者get開頭的動詞短語來命名,如size、hashCode、getTime。如果方法所在的類是個Bean,就要強制使用以get開頭的形式。
有些方法名稱值得專門提及:

  • 轉換對象類型的方法、返回不同類型的獨立對象的方法,通常被稱爲toType,如toString、toArray。
  • 返回視圖的方法通常被稱爲asType,如asList。
  • 返回一個與被調用對象同值的基本類型方法,通常被稱爲typeValue,如intValue。
  • 靜態工廠常命名爲valueOf、of、getInstance、newInstance、getType、newType

        應該把標準的命名慣例當作一種內在的機制來看待,並且學着用它們作爲第二特性。
        命名是極爲重要的,好的命名本身具有自注釋的功能,可以大大的提高代碼的可讀性。很多經典書籍中都有介紹對程序元素命名的技巧,可以參考學習。
        這類書籍有《編寫可讀代碼的藝術》 、《代碼整潔之道》、《實現模式》、《重構》等。

 

發佈了66 篇原創文章 · 獲贊 66 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章