【碼藝雜談】Java中的相同與不同

北風之神發表於2019-01-19

Java中有很多場景需要判斷兩個物件或者兩個值,那麼

  • 判斷是否相同的依據是什麼?
  • 如何判斷是否相同呢?

為了解釋這個問題,我們從Java語言的根說起,那Java語言的根在哪裡?我們知道Java是一種物件導向的程式語言,物件是類的例項,所有的類都隱式繼承Object類,那Object類就是所有類的父類,也就是我們所說的根。
Object類中方法不多,其中有兩個方法,一個叫equals,另一個叫hashCode;

JDK中對equals的定義是:

Indicates whether some other object is “equal to” this one.

意思是說equals方法是用來判斷兩個物件是否相同的,到這裡是不是已經得到了文章開始的問題答案了呢,其實只能說得到了三分之一的答案,再看equals方法的實現,

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

程式碼很簡單,通過“== ”判斷兩個物件是否相同;這裡就要解釋一下Java中“== ”符號的作用,文章開頭也說了Java是一種物件導向的程式語言,所以Java中全都是物件,這樣表述是否正確呢?應該算不完全正確,因為Java中還有一類基本資料型別,比如byte、short、int、long、float、double、boolean;當“== ”符號作用於基本資料型別時,其比較的是值,當==符號作用於類物件時其比較的是物件在堆記憶體的地址。

顯然Object類的equals方法適用的是類物件;也就是equals比較的是物件的堆記憶體地址,如果自定義的類不重寫Object的equals方法,那麼比較這個類的兩個物件相當於比較這兩個物件的堆記憶體地址,實際情況是,很多自定義的類物件的比較並不想通過判斷物件的堆記憶體地址判斷兩個物件是否相同,而需要定義一些符合業務需求的規則,也就是說需要重寫equals方法;比如String類就重寫了Object的equals方法,

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

說了半天都是在說equals方法,開頭還提到了hashCode方法,hashCode方法有什麼作用呢?

Object類的hashCode方法定義如下,可以看出其是一個本地方法,該方法的解釋是“Returns a hash code value for the object.”

public native int hashCode();

hashCode方法的定義中描述了其general contract:

  • 相同的物件每次執行hashCode的結果是相同的,這裡的相等是指通過equals比較相同。
  • 如果根據equals方法得到兩個物件相同,那麼這兩個物件的hashCode方法的結果也一定相同。
  • 如果根據equals方法得到兩個物件不相同,那麼兩個物件的hashCode方法的結果不一定不相同,我們可以利用這一點來提高雜湊表的效能。

hashCode方法的定義還有一句話:This method is supported for the benefit of hash tables such as those provided by {@link java.util.HashMap}.
這句話什麼意思呢?直白一點就是hashCode方法只有在雜湊表中才有作用;那什麼是雜湊表?下面是維基百科的定義:

In computing, a hash table (hash map) is a data structure that implements an associative array abstract data type, a structure that can map keys to values. A hash table uses a hash function to compute an index into an array of buckets or slots, from which the desired value can be found.

雜湊表是一個以空間換時間的資料結構,通過key查詢value,通過一個計算hash值的函式得到每個key在結構中的位置索引,通過位置索引能夠快速定位value。HashMap是Java語言中雜湊表的一種實現,下面是HashMap的hash方法和put方法的原始碼,

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

可以看到在計算key的雜湊值時,用到了key的hashCode方法。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
        ...
    }

當網map中放值時,如果key的hash值相等時,用equals方法判斷key是否相等,如果相等說明key在map中存在,則用新的value替換當前的value。說了這麼多,到底想說明什麼?我們反過來思考一下,如果沒有hashCode方法,我們判斷某個鍵是否已經存在,要通過equals方法逐個比較這個key和map中的所有key,資料量小的情況下是可以接受的,如果資料量大,這個比較的開銷是難以接受的,這就體現出了hashCode的作用,不需要和map中所有的key逐個比較,只需要比較hash值相同的即可,大大減少了比較的時間。這裡即用到了key的equals方法又用到了hashCode方法,如果自定義類按照業務邏輯重寫了equals方法,但沒有按照類似的邏輯重寫hashCode方法,key值是否重複的判斷結果就會有問題,所以一般情況下如果重寫了equals方法,一定要重寫hashCode方法。

最後回到文章開頭的問題,如何判斷兩個物件或值是否相同?這個問題其實有兩方面的含義,一方面是判斷的方法,另一方面是判斷的效率。

  • 判斷的方法:equals方法和符號“== ”,分別用於物件和基本資料型別。
  • 在需要判斷的資料量很大的情況下,用equals方法逐個比較效率是很低的,這時候hashCode方法就派上用場了,hashCode方法的定義決定了其一些特性,相同物件的hashCode方法返回的值是相等的,不同的物件的hashCode方法返回值不一定不相同,所以通過hashCode方法能夠大大縮小比較的範圍,提高比較的效率。

相關文章