Effective Java——對(duì)于所有對(duì)象都通用的方法

Android.jpg

本系列文章是總結(jié)Effective Java文章中我認(rèn)為最重點(diǎn)的內(nèi)容,給很多沒(méi)時(shí)間看書的朋友以最短的時(shí)間看到這本書的精華。
第一篇《Effective Java——?jiǎng)?chuàng)建和銷毀對(duì)象》

第三章對(duì)于所有對(duì)象都通用的方法

目錄.png

第8條:覆蓋equals時(shí)請(qǐng)遵守通用約定

符合以下條件則不需要覆蓋equals情況

類的每個(gè)實(shí)例本質(zhì)上都是唯一的

對(duì)于代表活動(dòng)實(shí)體而不是值(value)的類。例如:Thread,他只關(guān)注該類能完成的任務(wù)或者功能,而不是像Integer關(guān)心他中存在的值(value),對(duì)于Thread這樣的類Object提供的默認(rèn)equals方法完全能夠滿足要求。

不關(guān)心類是否提供了“邏輯相等”的測(cè)試功能

例如:Random類,如果對(duì)他進(jìn)行覆蓋equals方法將毫無(wú)意義,使用者根本不關(guān)心多個(gè)Random實(shí)例產(chǎn)生的隨機(jī)數(shù)是否相等。
邏輯相等:實(shí)例內(nèi)存在的值是否相等,例如:Integer的兩個(gè)實(shí)例用equals來(lái)判斷是否邏輯相等。

超類已經(jīng)覆蓋了equals,從超類繼承過(guò)來(lái)的行為對(duì)于子類也是合適的

集合類:Set實(shí)現(xiàn)都從AbstractSet繼承equals實(shí)現(xiàn),List實(shí)現(xiàn)從AbstractList繼承equals實(shí)現(xiàn),Map實(shí)現(xiàn)從AbstractMap繼承equals實(shí)現(xiàn)。

類是私有的或是包級(jí)私有的,可以確定他的equals方法永遠(yuǎn)不會(huì)被調(diào)用

私有的或是包級(jí)私有的類不可能對(duì)外部提供。

需要覆蓋equals方法

對(duì)于“值類(value class)”來(lái)說(shuō),它們需要判斷“邏輯相等”,且超類還沒(méi)有覆蓋equals實(shí)現(xiàn)期望的行為,這時(shí)我們需要覆蓋equals方法。
值類:例如,Integer、Date,僅僅表示一個(gè)值,我們?cè)谡{(diào)用equals時(shí)希望知道它們邏輯上是否相等,而不是想了解他們是否指向同一個(gè)對(duì)象。

覆蓋equals遵守的約定

自反性

對(duì)于任何非null的引用值x,x.equals(x)必須返回true。

對(duì)稱性

對(duì)于任何非null的引用值x和y,如果x.equals(y)返回true,則y.equals(x)也必須返回true

傳遞性

對(duì)于任何非null的引用值x、y和z,如果x.equals(y)返回truey.equals(z)返回truex.equals(z)也必須返回true

一致性

對(duì)于任何非null的引用值x和y,只要equals方法內(nèi)部比較所用到的字段內(nèi)容沒(méi)有被修改,那么多次調(diào)用x.equals(y)就會(huì)一致地返回true,或者false。

非空性

所有比較的對(duì)象都不為null,如下代碼:

 @Override
public boolean equals(Object o){
    if(null == o){
        return false;
    }
......
}

其實(shí)如上代碼根本不必要我們只需要用instanceof來(lái)判斷參數(shù)類型,如果參數(shù)時(shí)null那么instanceof一定會(huì)返回false,如下代碼:

@Override
public boolean equals(Object o){
    if(!(o instanceof MyClass)){
        return false;
    }
......
}

編寫equals規(guī)則

1. 使用==操作符檢查“參數(shù)是否為這個(gè)對(duì)象的引用”
2. 使用instanceof操作符檢查“參數(shù)是否為正確的類型”
3. 把參數(shù)轉(zhuǎn)換成正確的類型
4. 對(duì)于該類中的每個(gè)“關(guān)鍵”域,檢查參數(shù)中的域是否與該對(duì)象中對(duì)應(yīng)的域相匹配

在這里要多說(shuō)幾點(diǎn):
對(duì)于float字段,要用Float.compare方法來(lái)比較,對(duì)于double字段,則使用Double.compare方法,由于存在著Float.NaN、-0.0f以及類似的double常量。
為了獲得equals方法的最佳性能,應(yīng)該最先比較最有可能不一致的字段,或者開(kāi)銷最低的字段。

5. 當(dāng)編寫完成equals方法之后,檢查他是否滿足,對(duì)稱,傳遞,一致性

如下代碼:

public class TestClass{
    private String field;
    @Override
    public boolean equals(Object obj) {
        //第一步
        if(this == obj){
            return true;
        }
        //第二步
        if(!(obj instanceof TestClass)){
            return false;
        }
        //第三步
        TestClass testClass = (TestClass) obj;
        //第四步
         return null == this.field ? null == testClass.field : this.field.equals(testClass.field);
    }
}

編寫equals的忠告

  1. 覆蓋equals時(shí)一定要覆蓋hashCode方法
  2. 要盡量讓equals方法簡(jiǎn)單一些
  3. 不要將equals方法聲明中的參數(shù)轉(zhuǎn)換為其他類型

第9條:覆蓋equals時(shí)總要覆蓋hashCode

hashCode約定

  1. 在同一個(gè)實(shí)例對(duì)象中,只要equals方法用到的信息沒(méi)有被修改,那么這個(gè)對(duì)象多次調(diào)用hashCode方法應(yīng)該返回結(jié)果相同。
  2. 如果兩個(gè)對(duì)象根據(jù)equals方法比較是相等的,那么這兩個(gè)對(duì)象hashCode方法產(chǎn)生的結(jié)果也必須相等。
  3. 如果兩個(gè)對(duì)象根據(jù)equals方法比較是不相等的,那么這兩個(gè)對(duì)象hashCode方法產(chǎn)生的結(jié)果有可能相等,也有可能不相等。如果能實(shí)現(xiàn)不相等,可以提高散列表的性能。
public static final class PhoneNumber{
        private final int areaCode;
        private final int prefix;
        private final int lineNumber;
        public PhoneNumber(int areaCode, int prefix, int lineNumber){
            this.areaCode = areaCode;
            this.prefix = prefix;
            this.lineNumber = lineNumber;
        }
        @Override
        public boolean equals(Object obj) {
            if(this == obj){
                return true;
            }
            if(!(obj instanceof PhoneNumber)){
                return false;
            }
            PhoneNumber phoneNumber = (PhoneNumber) obj;
            return this.areaCode == phoneNumber.areaCode
                    && this.prefix == phoneNumber.prefix
                    && this.lineNumber == phoneNumber.lineNumber;
        }
    }

如上代碼只覆蓋了equals方法沒(méi)有覆蓋hashCode,當(dāng)我們把PhoneNumber應(yīng)用到Map集合上如下代碼:

Map<PhoneNumber, String > map = new HashMap<>();
map.put(new PhoneNumber(010,1111,2222),"Jenny");
        
String value = map.get((new PhoneNumber(010,1111,2222));

我們期望value=Jenny,實(shí)際上返回的null。
這是由于有兩個(gè)PhoneNumber實(shí)例,第一個(gè)實(shí)例用于put插入到HashMap中,第二個(gè)實(shí)例用于getHashMap中獲取,由于沒(méi)有覆蓋hashCode方法所以兩個(gè)實(shí)例的hashCode是不相等的。因此第一個(gè)實(shí)例根據(jù)自己的散列碼來(lái)保存數(shù)據(jù),第二個(gè)實(shí)力根據(jù)自己的散列碼來(lái)獲取數(shù)據(jù),由于兩個(gè)散列碼不相等所以get方法返回為null。
對(duì)于HashMap如何根據(jù)key來(lái)保存value請(qǐng)查看這篇文章,說(shuō)的非常想詳細(xì).
解決這個(gè)問(wèn)題就是在PhoneNumber類中根據(jù)上面的約定來(lái)覆蓋hashCode方法,
舉例如下代碼:

@Override
public int hashCode() {
      return lineNumber;
}

這段代碼只是舉個(gè)例子,具體的哈希值的計(jì)算非常復(fù)雜這里就不做講解了。哈希值計(jì)算的好壞直接影響HashMap的效率,有興趣可以在網(wǎng)上查查。

第10條:始終要覆蓋toString

  1. 對(duì)于任何類都推薦覆蓋toString方法,尤其是“值類”。
  2. toString返回的字符串描述了這個(gè)類的所有有用的信息,如下代碼:
 @Override
public String toString() {
    return "PhoneNumber{" +
               "areaCode=" + areaCode +
               ", prefix=" + prefix +
               ", lineNumber=" + lineNumber +
                '}';
}
  1. 對(duì)toString的返回值格式進(jìn)行約束。例如返回字符串在文檔中約束為Json格式,那么無(wú)論何種情況都不能修改它的返回格式。如果修改了,那么之前代碼中對(duì)這個(gè)返回值進(jìn)行解析的代碼都會(huì)報(bào)錯(cuò)。
  2. 對(duì)toString的返回值格式不進(jìn)行約束。在文檔中不限定他的返回格式,返回格式是可變的,有可能是Json有可能是Xml形式。如果代碼中用到解析toString返回格式的代碼其正確性就應(yīng)該由使用者來(lái)保證了。
  3. 對(duì)于toString中用到的有用信息,類中都要有相關(guān)的方法來(lái)返回值。避免使用者來(lái)解析格式不固定的toString方法的返回值.
    如下代碼:
 @Override
public String toString() {
    return "PhoneNumber{" +
               "areaCode=" + areaCode +
               ", prefix=" + prefix +
               ", lineNumber=" + lineNumber +
                '}';
}
//對(duì)于上面toString方法用到的信息都需要提供getX方法來(lái)獲取他們的值
public int getAreaCode() {
    return areaCode;
}
public int getPrefix() {
    return prefix;
}
public int getLineNumber() {
    return lineNumber;
}

第9條:謹(jǐn)慎的覆蓋clone

設(shè)計(jì)模式的原型模式可以由Objectclone方法來(lái)實(shí)現(xiàn)。

實(shí)現(xiàn)clone
  1. 當(dāng)前對(duì)象實(shí)現(xiàn)Cloneable接口。
  2. 當(dāng)前對(duì)象在public Object clone()方法中調(diào)用super.clone得到當(dāng)前類的一個(gè)對(duì)象。前提是父類實(shí)現(xiàn)的clone方法沒(méi)有問(wèn)題,也就是所有字段(可變字段)都已經(jīng)clone好了(深拷貝)。
  3. 把克隆對(duì)象淺拷貝的字段指向該對(duì)象clone得到的地址空間。
    如下代碼:
public class E implements Cloneable {
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class C implements Cloneable {
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class D implements Cloneable {
    public E e = new E();
    @Override
    public Object clone() throws CloneNotSupportedException {
        D d = (D) super.clone();
        d.e = (E) this.e.clone();
        return d;
    }
}

public class B implements Cloneable{
    public D d = new D();
    @Override
    public Object clone() throws CloneNotSupportedException {
        B b = (B) super.clone();
        b.d = (D) this.d.clone();
        return b;
    }
}

public class A extends B implements Cloneable {
    private static String TAG = "aaaaa";
    private int count = 100;
    public C c = new C();
    @Override
    public Object clone() throws CloneNotSupportedException {
        A a = (A) super.clone();
        a.c = (C) this.c.clone();
        return a;
    }
    public static void main(String[] args) throws CloneNotSupportedException{
        System.out.println("main");
        A a = new A();
        A a1 = (A) a.clone();
        System.out.println("a  : " + a.toString());
        System.out.println("a1 : " + a1.toString());
        System.out.println("a.c  : " + a.c);
        System.out.println("a1.c : " + a1.c);
    }
}
//打印信息
a  : com.example.test.A@723279cf
a1 : com.example.test.A@10f87f48
a.c  : com.example.test.C@b4c966a
a1.c : com.example.test.C@2f4d3709
深拷貝

以上代碼實(shí)現(xiàn)了深拷貝,每一個(gè)可變字段都調(diào)用clone方法產(chǎn)生一個(gè)最新的對(duì)象,每個(gè)類都實(shí)現(xiàn)了Cloneable接口并且覆蓋了public Object clone()方法。

淺拷貝

將上述代碼稍微修改就可以變成淺拷貝

public class A extends B implements Cloneable {
    private static String TAG = "aaaaa";
    private int count = 100;
    public C c = new C();
    @Override
    public Object clone() throws CloneNotSupportedException {
        A a = (A) super.clone();
 //如果類中的可變字段沒(méi)有調(diào)用如下代碼,那么這個(gè)類就是淺拷貝
//        a.c = (C) this.c.clone();
        return a;
    }
    public static void main(String[] args) throws CloneNotSupportedException{
        System.out.println("main");
        A a = new A();
        A a1 = (A) a.clone();
        System.out.println("a  : " + a.toString());
        System.out.println("a1 : " + a1.toString());
        System.out.println("a.c  : " + a.c);
        System.out.println("a1.c : " + a1.c);
    }
}
//打印信息如下:
a  : com.example.test.A@6e0be858
a1 : com.example.test.A@61bbe9ba
a.c  : com.example.test.C@610455d6
a1.c : com.example.test.C@610455d6

以上是淺拷貝,在A類的public Object clone()方法中沒(méi)有對(duì)可變對(duì)象C進(jìn)行拷貝,導(dǎo)致打印的信息中C對(duì)象的地址是相同的。

代替clone的方案

寫代碼很少覆蓋Cloneable接口,說(shuō)兩種代替方案

  1. 拷貝構(gòu)造器
    例如HashMap類的構(gòu)造方法public HashMap(Map<? extends K, ? extends V> map);就是一個(gè)拷貝構(gòu)造器
  2. 拷貝工廠方法
    例如public static Yum newInstance(Yum yum);

最好不要覆蓋Cloneable接口,使用clone方法。

第12條:考慮實(shí)現(xiàn)Comparable接口

實(shí)現(xiàn)這個(gè)接口主要是為了對(duì)象之間的排序,Java平臺(tái)類庫(kù)中的所有“值類”都實(shí)現(xiàn)了這個(gè)接口。
規(guī)則:
將這個(gè)對(duì)象與指定對(duì)象進(jìn)行比較。當(dāng)該對(duì)象小于、等于、大于指定對(duì)象的時(shí)候,分別返回負(fù)數(shù)、零、正整數(shù)。如果由于指定對(duì)象的類型而無(wú)法與該對(duì)象進(jìn)行比較,則拋出ClassCastException異常。
如下代碼:

public class PhoneNumber implements Comparable<PhoneNumber> {
    public final int area;
    public final int phone;
    public PhoneNumber(int area, int phone) {
       this.area = area;
       this.phone = phone;
    }
    @Override
    public int compareTo(@NonNull PhoneNumber pn) {
        if (area < pn.area) {
            return -1;
        }
        if (area > pn.area) {
            return 1;
        }
        if (phone < pn.phone) {
            return -1;
        }
        if (phone > pn.phone) {
            return 1;
        }
        return 0;
    }
    @Override
    public String toString() {
        return "PhoneNumber{" +
                "area=" + area +
                ", phone=" + phone +
                '}';
    }
    public static void main(String[] args) throws CloneNotSupportedException {
        PhoneNumber[] phoneNumbers = {
                new PhoneNumber(100,300),
                new PhoneNumber(200,500),
                new PhoneNumber(50,10),

        };
        List<PhoneNumber> phoneNumberList = Arrays.asList(phoneNumbers);
        System.out.println(phoneNumberList);
        Collections.sort(phoneNumberList);
        System.out.println(phoneNumberList);
    }
}
打印日志
[PhoneNumber{area=100, phone=300}, PhoneNumber{area=200, phone=500}, PhoneNumber{area=50, phone=10}]
[PhoneNumber{area=50, phone=10}, PhoneNumber{area=100, phone=300}, PhoneNumber{area=200, phone=500}]
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容