關(guān)于重寫equals方法就得跟著重寫hashCode方法的原因

equals() 和 hashCode() 都是屬于Object類的方法。

先說明它們之間的關(guān)系:

若兩個對象equals(Object obj)返回true,則hashCode()有必要也返回相同的int數(shù)。

若兩個對象equals(Object obj)返回false,則hashCode()有可能是相同的int數(shù),也有可能不相同。

若兩個對象hashCode()返回相同int數(shù),則equals(Object obj)不一定返回true。

若兩個對象hashCode()返回不同int數(shù),則equals(Object obj)一定返回false。

看一下它們的源碼解釋:

equals

* Note that it is generally necessary to override the {@code hashCode}
     * method whenever this method is overridden, so as to maintain the
     * general contract for the {@code hashCode} method, which states
     * that equal objects must have equal hash codes.
     *
     * @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);
    }

這注釋說了,如果equals被重寫了,務(wù)必hashCode方法也需要被重寫,因為equals相等的兩個對象,它們的hashCode必須是相等的。這里注意equals()里頭用的是==,比較的是內(nèi)存地址。

hashCode

/**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.
     * <p>
     * The general contract of {@code hashCode} is:
     * <ul>
     * <li>Whenever it is invoked on the same object more than once during
     *     an execution of a Java application, the {@code hashCode} method
     *     must consistently return the same integer, provided no information
     *     used in {@code equals} comparisons on the object is modified.
     *     This integer need not remain consistent from one execution of an
     *     application to another execution of the same application.
     * <li>If two objects are equal according to the {@code equals(Object)}
     *     method, then calling the {@code hashCode} method on each of
     *     the two objects must produce the same integer result.
     * <li>It is <em>not</em> required that if two objects are unequal
     *     according to the {@link java.lang.Object#equals(java.lang.Object)}
     *     method, then calling the {@code hashCode} method on each of the
     *     two objects must produce distinct integer results.  However, the
     *     programmer should be aware that producing distinct integer results
     *     for unequal objects may improve the performance of hash tables.
     * </ul>
     * <p>
     * As much as is reasonably practical, the hashCode method defined by
     * class {@code Object} does return distinct integers for distinct
     * objects. (This is typically implemented by converting the internal
     * address of the object into an integer, but this implementation
     * technique is not required by the
     * Java&trade; programming language.)
     *
     * @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();

里面說hashCode()返回是一個int類型的值,是由對象的內(nèi)存地址生成而來。里面還定義了3條守則:

  1. 一個對象無論調(diào)用它的hashCode方法多少次,都會返回相同的integer(哈希值)。
  2. 兩個對象如果通過equals方法判定為相等,那么就應(yīng)當(dāng)返回相同integer(哈希值)。
  3. 兩個地址值不相等的對象調(diào)用hashCode方法不要求返回不相等的integer(哈希值),但是要求擁有兩個不相等integer的對象必須是不同對象。

hashCode主要作用于hashMap、hashSet、hashTable這些散列數(shù)據(jù)結(jié)構(gòu)中。

現(xiàn)在就拿hashMap的源碼來舉例:


可以看到,相同key替換的時候,用到邏輯是hash相同而且要equals,不然就替換失敗,而hash也要用到key的hashCode()來計算。

如果某個對象需要用到hash類型的數(shù)據(jù)結(jié)構(gòu)工具時,而又沒有重寫,就會產(chǎn)生非常嚴重的問題,當(dāng)然如果用不到,可以不寫,但最好還是寫上,因為在程序的編寫過程,不保證不會用這些hash結(jié)構(gòu)數(shù)據(jù)工具。

佐證

/**
 * 重寫equals必須要重寫hashCode
 *
 * 使用hashSet來判斷,hashSet底層還是hashMap,putVal會判斷hash和equals是否都相同,才認為是同一個元素key
 *
 * @author Administrator
 */
public class OverrideHashEqualsTs {

    public static void main(String[] args) {
        System.out.println("==========================既不重寫equals也不重寫hashCode==========================");

        HashSet<OverrideNeitherEqualsOrHash> overrideNeitherEqualsOrHash_hashSet = new HashSet<OverrideNeitherEqualsOrHash>();
        OverrideNeitherEqualsOrHash overrideNeitherEqualsOrHash_1 = new OverrideNeitherEqualsOrHash(1, "1");
        OverrideNeitherEqualsOrHash overrideNeitherEqualsOrHash_2 = new OverrideNeitherEqualsOrHash(1, "1");

        overrideNeitherEqualsOrHash_hashSet.add(overrideNeitherEqualsOrHash_1);
        overrideNeitherEqualsOrHash_hashSet.add(overrideNeitherEqualsOrHash_2);
        overrideNeitherEqualsOrHash_hashSet.add(overrideNeitherEqualsOrHash_1);

        System.out.println("overrideNeitherEqualsOrHash_1 hashCode : " + overrideNeitherEqualsOrHash_1.hashCode()
                + " , overrideNeitherEqualsOrHash_2 hashCode : " + overrideNeitherEqualsOrHash_2.hashCode()
                + ", they equals ? " + overrideNeitherEqualsOrHash_1.equals(overrideNeitherEqualsOrHash_2)
                + ", hashSet : " + overrideNeitherEqualsOrHash_hashSet
        );
        /* 結(jié)果:哈都沒重寫,該覆蓋的覆蓋
         * overrideNeitherEqualsOrHash_1 hashCode : 685325104 , overrideNeitherEqualsOrHash_2 hashCode : 460141958,
         * they equals ? false,
         * hashSet : [OverrideNeitherEqualsOrHash{age=1, name='1', hashCode='685325104},
         *            OverrideNeitherEqualsOrHash{age=1, name='1', hashCode='460141958}]
         */

        System.out.println();
        System.out.println("==========================只重寫equals不重寫hashCode==========================");

        HashSet<OverrideEqualsNotHash> overrideEqualsNotHash_hashSet = new HashSet<OverrideEqualsNotHash>();
        OverrideEqualsNotHash overrideEqualsNotHash_1 = new OverrideEqualsNotHash(1, "1");
        OverrideEqualsNotHash overrideEqualsNotHash_2 = new OverrideEqualsNotHash(1, "1");

        overrideEqualsNotHash_hashSet.add(overrideEqualsNotHash_1);
        overrideEqualsNotHash_hashSet.add(overrideEqualsNotHash_2);
        overrideEqualsNotHash_hashSet.add(overrideEqualsNotHash_1);

        System.out.println("overrideEqualsNotHash_1 hashCode : " + overrideEqualsNotHash_1.hashCode()
                + " , overrideEqualsNotHash_2 hashCode : " + overrideEqualsNotHash_2.hashCode()
                + ", they equals ? " + overrideEqualsNotHash_1.equals(overrideEqualsNotHash_2)
                + ", hashSet : " + overrideEqualsNotHash_hashSet
        );
        /* 結(jié)果:只是重寫equals沒有重寫hashCode,hashSet底層還是hashMap,putVal會判斷hash和equals是否都相同,才認為是同一個元素key
         * overrideEqualsNotHash_1 hashCode : 1163157884 , overrideEqualsNotHash_2 hashCode : 1956725890,
         * they equals ? true,
         * hashSet : [OverrideEqualsNotHash{age=1, name='1', hashCode='1956725890},
         *            OverrideEqualsNotHash{age=1, name='1', hashCode='1163157884}]
         */

        System.out.println();
        System.out.println("==========================只重寫hashCode不重寫equals==========================");

        HashSet<OverrideHashNotEquals> overrideHashNotEquals_hashSet = new HashSet<OverrideHashNotEquals>();
        OverrideHashNotEquals overrideHashNotEquals_1 = new OverrideHashNotEquals(1, "1");
        OverrideHashNotEquals overrideHashNotEquals_2 = new OverrideHashNotEquals(1, "1");

        overrideHashNotEquals_hashSet.add(overrideHashNotEquals_1);
        overrideHashNotEquals_hashSet.add(overrideHashNotEquals_2);
        overrideHashNotEquals_hashSet.add(overrideHashNotEquals_1);

        System.out.println("overrideHashNotEquals_1 hashCode : " + overrideHashNotEquals_1.hashCode()
                + " , overrideHashNotEquals_2 hashCode : " + overrideHashNotEquals_2.hashCode()
                + ", they equals ? " + overrideHashNotEquals_1.equals(overrideHashNotEquals_2)
                + ", hashSet : " + overrideHashNotEquals_hashSet
        );
        /* 結(jié)果:只是重寫hashCode沒有重寫equals,hashSet底層還是hashMap,putVal會判斷hash和equals是否都相同,才認為是同一個元素key
         * overrideHashNotEquals_1 hashCode : 80 , overrideHashNotEquals_2 hashCode : 80,
         * they equals ? false.
         * hashSet : [OverrideHashNotEquals{age=1, name='1', hashCode='80},
         *            OverrideHashNotEquals{age=1, name='1', hashCode='80}]
         */

        System.out.println();
        System.out.println("==========================既重寫equals也重寫hashCode==========================");

        HashSet<OverrideEqualsAndHash> OverrideEqualsAndHash_hashSet = new HashSet<OverrideEqualsAndHash>();
        OverrideEqualsAndHash overrideEqualsAndHash_1 = new OverrideEqualsAndHash(1, "1");
        OverrideEqualsAndHash overrideEqualsAndHash_2 = new OverrideEqualsAndHash(1, "1");

        OverrideEqualsAndHash_hashSet.add(overrideEqualsAndHash_1);
        OverrideEqualsAndHash_hashSet.add(overrideEqualsAndHash_2);
        OverrideEqualsAndHash_hashSet.add(overrideEqualsAndHash_1);

        System.out.println("overrideEqualsAndHash_1 hashCode : " + overrideEqualsAndHash_1.hashCode()
                + " , overrideEqualsAndHash_2 hashCode : " + overrideEqualsAndHash_2.hashCode()
                + ", they equals ? " + overrideEqualsAndHash_1.equals(overrideEqualsAndHash_2)
                + ", hashSet : " + OverrideEqualsAndHash_hashSet
        );
        /* 結(jié)果:既重寫equals也重寫hashCode,add進去的都當(dāng)做是相同的元素,所以都是覆蓋,hashSet最后最后一個元素
         * overrideEqualsAndHash_1 hashCode : 80 , overrideEqualsAndHash_2 hashCode : 80,
         * they equals ? true,
         * hashSet : [OverrideEqualsAndHash{age=1, name='1', hashCode='80}]
         */

    }


    static class OverrideNeitherEqualsOrHash {
        Integer age;
        String name;

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

        @Override
        public String toString() {
            return "OverrideNeitherEqualsOrHash{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    ", hashCode='" + hashCode() +
                    '}';
        }
    }

    static class OverrideEqualsNotHash {
        Integer age;
        String name;

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

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            OverrideEqualsNotHash that = (OverrideEqualsNotHash) o;

            if (age != null ? !age.equals(that.age) : that.age != null) {
                return false;
            }
            return name != null ? name.equals(that.name) : that.name == null;
        }

        @Override
        public String toString() {
            return "OverrideEqualsNotHash{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    ", hashCode='" + hashCode() +
                    '}';
        }
    }

    static class OverrideHashNotEquals {
        Integer age;
        String name;

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

        @Override
        public int hashCode() {
            int result = age != null ? age.hashCode() : 0;
            result = 31 * result + (name != null ? name.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "OverrideHashNotEquals{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    ", hashCode='" + hashCode() +
                    '}';
        }
    }

    static class OverrideEqualsAndHash {
        Integer age;
        String name;

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

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            OverrideEqualsAndHash that = (OverrideEqualsAndHash) o;

            if (age != null ? !age.equals(that.age) : that.age != null) return false;
            return name != null ? name.equals(that.name) : that.name == null;
        }

        @Override
        public int hashCode() {
            int result = age != null ? age.hashCode() : 0;
            result = 31 * result + (name != null ? name.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "OverrideEqualsAndHash{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    ", hashCode='" + hashCode() +
                    '}';
        }
    }
}

結(jié)論:

在使用hashSet、hashMap這種hash結(jié)構(gòu)的數(shù)據(jù)工具時,放置的key如果重寫了equals就必須重寫hashCode,只有這樣才會視為同一個key,不違反對象相等必須有相同的hashCode的原則。

關(guān)于hashMap內(nèi)存泄露的問題

皆因都是因為hashCode和equals重寫的原因,改變了對象的屬性就改變了其hashCode,導(dǎo)致找不到桶位,get出來自然就是null,然后原來位置的數(shù)據(jù)并沒有被清除,造成內(nèi)存泄露。

代碼例子
/**
 * 測試下改變對象是否會改變其hashCode
 *
 * @author Administrator
 */
public class HashCodeWillChangeTs {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        Object value = new Object();

        Integer integer = new Integer(5);
        //integer重寫hashCode方法,hashCode就等于value
        System.out.println(integer.hashCode()); // 5

        hashMap.put(integer, value);
        System.out.println(hashMap);//5

        integer = 128;
        System.out.println(integer.hashCode()); // 128,integer是不可變類,這樣等于指向另外一個對象
        System.out.println(hashMap.get(integer));// null,已經(jīng)換了對象了,肯定找不出來了,內(nèi)存泄露了
        hashMap.clear();

        System.out.println("=======================");

        //數(shù)組是可變對象
        String[] strAy = new String[]{"1", "2", "3"};
        System.out.println(strAy.hashCode()); //460141958
        hashMap.put(strAy, value);
        hashMap.forEach((k, v) ->System.out.println(Arrays.toString((String[])k) + "--" + v));//[1, 2, 3]--java.lang.Object@28d93b30

        strAy[2] = "44";
        System.out.println(strAy.hashCode()); //460141958,strAry指向堆中一塊內(nèi)存,內(nèi)存地址一直沒變,變的是那塊內(nèi)存上的數(shù)據(jù)
        System.out.println(hashMap.get(strAy));//java.lang.Object@28d93b30,依然能拿出來,它的hashCode、equals本質(zhì)沒有重寫過。

        hashMap.clear();
        System.out.println("=======================");

        //沒有重寫hashCode和equals的自定義類
        DemoClazz demoClazz = new DemoClazz(4, "demoClazz");
        System.out.println(demoClazz.hashCode());//460141958
        hashMap.put(demoClazz,value);
        System.out.println(hashMap);//{DemoClazz{age=4, name='demoClazz'}=java.lang.Object@28d93b30}

        demoClazz.setName("fakeDemoClazz");
        System.out.println(demoClazz.hashCode());//460141958,和上面同樣的道理
        System.out.println(hashMap.get(demoClazz));//java.lang.Object@28d93b30

        hashMap.clear();
        System.out.println("=======================");

        //只重寫hashCode沒有重寫equals的自定義類
        DemoClazzOverHashCode demoClazzOverHashCode = new DemoClazzOverHashCode(99, "demoClazzOverHashCode");
        System.out.println(demoClazzOverHashCode.hashCode());//822272993
        hashMap.put(demoClazzOverHashCode,value);
        System.out.println(hashMap);//{DemoClazzOverHashCode{age=99, name='demoClazzOverHashCode'}=java.lang.Object@28d93b30}

        demoClazzOverHashCode.setName("fakeDemoClazzOverHashCode");
        System.out.println(demoClazzOverHashCode.hashCode());//-617106548,hashCode已經(jīng)變化
        System.out.println(hashMap.get(demoClazzOverHashCode));//null,找不到桶的位置了,造成內(nèi)存泄露

        hashMap.clear();
        System.out.println("=======================");

        //既重寫hashCode也重寫equals的自定義類
        DemoClazzOverHashAndEquals demoClazzOverHashAndEquals = new DemoClazzOverHashAndEquals(88, "demoClazzOverHashAndEquals");
        System.out.println(demoClazzOverHashAndEquals.hashCode());//-962172025
        hashMap.put(demoClazzOverHashAndEquals,value);
        System.out.println(hashMap);//{DemoClazzOverHashAndEquals{age=88, name='demoClazzOverHashAndEquals'}=java.lang.Object@28d93b30}

        demoClazzOverHashAndEquals.setName("fakeDemoClazzOverHashAndEquals");
        System.out.println(demoClazzOverHashAndEquals.hashCode());//-1142444356,hashCode已經(jīng)變化
        System.out.println(hashMap.get(demoClazzOverHashAndEquals));//null,找不到桶的位置了,造成內(nèi)存泄露
    }

    static class DemoClazz {
        private Integer age;
        private String name;

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

        public void setAge(Integer age) {
            this.age = age;
        }

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

        @Override
        public String toString() {
            return "DemoClazz{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    static class DemoClazzOverHashCode {
        private Integer age;
        private String name;

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

        public void setAge(Integer age) {
            this.age = age;
        }

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

        @Override
        public int hashCode() {
            int result = age.hashCode();
            result = 31 * result + name.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "DemoClazzOverHashCode{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    static class DemoClazzOverHashAndEquals{
        private Integer age;
        private String name;

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

        public void setAge(Integer age) {
            this.age = age;
        }

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

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            DemoClazzOverHashAndEquals that = (DemoClazzOverHashAndEquals) o;

            if (!age.equals(that.age)) return false;
            return name.equals(that.name);
        }

        @Override
        public int hashCode() {
            int result = age.hashCode();
            result = 31 * result + name.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "DemoClazzOverHashAndEquals{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
}

關(guān)于盡量使用不可變對象作為hashMap等hash結(jié)構(gòu)數(shù)據(jù)工具的key

因為不可變對象,如integer、string等,一般都重寫了hashCode和equals方法。變換了值,那hashCode肯定就是不同了,也get不出東西來。

如果一定要使用可變對象作為key,就需要保證該對象的屬性發(fā)生改變時,不會改變對象的hashcode值。

參考:
hashCode和equals方法的區(qū)別與聯(lián)系
重寫equals就得hashCode方法原因的探究

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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