为什么重写 equals 方法后还需要重写 hashCode 方法

下面我们先看 一下 Object 类的 equals 方法和 hashcode 方法源码:

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

从代码中我们知道,创建的对象在不重写的情况下使用的是 Object 的 equals 方法和 hashcode 方法,从 Object 类的源码我们知道,默认的 equals 判断的是两个对象的引用指向的是不是同一个对象,而 hashcode 也是根据对象地址生成一个整数数值。

重写 equals 方法

假如我们创建了一个 Student 类

public class Student {
	private Integer age;
	private String name;

	public Student() {
		
	}
	public Integer getAge() {
		return age;
	}

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

	public String getName() {
		return name;
	}

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

}
	public static void main(String[] args) {

		Student student = new Student();

		student.setName("张三");

		student.setAge(18);


		Student student2 = new Student();

		student2.setName("张三");

		student2.setAge(18);

		System.out.println("student.equals(student2)=" + student.equals(student2));
	}

运行上面的代码发现两个 new 出来的 Student() 对象,无论他们的的各项值是否一样两个对象 equals 永远都是 false。

其实原因就是我们没有重写 Student 的 equals 方法,默认会调用 Objec t的 equals 方法,Object 的 equals 方法是比较对象的引用对象是否是同一个,两个 new 出来的对象肯定是不一样的。

现在我们需要两个对象的各个属性值一样的,就认为这两个对象是相等的;那么此时我们就需要重写 equals 方法。

public class Student {
	private Integer age;
	private String name;

	public Integer getAge() {
		return age;
	}

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

	public String getName() {
		return name;
	}

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

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Student) {
			Student user = (Student) obj;
			if (user.getName().equals(this.name) && user.getAge() == this.age) {
				return true;
			} else {
				return false;
			}
		} else {
			return false;
		}

	}
}
	public static void main(String[] args) {

		Student student = new Student();

		student.setName("张三");

		student.setAge(18);


		Student student2 = new Student();

		student2.setName("张三");

		student2.setAge(18);

		System.out.println("user1.equals(user2)=" + student.equals(student2));
	}

最终的结果会返回 true。

在我们重写 equals 方法以后结果是为 true,如果这时把他们分别放入 Map 集合中会怎么样呢?

	public static void main(String[] args) {

		Student student = new Student();

		student.setName("张三");

		student.setAge(18);

		Student student2 = new Student();

		student2.setName("张三");

		student2.setAge(18);

		Map map = new HashMap();

		map.put(student, "student");

		map.put(student2, "student2");


		System.out.println("map 的长度为:" + map.keySet().size());
	}

在这里插入图片描述

现在问题来了,明明 student 和 student2 两个对象重写 equals 为 true,那么为什么把他们放到 Map 中会有两个对象,原因就在于 Map 把两个同样的对象当成了不同的 Key。

导致这个问题的原因是 student 和 student2 的 hashcode 不一样导致的,因为我们没有重写父类(Object)的 hashcode 方法,Object 的 hashcode 方法会根据两个对象的地址生成对相应的 hashcode。

重写 hashcode 方法

下面我们扩展上面的代码,重写 hashcode 方法:

public class Student {
	private Integer age;
	private String name;

	public Integer getAge() {
		return age;
	}

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

	public String getName() {
		return name;
	}

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

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Student) {
			Student user = (Student) obj;
			if (user.getName().equals(this.name) && user.getAge() == this.age) {
				return true;
			} else {
				return false;
			}
		} else {
			return false;
		}

	}

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

	}
}

在这里插入图片描述

当我们重写 hashcode 方法以后,map 中的数量变成了 1。

这里既然说到了 HashMap ,那么对 HashMap 补充一点,就对于 equals 和 hashcode 在 HashMap 中怎么实现的进一步讲解一下。

HashMap 相关

jdk1.7 中 HashMap 是由数组和链表组成,那么对象是怎么存储的呢?

put(K key, V value)

public V put(K key, V value) {  
    //如果 table 引用指向成员变量 EMPTY_TABLE,那么初始化 HashMap
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    //若 key 为 null,则将该键值对添加到 table[0] 处,遍历该链表,如果有 key 为 null,则将 value 替换。没有就创建新 Entry 对象放在链表表头
    //所以 table[0] 的位置上,永远最多存储 1 个 Entry 对象,形成不了链表。key 为 null 的 Entry 存在这里 
    if (key == null)  
        return putForNullKey(value);  
    //若 key 不为 null,则计算该 key 的哈希值
    int hash = hash(key);  
    //搜索指定 hash 值在对应 table 中的索引
    int i = indexFor(hash, table.length);  
    //循环遍历 table 数组上的 Entry 对象,判断该位置上 key 是否已存在
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
        Object k;  
        //哈希值相同并且对象相同
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
            //如果这个 key 对应的键值对已经存在,就用新的 value 代替老的 value,然后退出!
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;  
        }  
    }  
    //修改次数+1
    modCount++;
    //table 数组中没有 key 对应的键值对,就将 key-value 添加到 table[i] 处 
    addEntry(hash, key, value, i);  
    return null;  
}

从源码当中,我们可以看到,当我们给 put() 方法传递 key 和 value 时,HashMap 会让 key 来调用 hash() 方法,返回 key 的 hash 值,计算 Index 后用于找到 bucket(哈希桶)的位置来储存 Entry 对象。

如果两个对象 key 的 hash 值相同,那么它们的 bucket 位置也相同,但 equals() 不相同,添加元素时会发生 hash 碰撞,也叫 hash 冲突,HashMap 使用链表来解决碰撞问题。

分析源码可知,put() 时,HashMap 会先遍历 table 数组,用 hash 值和 equals() 判断数组中是否存在完全相同的 key 对象, 如果这个 key 对象在 table 数组中已经存在,就用新的 value 代替老的 value。如果不存在,就创建一个新的 Entry 对象添加到 table[ i ] 处。

如果该 table[ i ] 已经存在其他元素,那么新 Entry 对象将会储存在 bucket 链表的表头,通过 next 指向原有的 Entry 对象,形成链表结构。

get(Object key)

 public V get(Object key) {
        //若 key 为 null,遍历 table[0] 处的链表,取出 key 为 null 的 value
        if (key == null)
            return getForNullKey();
        //若 key 不为 null,用 key 获取 Entry 对象
        Entry<K,V> entry = getEntry(key);
        //若链表中找到的 Entry 不为 null,返回该 Entry 中的 value
        return null == entry ? null : entry.getValue();
    }

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //计算 key 的 hash 值
        int hash = (key == null) ? 0 : hash(key);
        //计算 key 在数组中对应位置,遍历该位置的链表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //若 key 完全相同,返回链表中对应的 Entry 对象
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        //链表中没找到对应的 key,返回 null
        return null;
    }

从源码当中,我们可以知道,如果两个不同的 key 的 hashcode 相同,两个值对象储存在同一个 bucket 位置,要获取 value,我们调用 get() 方法,HashMap 会使用 key 的 hashcode 找到 bucket 位置,因为 HashMap 在链表中存储的是 Entry 键值对,所以找到 bucket 位置之后,会调用 key 的 equals() 方法,按顺序遍历链表的每个 Entry,直到找到想获取的 Entry 为止,如果刚好要查询的 Entry 位于该 Entry 链的最末端,那 HashMap 必须循环到最后才能找到该元素。

总结

从上面的测试我们可以得出结论:

假如我们需要实现类似 Student 这种基于对象内容来判断两个对象是否相等的情况时,我们肯定会在对象中重写 equals 方法,在平时的使用中,我们只要重写 equals 方法即可。

但是如果涉及需要将对象放入类似 HashMap、HashSet 类似的集合中时,他们底层的原理都是,先判断传入的键的 hash 值是否相同,如果不同则直接放入集合中,如果相同,则在进行 equals 判断,如果 equals 也是相同,那么后来传入的键会将前面的键覆盖。

对于 String、Integer 这类的包装类,底层已经重写了 hashCode 方法,即都是唯一的。但是,如果我们自己声明了类似 Student 这样的对象,在没有重写 hashCode 方法的情况下,在将对象传入集合类的过程中,会首先计算你传入值的 hash 值,因为对象没有重写 hashCode 方法,因此你两次放入的内容相同的对象还是会被当作两个不同的对象。此时,唯一的解决方法便是,在对象中重写 hashCode 方法。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值