《Effective Java》看這一篇就夠了

前言

最近讀完了《Effective Java》這本書,筆者這裏對一些比較重點的準則作個總結。數組

避免建立沒必要要的對象

String s = new String("wugui");

上面這句每次執行都會建立一個新的String實例,改進後的版本以下:性能優化

String s = "wugui";

改進後只用了一個String實例,而不是每次執行的時候都建立一個新的實例。編輯器

還有一個很重要的點:要優先使用基本類型而不是裝箱基本類型,要小心無心識的自動裝箱ide

上面這句話是什麼意思呢,看下面這個例子就知道了:性能

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,每次循環i都會進行自動裝箱升級爲Long類型,意味着程序構造了大約2的31次冪個多餘的Long實例。測試

消除過時的對象引用

所謂的過時引用,是指永遠也不會被再被解除的引用。 優化

舉個例子:從棧中彈出來的對象不會被當作垃圾回收,即便使用棧的程序再也不引用這些對象,它們也不會被回收。 棧內部維護着對這些對象的過時引用。ui

所以,一旦對象引用已通過期,只需清空這些引用便可this

element[size]=null;

使類和成員的可訪問性最小化

儘量地使每一個類或者成員不被外界訪問設計

除了public static final變量的特殊情形以外,任何類都不該該包含public變量,而且要確保public static final變量所引用的對象都是不可變的(好比String)

public static final變量要麼指向基本類型,要麼指向不可變對象。由於雖然引用自己不能修改,可是它引用的對象卻能夠被修改

許多編輯器會返回指向私有數組域的訪問方法,能夠用下面方法解決:

private static final Employee[] PRIVATE_VALUES = {......}
public static final List<Employee> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

覆蓋equals時請遵照通用約定

1.equals方法實現了等價關係:
(1)自反性
對於任何非null的引用值x,x.equals(x)必須返回true
(2)對稱性
對於任何非null的引用值x,當x.equals(y)返回true時,y.equals(x)也必須返回true
(3)傳遞性
對於任何非null的引用值x,若是x.equals(y)返回truey.equals(z)返回true,那麼x.equals(z)也應該返回true
(4)一致性
對於任何非null的引用值x,只要對象沒有修改,那麼x.equals(y)會一直返回true
(5)非空性
對於任何非null的引用值x,x.equals(null)必須返回false

  1. 實現高質量equals方法的訣竅:

(1)使用 == 操做符檢查 "參數是否爲這個對象的引用"

若是是,則返回true。這只不過是一種性能優化,若是比較操做有可能很昂貴,就值得這麼作。

(2)使用 instanceof 操做符檢查 "參數是否爲正確的類型"

若是不是,則返回false。所謂的正確類型是指equals方法所在的那個類。
某些狀況是指該類實現的改進了equals方法的接口,此接口容許實現了該接口的類進行比較。

(3)把參數轉換爲正確的類型

由於轉換以前進行過instanceof測試,因此確保會成功。

(4)檢查參數中的域是否與該對象中對應的域相匹配

若是這些測試所有成功,則返回true,不然返回false
對於不是floatdouble類型的基本類型域,可使用==操做符進行比較。
對於對象引用域,能夠遞歸地調用euqals方法。
對於floatdouble域,應該使用Float.compareDouble.compare方法。由於存在Float.NaN、-0.0f等常量
對於數組,則可使用Arrays.equals方法。
有些對象引用域包含null是合法的。爲了不空指針異常,能夠這樣比較:
(field == null ? o.field==null : field.equals(o.field))

(5)當寫完equals方法後,應該測試它們是不是對稱的、傳遞的、一致的

示例以下:

public boolean equals(Object o){
    if(o == this) 
        return true;
    if(!(o instanceof MyClass)) 
       return false;
    Myclass mc=(MyClass)o;
    return mc.x == x && mc.y == y;
}

3.注意:
(1)覆蓋equals時總要覆蓋hashCode
(2)不要讓equals方法過於智能
(3)不要將equals聲明中的Object對象替換成其它類型

public boolean equals(MyClass o){
          ..........
     }

問題在於,這個方法並無覆蓋Object.equals,由於它的參數類型應該是Object。相反,它重載了Object.equals。

覆蓋equals時總要覆蓋hashCode

在每一個覆蓋了equals方法的類中,也必須覆蓋hashCode方法

  • 若是x.euqals(y)返回true,那麼x和y的哈希值必定相等。
  • 若是x.equals(y)返回false,那麼x和y的哈希值也有可能相等。
  • 若是x和y的哈希值不相等,那麼x.equals(y)必定返回false。

示例以下

public int hashCode(){
   int result = 17;
   result = 31 * result + x;
   result = 31 * result + y;
   result = 31 * result + z;
   return result;
}

若是一個類是不可變的,而且計算哈希值的開銷比較大,就應該考慮把哈希值保存在對象內部,而不是每次請求的時候都從新計算哈希值(好比String類內部就有個int類型的hash變量來保存哈希值)。

堅持使用Override註解

先看下面這個反例:

public class Bigram {

    private final char first;
    private final char second;

    public Bigram(char first, char second) {
        this.first = first;
        this.second = second;
    }

    public boolean equals(Bigram b) {
        return b.first == first && b.second == second;
    }

    public int hashcode() {
        return 31 * first + second;
    }

    public static void main(String[] args) {
        Set<Bigram> s = new HashSet<>();
        for (int i = 0; i < 10; i++) {
            for (char ch = 'a'; ch <= 'z'; ch++) {
                s.add(new Bigram(ch, ch));
            }
        }
        System.out.println(s.size());
    }
}

上面這個例子你可能覺得程序打印出的大小爲26,由於集合不能包含重複對象,可是運行後你會發現打印的不是26而是260,究竟是哪裏出錯了呢?

很顯然,Bigram類的建立者本來想要覆蓋equals方法,同時還記得覆蓋了hashcode方法,惋惜這個程序沒能覆蓋到equals方法,而是重載了Object類的equals方法。由於若是想要覆蓋Object類的equals方法你必須定義一個Object類型的equals方法,而在上面的例子中只是作了重載操做。

只有當你使用@Override標註Bigram類時,編譯器才能幫你發現這個錯誤,若是加上這個註解而且試着運行程序,編譯器會產生一條下面這樣地錯誤信息:method does not override or implement a method from a supertype,這樣的話你會立刻意識到本身哪裏錯了,而且用正確的來取代錯誤的方法,以下:

@Override
    public boolean equals(Object o) {
        if (!(o instanceof Bigram)) {
            return false;
        }
        Bigram b = (Bigram) o;
        return b.first == first && b.second == second;
    }

所以,你應該在你想要覆蓋父類的每一個方法中加上@Override註解,這樣的話編譯器就能夠幫你防止大量的錯誤。

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

Java1.5發行版本中引入的for-each循環,經過徹底隱藏迭代器或者索引變量,避免了混亂和出錯的可能,以下:

for(Element e : elements){
     doSomething(e)
}

注意:利用for-each循環不會有性能損失,實際上在某些狀況下比起普通的for循環,它還有些性能優點,由於它對數組索引的邊界值只計算一次。

總之,for-each循環在簡潔性和預防BUG方面有着傳統的for循環沒法比擬的優點,而且沒有性能損失。應該儘量地使用for-each循環。遺憾的是,有三種常見的狀況沒法使用for-each循環:

  • 過濾——若是須要遍歷集合,並刪除指定的元素,就須要使用顯示的迭代器,以即可以調用它的remove方法
  • 轉換——若是須要遍歷列表或者數組,並取代它部分或者所有的元素值,就須要列表迭代器或者索引數組,以便設定元素的值
  • 平行迭代——若是須要並行地遍歷多個集合,就須要顯示的控制迭代器或者索引變量。以便全部迭代器啊或者索引變量均可以獲得同步移動

若是須要精確的答案,請避免使用float和double

floatdouble類型主要是爲了科學計算和工程計算而設計的,然而塔門並無提供徹底精確的結果,因此不該該被用於須要精確結果的場合。float和double類型尤爲不適合於貨幣計算,由於要讓一個float和double精確地表示0.1是不可能的。

好比下面這個例子

public class Test {
    public static void main(String[] args) {
        System.out.println(1.0 - 0.9);
    }
}

輸出結果爲:0.09999999999999998

解決這個問題的正確方法時使用BigDecimalint或者long進行貨幣計算。

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

Java中變量主要由兩部分組成,一個是基本類型,如int、double和boolean等,另一個是引用類型,如String和List等。每一個基本類型都有一個對應的引用類型,稱做裝箱基本類型。好比int對應Integer、boolean對應Boolean等

Java1.5版本增長了自動裝箱和自動拆箱,可是這兩種類型之間是有差異的。

看下面這個比較器

Comparator<Integer> comparator = new Comparator<Integer>(){
   public int compare(Integer first,Integer second){
       return first < second ? -1:(first == second ? 0:1);
   }
}

這個比較器表面上看起來不錯,它能夠經過許多測試。可是當你打印comparator.compare(new Integer(42),new Integer(42))時,本應該打印出0,可是最後結果倒是1,這代表第一個Integer值大於第二個。

問題出在哪裏呢?方法中的第一個測試作的很好,當執行first < second 時會使first和second引用的Integer類型被自動拆箱,可是後面再計算first == second時,由於是對象之間使用==比較,這時候比較的是對象的內存地址,若是是兩個不一樣的對象就會返回false,因此不該該用==來比較兩個對象的值。

正確的程序應該是下面這樣

Comparator<Integer> comparator = new Comparator<Integer>(){
   public int compare(Integer first,Integer second){
       int f = first;
       int s = second;
       return f < s ? -1:(f == s ? 0:1);
   }
}

那麼何時使用裝箱基本類型呢?它們有幾個合理的用處:
第一個是做爲集合中的元素,你不能將基本類型放在集合中,如List<Integer>而不是List<int>
第二個是在泛型中必須使用裝箱基本類型爲類型參數,如ThreadLocal<Integer>。

總之,當能夠選擇的時候,基本類型要優先於裝箱基本類型。基本類型更加簡單,也更加快速。

總結

有關《Effective Java》的知識點就介紹到這裏,最近在看《代碼整潔之道》,後續可能也會來單獨作個總結,如有不對的地方請多多指教。

相關文章
相關標籤/搜索