Object 類中的equals

先看幾個基礎的東西:
SDK: Software Development Kit, 軟件開發工具包;
JDK: Java Development Kit,java開發工具包;
JRE: Java Runtime Environment,java運行時環境;
對於JRE,在安裝JDK的時候會提示是否再安裝一個JRE,安裝完了之後會發現有兩個JRE目錄,一個是在jdk內部目錄中,一個是後來提示安裝的目錄。其實這兩個是不衝突的,內部的JRE是運行java內部程序的環境,其本身的程序也是用JAVA編寫的,所以它也需要一個運行環境,而後來安裝的JRE就屬於外部的運行環境了,兩個運行環境並不衝突,使用哪個都沒有關係。
JDK目錄:

  • bin 存放的是一些可執行的.exe程序及一些.dll文件,比如:java.exe,javac.exe等。
  • include 語言語言頭文件 支持 用Java本地接口和Java虛擬機接口 來本機代碼編程
  • lib 包目錄,有着兩個比較重要的工具包,dt.jar和tools.jar。
  • jre 運行環境的根目錄

    • lib java運行時所要的代碼包及一些資源文件,其中重要的有rt.jar ,java核心類庫,運行時的基礎類庫。可以平時寫代碼所用到的java類幾乎都出自這個jar包,包括下面講的Object類。
      -bin 這個目錄裏也是一些可執行的.exe文件和.dll文件,一個client目錄和server目錄,裏面放的分別就是我們所熟悉的客戶端和服務器版的jvm了。

    Object 類存在於java.lang包中,是java所有類的基類,雖然說沒有在代碼層上顯示的extends Object,但是在類創建的時候是默認其基類就是Object類。
    jdk1.7中的Object源碼中,首先其構造器:

 public Object()
    {
    }

然後是一些個本地化的方法

private static native void registerNatives();

public final native Class getClass();

public native int hashCode();

public final native void notify();

public final native void notifyAll();

public final native void wait(long l)
    throws InterruptedException;

protected native Object clone()
        throws CloneNotSupportedException;

靜態代碼塊,先註冊上述的一些本地方法。

    static 
    {
        registerNatives();
    }
}

接一下就是一些普通的方法了,我們先看toString()方法:

public String toString()
    {
        return (new StringBuilder()).append(getClass().getName()).append("@").append(Integer.toHexString(hashCode())).toString();
    }

該方法是把一個對象轉換爲字符串,基本思路就是強制類型轉換,將其轉換爲StringBuilder非線程安全的字符集對象,再將其類名+@+hashCode值

接下來看兩個wati方法

 public final void wait(long l, int i)
        throws InterruptedException
    {
        if(l < 0L)
            throw new IllegalArgumentException("timeout value is negative");
        if(i < 0 || i > 999999)
            throw new IllegalArgumentException("nanosecond timeout value out of range");
        if(i >= 500000 || i != 0 && l == 0L)
            l++;
        wait(l);
    }

    public final void wait()
        throws InterruptedException
    {
        wait(0L);
    }

在前面也還一個帶一個參數的本地化wait(long l) 方法,所以在Object類中共有三個wait方法,wait(),wait(long l),wait(long l,int i)。不同於equals和toString,這兩個普通的wait方法是final的,也就是說它是不能覆蓋的。wait方法實用於多線程的程序之中,其作用是暫停當前線程的執行,直到接到有信息(notify)來終止這個暫停。具體的多線程細節這裏就先不介紹了。

protected void finalize()
        throws Throwable
    {
    }

Java 技術允許使用 finalize() 方法在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。這個方法是由垃圾收集器在確定這個對象沒有被引用時對這個對象調用的。它是在 Object 類中定義的,因此所有的類都繼承了它。子類覆蓋 finalize() 方法以整理系統資源或者執行其他清理工作。finalize() 方法是在垃圾收集器刪除對象之前對這個對象調用的。

接下來我們重點看equals 方法:

 public boolean equals(Object obj)
    {
        return this == obj;
    }

這個方法也很簡單,就是把this和傳進來的這個對象引用比較一下,返回比較結果。但是在很多的時候我們是需要覆蓋這個方法的,在覆蓋equals方法的時候,必須要遵守它的通用約定:

  • 自反性。對於任何非null的引用值x,x.equals(x)必須返回true。
  • 對稱性。對於任何非null的引用值x和y,當且僅當x.equals(y)返回true,y.equals(x)也必須返回true.
  • 傳遞性。對於任何非null的引用值x,y,z,如果x.equals(y)返回true,並且y.equals(z)也返回true,那麼x.equals(z)也必須返回true。
  • 一致性。對於任何非null的引用值x和y,只要equals的比較操作在對象中所用的信息沒有被修改,多次調用x.equals(y)就會一致地返回true,或者一致地返回false。
  • 對於任何非null的引用值x,x.equals(null)必須返回false。

在對象類覆蓋equals時要注意,總是要覆蓋hashCode方法
因沒有覆蓋hashCode而違反的關鍵約定是:相等的對象必須具有相等的散列碼
舉個栗子:

public 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 void rangeCheck(int arg, int max, String name) {
        if(arg<0||arg>max){
            throw new IllegalArgumentException(name +":"+arg);
        }
    }
        @Override
    public boolean equals(Object o){
        if(o==this){
            return true;
        }
        if(!(o instanceof PhoneNumber)){
            return false;
        }
        PhoneNumber pn=(PhoneNumber) o;
        return pn.lineNumber==lineNumber 
                && pn.prefix==prefix 
                && pn.areaCode==areaCode; 
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Map<PhoneNumber,String> map=new HashMap<PhoneNumber,String>();
        PhoneNumber jenny=new PhoneNumber(707, 867,5309);
        PhoneNumber jenny_sub=new PhoneNumber(707, 867,5309);
        map.put(jenny,"Jenny");
        System.out.println(map.get(jenny_sub));//null
    }
}

對於jenny和jenny_sub它們所具有的內容完全相同,我們在業務上可認爲這兩個對象是相等的,但是上輸出的結果卻並不是我們希望的Jenny而是null。
由於PhoneNumber類沒有覆蓋hashCode方法,從而導致兩個相等的實例具有不相等的散列碼,違反了hashCode的約定。因此,put方法把電話號碼對象放在一個散列桶(hash bucket)中,get方法卻在另一個散列桶中查找這個電話號碼。即使這兩個實例正好被放在同一個散列桶中,get方法也必定會返回null,因爲HashMap有一項優化,可以將與每個項相關的散列碼緩存起來,如果散列碼不匹配,也不必檢驗對象的等同性。
修正這個問題只要爲PhoneNumber類提供一個適當的hashCode方法即可。然而一個好的散列函數通常傾向於”爲不相等的對象產生不相等的散列碼”。這正是hashCode約定中第三條的含義。理想情況下,散列函數應該把集合中不相等的實例均勻地分佈到所有可能的散列值上。下面給出一個簡單的解決辦法:

  1. 把某個非零的常數值,比如說17,保存在一個名爲result的int類型變量中。
  2. 對於對象中每個關鍵域f(指equals方法中涉及的每個域),完成以下步驟:
    1. 如果該域是boolean類型,則計算(f?1:0)。
    2. 如果該域是btye,char,short,int類型,則計算(int ) f。
    3. 如果該域是long類型,則計算(int)(f^(f>>>32))。
    4. 如果該域是float類型,則計算Float.floatToIntBits(f)。
    5. 如果該域是double類型,則計算Double.doubleToLongBits(f),然後按照步驟3,爲得到的long類型值計算散列值。
    6. 如果該域是一個對象引用,並且該類的equals方法通過遞歸地調用equals的方式來比較這個域,則同樣爲這個域遞歸地調用hashCode。如果需要更復雜的比較,則爲這個域計算一個”範式“,然後針對這個範式調用hashCode。如果這個域的值爲null,則返回0。
    7. 如果該域是一個數組,則要把每一個元數當做單獨的域來處理。也就是說,遞歸地應用上述規則,對每個重要元素計算一個散列碼,然後再下面8的方法把這些散列組合起來。如果數組域中的每個元素都很重要,可以利用Arrays.hashCode方法。
    8. 把上述計算得到的散列碼c合併到result:
      result=31*result+c;
      返回result;

所有要解決上述PhoneNumber的問題只要加上

@Override
public int hashCode(){
    int result=17;
    result=31*result+areaCode;
    result=31*result+prefix;
    result=31* result+lineNumber;
    return result;
}
System.out.println(map.get(jenny_sub));//Jenny

最後我們來看看String類覆蓋的hashCode方法

private int hash;
private final char value[];
public int hashCode()
    {
        int i = hash;
        if(i == 0 && value.length > 0)
        {
            char ac[] = value;
            for(int j = 0; j < value.length; j++)
                i = 31 * i + ac[j];

            hash = i;
        }
        return i;
    }

String本身就是一個字符數組value[],所以計算String的hashCode值要把每一個元素當做單獨的域來處理,因些得循環整個數組來計算其散列碼。但是它把散列值定義成局部變量,緩存了起來,再計算前先確定該散列值是否爲存在,在不存在(爲0)的情況下再進行計算,這種做法叫做”延遲初始化“,即要一直到hashCode被第一次調用的時候才初始化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章