Effective Java:对于所有对象都通用的方法
前言:
? 讀這本書第1條規則的時候就感覺到這是一本很好的書,可以把我們的Java功底提升一個檔次,我還是比較推薦的。這里我主要就關于覆蓋equals、hashCode和toString方法來做一個筆記總結,希望能夠與君共勉。
概述:
? 這一章主要是說明一些對于所有對象都通用的方法。我們知道Java的多態是其特色之一,而多態的體現方式中就有一種方式叫做“重寫”。這些概念性的東西我想在大學我們學習Java的初期,老師就會如數家珍一樣地灌輸給我們,不過,在那個時候有多少人真的了解了什么是重載,什么是重寫,什么是多態呢?
? 而對于現在的一些開發者而言,了解并使用它們是家常便飯,理所應當。但是,你真的是已經夠了解嗎?
相關內容:
1.覆蓋equals時請遵守通用約定
? 我們知道Java中如果需要比較兩個對象是否相等的時候,就會用到equals。對于初學者,可能遇到更多的是equals與"=="的區別,可能一開始大家都是一頭霧水,傻傻分不清楚,這可能是因為你還沒有地址和值的概念。關于equals與"=="的區別,大家可以看看這篇博客——Java中equals和==的區別
? 如果你還不是很清楚equals和"=="的區別,那么,你可以花幾分鐘看看上面的博客,以便你可以明白,我們為什么要覆蓋equals方法。如果你已全然了解,那么便沒有什么東西可以阻止你繼續往下看。
? 我們知道equals要實現的是邏輯上的等。站在數學的角度來看,兩個事物相等的條件,有如下幾個:
? 1.自反性:對于任何非null的引用值x,x.equals(x)必須返回true.
? 2.對稱性:對于非空的引用值x,y,當且僅當x.equals(y)返回true時,y.equals(x)必須返回true.
? 3.傳遞性:對于任何非null的引用值x,y,z,如果x.equals(y)=true,y.equals(z)=true,那么x.equals(z)也必須返回true。
? 4.一致性:對于任何非null的引用值x,y,只要equals的比較操作在對象中所用的信息沒有被修改,多次調用x.equals(y)就會一致地返回true,或一致地返回false.
? 5.對于非null的引用值x,x.equals(null)必須返回false.
? 看完上面的這些數學式的規則,你是不是有一種哪要這么麻煩的事的感覺呢?從直觀上來說,上面的這些規則的確是有一些麻煩,但你卻不能忽視它們,不然麻煩的可就是你了。
? 下面我會通過一些實例的學習,來說明這些規則。
? 1.自反性:
<span style="font-family:Courier New;font-size:18px;">public static void equalsOppositeSelf() {String s = "ABC";Object o = new Object();System.out.println(s.equals(s));</span> 結果:
<span style="font-family:Courier New;font-size:18px;">true true</span>
? 2.對稱性:
? 對于對稱性,可能你會感覺理所當然。這是因為在你看來,我們要比較的兩者必定是同一類型,這個必定太過理想化了,如果我們比較的兩個對象不是同一種類型呢?下面可以看看這個例子。
<span style="font-family:Courier New;font-size:18px;">public final class CaseInsensitiveString {private final String s;public CaseInsensitiveString(String s) {if (s == null) {throw new NullPointerException();}this.s = s;}public boolean equals(Object o) {if (o instanceof CaseInsensitiveString) {return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);}if (o instanceof String) {return s.equalsIgnoreCase((String)o);}return false;} }</span> ? 上面equals方法的代碼實現了忽略大小寫來比較字符串。我們先不考慮同類型的兩個對象比較,對于不同類型的兩個對象,從上面的代碼中我們可以看出,如果被比較的對象是一個String類型的,那么我們就可以去忽視大小寫進行比較,答案也是在情理之中。下面看看例證:? 比較方法:
<span style="font-family:Courier New;font-size:18px;">public static void equalsSymmetric() {CaseInsensitiveString s1 = new CaseInsensitiveString("abc");String s2 = "abc";System.out.println("s1 == s2 ? " + s1.equals(s2));System.out.println("s2 == s1 ? " + s2.equals(s1));}</span>? 比較結果:
<span style="font-family:Courier New;font-size:18px;">s1 == s2 ? true s2 == s1 ? false</span>? 這是為什么?不是說equals要滿足對稱性的嗎?怎么這里又行不通了呢?
? 仔細推敲一番就可以發現了,我們在進行s1.equals(s2)的時候,是因為s1是CaseInsensitiveString類型的,它會執行到上面的代碼,而s2是String類型的,s2.equals(s1)的比較自然是String中的equals方法。
? 那你又會問,既然這樣我們總不能去修改String類中的代碼吧。如果你這樣想,那我就無言以對了。我們知道一件事,兩個不同類型的對象我就讓它不相同去吧。也就是說,我們要有一個判斷告訴程序,如果被比較的對象不是CaseInsensitiveString類型,那我們就不用客氣直接返回false就行了。修改后的代碼如下:
<span style="font-family:Courier New;font-size:18px;"> public boolean equals(Object o) {return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);}</span>? 3.傳遞性
? 傳遞性的判斷是x = y, y = z,那么就可以判斷x = z了。
? 現在假設我們有一個類Point和一個Point的子類ColorPoint分別如下:
? Point
<span style="font-family:Courier New;font-size:18px;">public class Point {private final int x;private final int y;public Point(int x, int y) {this.x = x;this.y = y;}public boolean equals(Object o) {if (!(o instanceof Point)) {return false;}Point p = (Point) o;return p.x == x && p.y == y;} }</span>
? ColorPoint
<span style="font-family:Courier New;font-size:18px;">public class ColorPoint extends Point {private final Color color;public ColorPoint(int x, int y, Color color) {super(x, y);this.color = color;} }</span>? 可以看到ColorPoint繼承于Point,不過比Point類多一個顏色屬性。當我們把ColorPoint與Point和Point與ColorPoint進行比較,如下: <span style="font-family:Courier New;font-size:18px;">public static void equalsTransitivity() {Point p1 = new Point(1, 2);ColorPoint cp1 = new ColorPoint(1, 2, Color.BLACK);System.out.println("p1 == cp1 ? " + p1.equals(cp1));System.out.println("cp1 == p1 ? " + cp1.equals(p1));}</span>
會得到如下結果:
? 為什么兩個都true呢?明明兩個不同類型啊,如果真的要去考慮父類與子類的關系,也應該是一個true一個false啊。因為這里我們的ColorPoint本身沒有重寫Point的equals,它使用的是Point的equals,這時無論哪一次的比較中,都是去比較x和y,與color無關。
? 這樣就會導致一個問題,如果我的兩個比較對象都是ColorPoint呢?這樣一來如果我的兩個ColorPoint的x和y全都一樣,只是color不同,那么無論怎么比較,其結果值都會是true.這里不會去檢查color。那你可能就會說,那我們就重寫ColorPoint的equals啊。
? 這里我們使用一條建議:復合優于繼承(這一點在設計模式中也有體現)。
? 實例示范:
<span style="font-family:Courier New;font-size:18px;">public static void equalsTransitivity() {Point p1 = new Point(1, 2);ColorPoint cp1 = new ColorPoint(1, 2, Color.BLACK);ColorPoint cp2 = new ColorPoint(1, 2, Color.BLUE);ColorPointNew cpn1 = new ColorPointNew(1, 2, Color.BLACK);ColorPointNew cpn2 = new ColorPointNew(1, 2, Color.BLUE);System.out.println("p1 == cp1 ? " + p1.equals(cp1));System.out.println("cp1 == p1 ? " + cp1.equals(p1));System.out.println("cp1 == cp2 ? " + cp1.equals(cp2));System.out.println("cpn1 == cpn2 ? " + cpn1.equals(cpn2));System.out.println("cpn1 == cp1 ? " + cpn1.equals(cp1));System.out.println("cp1 == cpn1 ? " + cp1.equals(cpn1));}</span>
? 結果:
<span style="font-family:Courier New;font-size:18px;">p1 == cp1 ? true cp1 == p1 ? true cp1 == cp2 ? true cpn1 == cpn2 ? false cpn1 == cp1 ? false cp1 == cpn1 ? false</span> ? 上面的代碼看上去很簡潔。
? 4.一致性
? 一致性的要求是,如果兩個對象相等,它們就必須始終保持相等,除非它們中有一個對象被修改了。?
2.覆蓋equals時總要覆蓋hashCode
? 為什么要說覆蓋equals時總要覆蓋hashCode呢?前面我們說的那些不都好好的么?一些equals必需的數學規則不是都已經滿足了么?我們不是已經做得差不多了么?是的,的確是差不多了,不過我們還是要去覆蓋hashCode方法。這是因為我們如果把我們的對象與HashMap之類的Hash值聯系起來,有此時候可能會感到困惑,甚至大失所望。下面,我們就來列舉一個例子,根據例子來說明再合適不過了。
? 我們有這樣一個PhoneNumber類:
package com.java.effective.samples;public final class PhoneNumber {private final short areaCode;private final short prefix;private final short lineNumber;public PhoneNumber(int areaCode, int prefix, int lineNumber) {rangeCheck(areaCode, 999, "area code");rangeCheck(prefix, 999, "prefix");rangeCheck(lineNumber, 9999, "line number");this.areaCode = (short)areaCode;this.prefix = (short)prefix;this.lineNumber = (short)lineNumber;}private static void rangeCheck(int arg, int max, String name) {if (arg < 0 || arg > max) {throw new IllegalArgumentException(name + ": " + arg);}}@Overridepublic boolean equals(Object o) {if (o == this) {return true;}if (!(o instanceof PhoneNumber)) {return false;}PhoneNumber pNumber = (PhoneNumber)o;return (pNumber.lineNumber == lineNumber) && (pNumber.prefix == prefix) && (pNumber.areaCode == areaCode);} } ? 上面的代碼對equals的處理完全OK,不過如果我們把PhoneNumber和HashMap放在一起使用,結果會如何?下面是我們的測試用例:
public static void hashCodePhoneNumber() {Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>();PhoneNumber phoneNumber = new PhoneNumber(707, 867, 9876);map.put(phoneNumber, "Jenny");System.out.println(map.get(new PhoneNumber(707, 867, 9876)));System.out.println(map.get(phoneNumber));}
結果:
? 我們可以這樣來理解上面的map.put()。如果我們不去覆蓋hashCode,那么當我們使用map.put時,我們是把這些PhoneNumber對象放在各個不同的盒子里,而我們去map.get()的時候,只是去某一個盒子里去找(當然,如果map.get()和map.put()中的對象是同一個的話,當然可以找到)。
? 而如果我們覆蓋了hashCode方法,這時,如果通過hashCode計算出來的值是相等的,就會放在同一個盒子里。這樣,只要我們對象中保存的值是完全一致的,就會找到這個key所對應的value。不知道你發現沒有,這個hashCode有點類似于分類,這樣在數據量比較大的情況下就會大大提高效率。
? 我們可以通過以下兩種方法來覆蓋hashCode方法:
方法一:
@Overridepublic int hashCode() {return 42;}
方法二:
@Overridepublic int hashCode() {int result = 17;result = 31 * result + areaCode;result = 31 * result + prefix;result = 31 * result + lineNumber;return result;}
? 首先兩種方法都可以。通過上面的分析,從效率的角度來考慮,當然是第二種方法更為恰當。
? 所以在覆蓋了equlas的同時,別忘了去覆蓋hashCode.
3.始終要覆蓋toString
? 承上,就拿PhoneNumber類來說,如果我們不去覆蓋類的toString()方法,后果就是當我們需要去打印這個類的對象時,會有一些并非是我們想要的那種。類似這樣的:com.java.effective.samples.PhoneNumber@12a7e3
? 有時我們不希望打印出這樣的對象,那我們就要去覆蓋它們的toString方法了。在這個方法里,我們可以按照我們自己的意愿來給類添加toString方法。對于PhoneNumber,我們可以這樣來寫:
@Overridepublic String toString() {String result = "";result += (areaCode + "-");result += (prefix + "-");result += (lineNumber);return result;}
打印結果:
707-867-9876
總結:
? 在我們優化代碼的時候不妨考慮一下去合理地覆蓋這些方法,可以讓我們的代碼更加健壯。
總結
以上是生活随笔為你收集整理的Effective Java:对于所有对象都通用的方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在Win7中使用Python的MySQL
- 下一篇: Java字符编码的转化问题