Java裏 equals 和 == 以及 hashcode

本文探討的是老掉牙的基礎問題,先建個實體類

package com.demo.tools;

public class User {

    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

測試:

    public static void main(String[] args){
        User a = new User("A");
        User b = new User("A");
        System.out.println(a.equals(b));
    }

輸出 false。明明存的都是A,爲啥equal的結果是false呢?不是說【equal比較的是值,==比較的是地址】嗎?因爲默認創建的類是繼承Object的,看一下Object類裏面的兩個方法:

    /**
     * 返回對象的哈希碼值。
     *
     * {@code hashCode}的一般約定爲:
     * 在Java應用程序執行期間對同一對象多次調用{@code hashcode}方法時,只要在對象{@code equals}的比較中使用的信息沒有被修改,則該方法必須一致地返回相同的整數。
     * 從應用程序的一次執行到同一應用程序的另一次執行,此整數不必保持一致。
     * 
     * 如果調用{@code equals(object)}方法,兩個對象相等,那麼兩個對象調用{@code hashcode}方法必須產生相同的整數結果。
     * 根據{@link java.lang.object equals(java.lang.object)}方法,如果兩個對象不相等,則這兩個對象調用{@code hashcode}方法都必須產生不同的整數結果。
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.lang.System#identityHashCode
     */
    public native int hashCode();

    /**
     * {@code equals}方法在非空對象引用上實現等價關係:
     *
     * 它是自反的:對於任何非空的引用值{@code x},{@code x.equals(x)}應該返回{@code true}。
     * 它是對稱的:對於任何非空的引用值{@code x}和{@code y},{@code x.equals(y)}應該返回{@code true};反過來也應該一樣{@code y.equals(x)}返回{@code true}。
     * 它是可傳遞的:對於任何非空的引用值{@code x}、{@code y}和{@code z},如果{@code x.equals(y)}返回{@code true},{@code y.equals(z)}返回{@code true},則{@code x.equals(z)}應返回{@code true}。
     * 它是一致的:對於任何非空的引用值{@code x}和{@code y},多次調用{@code x.equals(y)}應該一致的返回{@code true}或{@code false},前提是在{@code equals}比較的對象信息沒有被修改。
     * 對於任何非空引用值{@code x},{@code x.equals(null)}應返回{@code false}。
     *
     *
     * 對於任何非空的引用值{@code x}和{@code y},當且僅當{@code x}和{@code y}引用同一對象,此方法返回{@code true}【此時{@code x==y}的值是{@code true}】。
     *
     * 注意,通常需要重寫{@code hashcode}方法,以便保持{@code hashcode}方法的一般約定,它聲明:相等的對象必須有相等的哈希代碼。
     *
     * @param   obj   the reference object with which to compare.
     * @return  {@code true} if this object is the same as the obj
     *          argument; {@code false} otherwise.
     * @see     #hashCode()
     * @see     java.util.HashMap
     */
    public boolean equals(Object obj) {
        return (this == obj);
    }

可以看到: 1. 默認的hashcode方法是個本地方法,也就是對象的內存地址。 2. 而equals方法則是直接使用了==操作符,也是比較內存地址。這樣看來,equals和hashcode是一樣的。

而根據註釋所說: 1. 只要equals返回true,那麼hashcode一定相等。 2. 只要equals返回false,那麼hashcode一定不相等。3. 重寫了equals方法,也要重寫hashcode方法,因爲相等的對象必須有相同的哈希碼。

嘗試一:僅僅重寫equals

package com.demo.tools;

public class User {

    public static void main(String[] args){
        User a = new User("A");
        User b = new User("A");
        System.out.println(a.equals(b));
        System.out.println(a == b);
        System.out.println(a.hashCode() + "," + b.hashCode());
    }

    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        User user = (User) obj;
        return this.name.equals(user.name);
    }
}

輸出:

 

 可以看到,值比較成功了,可是hashcode不一樣,這裏的hashcode仍然是內存地址。這就違反了約定。

嘗試二:重寫equals和hashcode方法

package com.demo.tools;

public class User {

    public static void main(String[] args){
        User a = new User("A");
        User b = new User("A");
        System.out.println(a.equals(b));
        System.out.println(a == b);
        System.out.println(a.hashCode() + "," + b.hashCode());
    }

    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        User user = (User) obj;
        return this.name.equals(user.name);
    }

    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
}

輸出:

 

 可以看到,值和hashcode一致了,地址不一樣很正常,因爲這是兩個對象。

嘗試三:hashcode相同,equals就相等嗎?

    public static void main(String[] args){
        User a = new User("Aa");
        User b = new User("BB");
        System.out.println(a.equals(b));
        System.out.println(a == b);
        System.out.println(a.hashCode() + "," + b.hashCode());
    }

輸出:

 可以看到,兩個不同的字符串,hashcode可能一樣。

源碼一:Map裏的equals方法和hashcode方法

    public boolean equals(Object o) {
        // 地址一樣,返回true
        if (o == this)
            return true;

        // 不是Map的子類,返回false
        if (!(o instanceof Map))
            return false;
        Map<?,?> m = (Map<?,?>) o;
        // 大小不同,返回false
        if (m.size() != size())
            return false;

        try {
            // 遍歷Map內容,一一比較
            Iterator<Map.Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
                Map.Entry<K,V> e = i.next();
                K key = e.getKey();
                V value = e.getValue();
                // HashMap裏允許存儲null值
                if (value == null) {
                    // 對方不存在此key,返回false
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
                    // 都有此key,但是值不一樣,返回false
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
        } catch (ClassCastException unused) {
            return false;
        } catch (NullPointerException unused) {
            return false;
        }

        return true;
    }

這裏能回答一個問題:Map中的對象是自定義的,要不要重寫對象的equals方法?

看源碼我們能知道,默認Map裏的equals方法,在內部僅僅調用了value的equals方法,默認比較的是內存地址。在調用Map的equals方法時:

1. 默認一一比較對象的地址,也就是說,兩個Map中每一組比較的對象,都引用同一個地址,那纔算兩個Map相等。

2. 如果我們業務規定了相等的判定,那麼默認的則不符合實際需求,所以要重寫value的equals方法,不然調用Map的equals方法可能不符合預期,既然重寫了equals,那麼也要重寫hashcode了。

    public int hashCode() {
        int h = 0;
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext())
            h += i.next().hashCode();
        return h;
    }

不難看出,Map的hashcode方法是把內部元素的hashcode加在一起並返回。

源碼二:List的equals方法和hashcode方法

    public boolean equals(Object o) {
        // 本身比較,返回true
        if (o == this)
            return true;
        // 非List的子類,返回false
        if (!(o instanceof List))
            return false;

        ListIterator<E> e1 = listIterator();
        ListIterator<?> e2 = ((List<?>) o).listIterator();
        // 當兩個迭代器的hasNext都爲true的時候,進行比較
        while (e1.hasNext() && e2.hasNext()) {
            E o1 = e1.next();
            Object o2 = e2.next();
            // 這裏很巧妙,利用三目運算符完成值的比較
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        // 當任意一方hasNext爲true,返回false;當雙方都爲false,返回true
        return !(e1.hasNext() || e2.hasNext());
    }

基本邏輯和Map的一樣,都是遍歷元素進行比較。hashcode也是一樣。

    public int hashCode() {
        int hashCode = 1;
        for (E e : this)
            hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
        return hashCode;
    }

源碼三:String的equals方法和hashcode方法

    public boolean equals(Object anObject) {
        // 地址一樣,返回true
        if (this == anObject) {
            return true;
        }
        // 類型必須是String
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            // 比較兩個String的長度
            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;
    }

hashcode

    private final char value[];
    private int hash; // Default to 0
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
            // 這裏並不是把元素的hashcode加在一起,而是把元素的ASCII碼加在一起
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

總結

不管平時有沒有重寫過equals和hashcode方法,看了Map和Set的源碼就可以知道重寫的必要性。因爲Set也是基於Map實現的(是一個key不一樣,value全一樣的Map),所以只用Map舉證即可。

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }


    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;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

首先值的位置是根據hashcode決定的,然後判斷值相等的條件是equals爲true,hashcode相等。其它都爲不相等,有一種情況就是equals爲true,hashcode不相等,如果插入Set集合,就不能達到去重的效果:

package com.demo.tools;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;


public class Main {

    public static void main(String[] args) throws Exception {
        Set<Foo> set  = new HashSet<>();
        Foo a = new Foo("A", 12);
        Foo b = new Foo("A", 13);
        set.add(a);
        set.add(b);
        set.forEach(e -> System.out.println(e.name));
        System.out.println(a.equals(b));
        System.out.println(a.hashCode() + "," + b.hashCode());
    }
    
    static class Foo {
        private String name;
        private Integer age;

        public Foo(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Foo foo = (Foo) o;
            // 這裏我們認爲name一樣,則相等
            return Objects.equals(name, foo.name);
        }
    }
    
}

equals爲true,但是hashcode不一樣:

 

所以,如果重寫了equals,一定要重寫hashcode,以避免不必要的問題。

小技巧:IDEA裏可以自動生成兩個方法

快捷鍵:Alt+Insert

 

 

 標準equals和hashcode,值得借鑑

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Foo foo = (Foo) o;
    return Objects.equals(name, foo.name) &&
            Objects.equals(age, foo.age);
}

@Override
public int hashCode() {

    return Objects.hash(name, age);
}

 

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