Java HashCode & equals

本文详细探讨了Java中HashCode和Equals的概念及其关系。讲解了Object对象的hashCode()和equals()方法默认行为,强调了在重写equals时应同时重写hashCode()的重要性。还分析了HashCode在ArrayList、HashSet、HashMap等集合中的应用,以及其在内存管理和GC中的影响。此外,文章提到了未正确处理hashCode和equals可能导致的内存泄漏问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 HashCode是什么?

Hashcode简单来讲就是一个散列值或者说一串整数值。Java为啥要设计一个HashCode值?

1.1 Object对象的hashCode()函数 

我们都知道Object类默认包含了:hashCode() 方法。 

 是一个native类型的方法(JNI的c语言实现的方法),默认返回:对象存储的地址。也就是说java中的任何对象,都会有一个hashCode方法,然后返回的结果就是对象存储的地址。

1.2 Object对象的equals()函数

equals默认比较的就是对象的内存地址(也就是说是不是同一个对象);

1.3 hashCode的使用场景

ArrayList、HashSet、HashMap等等,以HashMap为例,当我们使用get方法获取一个对象的时候,比如:map.get("name"),是不是要一个一个集合中的元素做比对,然后确认是否等于name,如果等于name那么就返回相应的对象,如果这个map有1w个Key,那么意味着最大的可能性是遍历1w次找到,那么有没有更快的方式呢?????? ,登场的就是hashCode了,为每个对象都设置一个hashCode的值,然后将hashCode相同的值,分配到同一个区域,如果hashCode相同,那么就可以找到一个区域,再到这个区域内进行遍历查找元素,这就是hashCode存在的最直接的原因,为了:提高搜索效率。如下面的HashMap代码,getNode方法就是先找hashCode相同的槽,如果找不到就代表key不存在,如果存在了,再到对应的槽内找key相同的元素。

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

1.4 HashCode的作用

 从上述例子是不是就知道了,HashCode的作用是啥?简单来说就是为了提高检索效率的,没有hashCode的设计,导致的结果就是o(n)的检索效率。

2 HashCode 与Equals使用

应该从equals方法角度来阐述它们之间的关系,equals方法是用来比较对象是否相等的,默认实现就是判断是否是同一个对象。那么有那些equals的场景,同时对hashCode使用的要求是怎样的?

2.1 对象的比较

默认情况下的equals是==的作用,因此p1.equals(p2) 为false

修改equals方法,确保相等:(修改了equals让name和age相等那说明对象相等)

2.2 基础类型的比较

int n1 = 1;
Integer n2 = 1;
System.out.println(n2.equals(1)); // true

上面的为啥会相等呢,是因为Integer类型重写了equals方法:

2.3 集合中的使用

在上述的对象equals和基本数据类型的equals你会发现,我们并没有出来hashCode,因为实际的场景在单独使用对象和基本数据类型的时候,其实hashCode作用不大,真正要使用到hashCode的时候,是在集合中。

2.3.1 List & Set 集合

(1)如果我们不修改hashCode,也就是默认是对象存储地址,那么p1, p2做set存储的时候当成了2个对象来存储,因为hashCode不同。

(2)如果我们重写他们的hashCode()方法,你会发现hashCode相同的话,size=1而不是2,因为去除了重复,因为hashSet的本质实现是一个hashMap,对应的key就是集合的元素,进行add操作的时候,会进行hashCode的判断,如果hashCode相同那么就是存在某个值就更新某个值。

public class HashCodeEqualsTest {
    static class Person {

        public String name;
        public int age;

        public Person(String name, int 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;
            Person person = (Person) o;
            return age == person.age && Objects.equals(name, person.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
    }

    public static void main(String[] args) {
        Person p1 = new Person("wang", 23);
        Person p2 = new Person("wang", 23);
        System.out.println(p1.equals(p2)); // true

        List<Person> pList = new ArrayList<>();
        pList.add(p1);
        pList.add(p2);
        System.out.println(pList.size() == 2); // true

        Set<Person> sets = new HashSet<>();
        sets.add(p1);
        sets.add(p2);
        System.out.println(sets.size() == 2); // false
    }

}

2.3.2 HashMap等集合

Map<Person, Person> maps = new HashMap<>();
maps.put(p1, p1);
maps.put(p2, p2);
maps.put(p1, p1);
System.out.println(maps.size() == 1);

上述size=1,因为key会进行hash,p1和p2经过重写hashCode和equals之后,对象是等价于是同一个,也就会出现size=1;

2.4 hashCode与equals的关系

2.4.1 重写equals一定要重写hashCode(为啥)

重写equals的时候最好一起重写hashCode,并且保证:equals相等、hashCode相等;

(1)同一个对象的话,那么hashCode肯定相等,这个不用过多说明;

(2)修改equals不修改hashCode 以及 修改equals修改不一致的hashCode

public class HashCodeEqualsTest {

    static class Person {

        public String name;
        public int age;

        public Person(String name, int 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;
            Person person = (Person) o;
            return age == person.age && Objects.equals(name, person.name);
        }

        @Override
        public int hashCode() {
            return new Random().nextInt();
        }
    }

    public static void main(String[] args) {
        Person p1 = new Person("wang", 23);
        Person p2 = new Person("wang", 23);

        HashSet<Person> sets = new HashSet<>();
        sets.add(p1);
        sets.add(p2);
        System.out.println(sets.size() == 2); // true

    }

}

你会发现为啥size=2,不是1呢,是因为hashCode不同,set会先检查hash然后再处理value,因此会出现数据不一致的情况。(不修改hashCode,其实等于修改了不同的hashCode返回值)

2.4.2 hashCode相等,equals不一定相等

以hashMap的数据结构描述更加清楚,我们hashCode的目的是为了提高搜索效率,让我们减少查询的次数,如果hashCode相等,只能说明他们在同一个槽位,然后在同一个槽位对应的数组或者链表进行遍历查询。(这个做过多的描述)

2.4.3 内存泄漏

public class HashCodeEqualsTest {

    static class Person {

        public String name;
        public int age;

        public Person(String name, int 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;
            Person person = (Person) o;
            return age == person.age && Objects.equals(name, person.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
    }

    public static void main(String[] args) {
        Person p1 = new Person("wang", 23);
        Person p2 = new Person("wang", 24);
        Person p3 = new Person("wang", 25);

        HashSet<Person> sets = new HashSet<>();
        sets.add(p1);
        sets.add(p2);
        sets.add(p3);
        p3.age = 26;
        System.out.println(sets.size() == 3); // true
        sets.remove(p3);
        System.out.println(sets.size() == 3); // true
    }

}

为啥删除后不是size=2呢,因为p3.age=26修改了值,会导致hashCode()计算的时候,hashCode发生变化,然后我们在add元素的时候,实际上hashCode是改变之前的(25),这样你去删除的时候,就找不到这个元素,然后就删除不了元素,这样内存就泄漏了。

3 GC与hashCode的关系

hashCode方法默认情况下是对象的存储地址,但是当gc之后,地址会发生变化,意味着就是hashCode会发生变化,那么hashCode变化之后会不会导致问题呢?

3.1 默认hashCode方法的约定

大致意思如下:

(1) 在Java应用程序的执行过程中,每当在同一对象上多次调用hashCode方法时,只要没有修改对象上的equals比较中使用的信息,hashCode方法必须始终返回相同的整数。从应用程序的一次执行到同一应用程序的另一次执行,该整数不必保持一致。

(2)A.equals(B) == true,那么A.hashCode() == B.hashCode();

(3)A.equals(B) != true,那么A.hashCode() 与 B.hashCode()可以不相等,但是尽量保证不相等,因为可以减少碰撞,提高搜索效率;

有了这些保证,你才会发现,如果gc了hashCode地址变化了,首次调用hashCode()方法的时候会被写入到对象的ObjectHeader中,然后后续再调用就会直接返回对象头的hashCode,这样你的地址变了,但是你的hashCode值还是没有变。

4 引用

[1] https://blog.youkuaiyun.com/a745233700/article/details/83186808

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值