最近讀完了《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));
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)
返回true
,y.equals(z)
返回true
,那麼x.equals(z)
也應該返回true
(4)一致性
對於任何非null的引用值x,只要對象沒有修改,那麼x.equals(y)
會一直返回true
(5)非空性
對於任何非null的引用值x,x.equals(null)
必須返回false
(1)使用 ==
操做符檢查 "參數是否爲這個對象的引用"。
若是是,則返回true
。這只不過是一種性能優化,若是比較操做有可能很昂貴,就值得這麼作。
(2)使用 instanceof
操做符檢查 "參數是否爲正確的類型"
若是不是,則返回false
。所謂的正確類型是指equals
方法所在的那個類。
某些狀況是指該類實現的改進了equals
方法的接口,此接口容許實現了該接口的類進行比較。
(3)把參數轉換爲正確的類型
由於轉換以前進行過instanceof
測試,因此確保會成功。
(4)檢查參數中的域是否與該對象中對應的域相匹配
若是這些測試所有成功,則返回true
,不然返回false
。
對於不是float
和double
類型的基本類型域,可使用==
操做符進行比較。
對於對象引用域,能夠遞歸地調用euqals
方法。
對於float
和double
域,應該使用Float.compare
和Double.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方法。
示例以下
public int hashCode(){ int result = 17; result = 31 * result + x; result = 31 * result + y; result = 31 * result + z; return result; }
若是一個類是不可變的,而且計算哈希值的開銷比較大,就應該考慮把哈希值保存在對象內部,而不是每次請求的時候都從新計算哈希值(好比String類內部就有個int類型的hash變量來保存哈希值)。
先看下面這個反例:
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註解,這樣的話編譯器就能夠幫你防止大量的錯誤。
Java1.5發行版本中引入的for-each循環,經過徹底隱藏迭代器或者索引變量,避免了混亂和出錯的可能,以下:
for(Element e : elements){ doSomething(e) }
注意:利用for-each循環不會有性能損失,實際上在某些狀況下比起普通的for循環,它還有些性能優點,由於它對數組索引的邊界值只計算一次。
總之,for-each循環在簡潔性和預防BUG方面有着傳統的for循環沒法比擬的優點,而且沒有性能損失。應該儘量地使用for-each循環。遺憾的是,有三種常見的狀況沒法使用for-each循環:
float
和double
類型主要是爲了科學計算和工程計算而設計的,然而塔門並無提供徹底精確的結果,因此不該該被用於須要精確結果的場合。float和double類型尤爲不適合於貨幣計算,由於要讓一個float和double精確地表示0.1是不可能的。
好比下面這個例子
public class Test { public static void main(String[] args) { System.out.println(1.0 - 0.9); } }
輸出結果爲:0.09999999999999998
解決這個問題的正確方法時使用BigDecimal
、int
或者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》的知識點就介紹到這裏,最近在看《代碼整潔之道》,後續可能也會來單獨作個總結,如有不對的地方請多多指教。