Effective Java:对于所有对象都通用的方法
前言:
? 讀這本書(shū)第1條規(guī)則的時(shí)候就感覺(jué)到這是一本很好的書(shū),可以把我們的Java功底提升一個(gè)檔次,我還是比較推薦的。這里我主要就關(guān)于覆蓋equals、hashCode和toString方法來(lái)做一個(gè)筆記總結(jié),希望能夠與君共勉。
概述:
? 這一章主要是說(shuō)明一些對(duì)于所有對(duì)象都通用的方法。我們知道Java的多態(tài)是其特色之一,而多態(tài)的體現(xiàn)方式中就有一種方式叫做“重寫(xiě)”。這些概念性的東西我想在大學(xué)我們學(xué)習(xí)Java的初期,老師就會(huì)如數(shù)家珍一樣地灌輸給我們,不過(guò),在那個(gè)時(shí)候有多少人真的了解了什么是重載,什么是重寫(xiě),什么是多態(tài)呢?
? 而對(duì)于現(xiàn)在的一些開(kāi)發(fā)者而言,了解并使用它們是家常便飯,理所應(yīng)當(dāng)。但是,你真的是已經(jīng)夠了解嗎?
相關(guān)內(nèi)容:
1.覆蓋equals時(shí)請(qǐng)遵守通用約定
? 我們知道Java中如果需要比較兩個(gè)對(duì)象是否相等的時(shí)候,就會(huì)用到equals。對(duì)于初學(xué)者,可能遇到更多的是equals與"=="的區(qū)別,可能一開(kāi)始大家都是一頭霧水,傻傻分不清楚,這可能是因?yàn)槟氵€沒(méi)有地址和值的概念。關(guān)于equals與"=="的區(qū)別,大家可以看看這篇博客——Java中equals和==的區(qū)別
? 如果你還不是很清楚equals和"=="的區(qū)別,那么,你可以花幾分鐘看看上面的博客,以便你可以明白,我們?yōu)槭裁匆采wequals方法。如果你已全然了解,那么便沒(méi)有什么東西可以阻止你繼續(xù)往下看。
? 我們知道equals要實(shí)現(xiàn)的是邏輯上的等。站在數(shù)學(xué)的角度來(lái)看,兩個(gè)事物相等的條件,有如下幾個(gè):
? 1.自反性:對(duì)于任何非null的引用值x,x.equals(x)必須返回true.
? 2.對(duì)稱性:對(duì)于非空的引用值x,y,當(dāng)且僅當(dāng)x.equals(y)返回true時(shí),y.equals(x)必須返回true.
? 3.傳遞性:對(duì)于任何非null的引用值x,y,z,如果x.equals(y)=true,y.equals(z)=true,那么x.equals(z)也必須返回true。
? 4.一致性:對(duì)于任何非null的引用值x,y,只要equals的比較操作在對(duì)象中所用的信息沒(méi)有被修改,多次調(diào)用x.equals(y)就會(huì)一致地返回true,或一致地返回false.
? 5.對(duì)于非null的引用值x,x.equals(null)必須返回false.
? 看完上面的這些數(shù)學(xué)式的規(guī)則,你是不是有一種哪要這么麻煩的事的感覺(jué)呢?從直觀上來(lái)說(shuō),上面的這些規(guī)則的確是有一些麻煩,但你卻不能忽視它們,不然麻煩的可就是你了。
? 下面我會(huì)通過(guò)一些實(shí)例的學(xué)習(xí),來(lái)說(shuō)明這些規(guī)則。
? 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> 結(jié)果:
<span style="font-family:Courier New;font-size:18px;">true true</span>
? 2.對(duì)稱性:
? 對(duì)于對(duì)稱性,可能你會(huì)感覺(jué)理所當(dāng)然。這是因?yàn)樵谀憧磥?lái),我們要比較的兩者必定是同一類型,這個(gè)必定太過(guò)理想化了,如果我們比較的兩個(gè)對(duì)象不是同一種類型呢?下面可以看看這個(gè)例子。
<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方法的代碼實(shí)現(xiàn)了忽略大小寫(xiě)來(lái)比較字符串。我們先不考慮同類型的兩個(gè)對(duì)象比較,對(duì)于不同類型的兩個(gè)對(duì)象,從上面的代碼中我們可以看出,如果被比較的對(duì)象是一個(gè)String類型的,那么我們就可以去忽視大小寫(xiě)進(jìn)行比較,答案也是在情理之中。下面看看例證:? 比較方法:
<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>? 比較結(jié)果:
<span style="font-family:Courier New;font-size:18px;">s1 == s2 ? true s2 == s1 ? false</span>? 這是為什么?不是說(shuō)equals要滿足對(duì)稱性的嗎?怎么這里又行不通了呢?
? 仔細(xì)推敲一番就可以發(fā)現(xiàn)了,我們?cè)谶M(jìn)行s1.equals(s2)的時(shí)候,是因?yàn)閟1是CaseInsensitiveString類型的,它會(huì)執(zhí)行到上面的代碼,而s2是String類型的,s2.equals(s1)的比較自然是String中的equals方法。
? 那你又會(huì)問(wèn),既然這樣我們總不能去修改String類中的代碼吧。如果你這樣想,那我就無(wú)言以對(duì)了。我們知道一件事,兩個(gè)不同類型的對(duì)象我就讓它不相同去吧。也就是說(shuō),我們要有一個(gè)判斷告訴程序,如果被比較的對(duì)象不是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了。
? 現(xiàn)在假設(shè)我們有一個(gè)類Point和一個(gè)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,不過(guò)比Point類多一個(gè)顏色屬性。當(dāng)我們把ColorPoint與Point和Point與ColorPoint進(jìn)行比較,如下: <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>
會(huì)得到如下結(jié)果:
? 為什么兩個(gè)都true呢?明明兩個(gè)不同類型啊,如果真的要去考慮父類與子類的關(guān)系,也應(yīng)該是一個(gè)true一個(gè)false啊。因?yàn)檫@里我們的ColorPoint本身沒(méi)有重寫(xiě)Point的equals,它使用的是Point的equals,這時(shí)無(wú)論哪一次的比較中,都是去比較x和y,與color無(wú)關(guān)。
? 這樣就會(huì)導(dǎo)致一個(gè)問(wèn)題,如果我的兩個(gè)比較對(duì)象都是ColorPoint呢?這樣一來(lái)如果我的兩個(gè)ColorPoint的x和y全都一樣,只是color不同,那么無(wú)論怎么比較,其結(jié)果值都會(huì)是true.這里不會(huì)去檢查color。那你可能就會(huì)說(shuō),那我們就重寫(xiě)ColorPoint的equals啊。
? 這里我們使用一條建議:復(fù)合優(yōu)于繼承(這一點(diǎn)在設(shè)計(jì)模式中也有體現(xiàn))。
? 實(shí)例示范:
<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>
? 結(jié)果:
<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> ? 上面的代碼看上去很簡(jiǎn)潔。
? 4.一致性
? 一致性的要求是,如果兩個(gè)對(duì)象相等,它們就必須始終保持相等,除非它們中有一個(gè)對(duì)象被修改了。?
2.覆蓋equals時(shí)總要覆蓋hashCode
? 為什么要說(shuō)覆蓋equals時(shí)總要覆蓋hashCode呢?前面我們說(shuō)的那些不都好好的么?一些equals必需的數(shù)學(xué)規(guī)則不是都已經(jīng)滿足了么?我們不是已經(jīng)做得差不多了么?是的,的確是差不多了,不過(guò)我們還是要去覆蓋hashCode方法。這是因?yàn)槲覀內(nèi)绻盐覀兊膶?duì)象與HashMap之類的Hash值聯(lián)系起來(lái),有此時(shí)候可能會(huì)感到困惑,甚至大失所望。下面,我們就來(lái)列舉一個(gè)例子,根據(jù)例子來(lái)說(shuō)明再合適不過(guò)了。
? 我們有這樣一個(gè)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);} } ? 上面的代碼對(duì)equals的處理完全OK,不過(guò)如果我們把PhoneNumber和HashMap放在一起使用,結(jié)果會(huì)如何?下面是我們的測(cè)試用例:
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));}
結(jié)果:
? 我們可以這樣來(lái)理解上面的map.put()。如果我們不去覆蓋hashCode,那么當(dāng)我們使用map.put時(shí),我們是把這些PhoneNumber對(duì)象放在各個(gè)不同的盒子里,而我們?nèi)ap.get()的時(shí)候,只是去某一個(gè)盒子里去找(當(dāng)然,如果map.get()和map.put()中的對(duì)象是同一個(gè)的話,當(dāng)然可以找到)。
? 而如果我們覆蓋了hashCode方法,這時(shí),如果通過(guò)hashCode計(jì)算出來(lái)的值是相等的,就會(huì)放在同一個(gè)盒子里。這樣,只要我們對(duì)象中保存的值是完全一致的,就會(huì)找到這個(gè)key所對(duì)應(yīng)的value。不知道你發(fā)現(xiàn)沒(méi)有,這個(gè)hashCode有點(diǎn)類似于分類,這樣在數(shù)據(jù)量比較大的情況下就會(huì)大大提高效率。
? 我們可以通過(guò)以下兩種方法來(lái)覆蓋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;}
? 首先兩種方法都可以。通過(guò)上面的分析,從效率的角度來(lái)考慮,當(dāng)然是第二種方法更為恰當(dāng)。
? 所以在覆蓋了equlas的同時(shí),別忘了去覆蓋hashCode.
3.始終要覆蓋toString
? 承上,就拿PhoneNumber類來(lái)說(shuō),如果我們不去覆蓋類的toString()方法,后果就是當(dāng)我們需要去打印這個(gè)類的對(duì)象時(shí),會(huì)有一些并非是我們想要的那種。類似這樣的:com.java.effective.samples.PhoneNumber@12a7e3
? 有時(shí)我們不希望打印出這樣的對(duì)象,那我們就要去覆蓋它們的toString方法了。在這個(gè)方法里,我們可以按照我們自己的意愿來(lái)給類添加toString方法。對(duì)于PhoneNumber,我們可以這樣來(lái)寫(xiě):
@Overridepublic String toString() {String result = "";result += (areaCode + "-");result += (prefix + "-");result += (lineNumber);return result;}
打印結(jié)果:
707-867-9876
總結(jié):
? 在我們優(yōu)化代碼的時(shí)候不妨考慮一下去合理地覆蓋這些方法,可以讓我們的代碼更加健壯。
總結(jié)
以上是生活随笔為你收集整理的Effective Java:对于所有对象都通用的方法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 在Win7中使用Python的MySQL
- 下一篇: CentOS下配置HTTPS访问主机并绑