当HashMap的键遇见自定义类型时-阿里云开发者社区

开发者社区> javaedge> 正文

当HashMap的键遇见自定义类型时

简介: 当HashMap的键遇见自定义类型时
+关注继续查看

欢迎志同道合的小伙伴一起交流java学习,共同应对校招

点击链接加入群【java编程技术交流】:https://jq.qq.com/?_wv=1027&k=4A14H0S

1 概述

这是Java中经典的问题,在面试中也经常被问起.很多书提到要重载hashCode()和equals()两个方法才能实现自定义键在HashMap中的查找,但是为什么要这样以及如果不这样做会产生什么后果,好像很少有文章讲到,所以来这一篇记录下.

2 案例分析

首先,如果我们直接用以下的Person类作为键,存入HashMap中,会发生发生什么呢?

package com.csdn;

/**
 * @author Shusheng Shi
 * @since 2017/6/4 11:49
 */
public class Person {

    private String id;

    public Person(String id) {
        this.id = id;
    }
}
package com.csdn;

import java.util.HashMap;

/**
 * @author Shusheng Shi
 * @since 2017/6/4 16:03
 */

public class Main {
    public static void main(String[] args) {

        HashMap<Person, String> map = new HashMap<>();

        map.put(new Person("001"), "findingsea");
        map.put(new Person("002"), "linyin");
        map.put(new Person("003"), "henrylin");
        map.put(new Person("003"), "findingsealy");

        System.out.println(map.toString());

        System.out.println(map.get(new Person("001")));
        System.out.println(map.get(new Person("002")));
        System.out.println(map.get(new Person("003")));
    }
}

那么输出结果是什么呢?

{com.csdn.Person@74a14482=henrylin, com.csdn.Person@4554617c=linyin, com.csdn.Person@1b6d3586=findingsea, com.csdn.Person@1540e19d=findingsealy}
null
null
null

我们可以看到,这里出现了两个问题:

1.在添加的过程中,将key=new Person(“003”)的键值对添加了两次,那么在期望中,HashMap中应该只存在一对这样的键值对,因为key(期望的)是相同的,所以不应该重复添加,第二次添加的value=”findingsealy”应该替换掉原先的value=”henrylin”.但是 ,在输入中,我们发现期望中的情况并没有出现,而是在HashMap同时存在了value=”findingsealy”和value=”henrylin”的两个键值对,并且它们的key值还是不相同的,这显然是错误的;

2.在获取value值时,我们分别用三个Person对象去查找,这三个对象和我们刚刚存入的三个key值(在期望中)是相同的,但是查找出的却是三个null值,这显然也是错误的.


那么,正确的方法是直接对Person类进行修改,重写equals和hashCode方法,修改过后的Person类如下:

package com.csdn;

/**
 * @author Shusheng Shi
 * @since 2017/6/4 11:49
 */
public class Person {

    private String id;

    public Person(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof Person && (id == ((Person) o).id);
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }
}

尽管看起来equals()方法只是检查其参数是否为Person的实例,但是instanceof悄悄地检查了此对象是否为null,因为若instance左边参数为null,它会返回false.若参数不为null,且类型正确,则基于每一个对象中实际的id值的hashCode进行比较.从输出结果也看出,这种方式是正确的.

{com.csdn.Person@ba31=findingsea, com.csdn.Person@ba32=linyin, com.csdn.Person@ba33=findingsealy}
findingsea
linyin
findingsealy

可以看到,之前指出的错误都得到了改正.为什么会这样呢?

在HashMap中,查找key的比较顺序为:

  1. 计算对象的Hash Code,看在表中是否存在;
  2. 检查对应Hash Code位置中的对象和当前对象是否相等.


显然,第一步就是要用到hashCode()方法,而第二步就是要用到equals()方法.在没有进行重载时,这两步会默认调用Object类的这两个方法.


而在Object类中Hash Code默认是使用对象的地址计算的,那两个Person(“003”)的对象地址是不同的,所以它们的Hash Code也不同,自然HashMap也不会把它们当成是同一个key了.同时,在Object默认的equals()中,也是根据对象的地址进行比较,自然一个Person(“003”)和另一个Person(“003”)是不相等的.


理解了这一点,就很容易搞清楚为什么需要同时重载hashCode()和equals两个方法了.

  • 重载hashCode()是为了对同一个key,能得到相同的Hash Code,这样HashMap就可以定位到我们指定的key上.
  • 重载equals()是为了向HashMap表明当前对象和key上所保存的对象是相等的,这样我们才真正地获得了这个key所对应的这个键值对.

还有一个细节,在Person类中对于hashCode()的重在方法为:

@Override
public int hashCode() {
    return id != null ? id.hashCode() : 0;
}

这里可能有疑惑的点在于:为什么可以用String类型的变量的Hash Code作为Person类的Hash Code值呢?这样new Person(new String(“003”))和new Person(new String(“003”))的Hash Code是相等的吗?


来看看以下代码的输出:

728795174
728795174
728795174
728795174

可以看到四条语句的输出都是相等的,很直观的合理的猜测就是String类型也重载了hashCode()以根据字符串的内容来返回Hash Code值,所以相同内容的字符串具有相同的Hash Code.

  public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

同时,这也说明了一个问题:为什么在已知hashCode()相等的情况下,还需要用equals()进行比较呢?就是因为避免出现上述例子中的出现的情况,因为根据对Person类的hashCode()方法的重载实现,Person类会直接用id这个String类型成员的Hash Code值作为自己的Hash Code值,但是很显然的,一个Person(“003”)和一个String(“003”)是不相等的,所以在hashCode()相等的情况下,还需要用equals()进行比较.


以下例子可以作为上述说明的佐证:

System.out.println(new Person("003").hashCode()); // 47667
System.out.println(new String("003").hashCode()); // 47667

System.out.println(new Person("003").equals(new String("003"))); // false



版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
使用注解开发| 学习笔记
快速学习 使用注解开发
23 0
案例_点菜| 学习笔记
快速学习案例_点菜
15 0
我为什么要放弃 RESTful,选择拥抱 GraphQL
REST作为一种现代网络应用非常流行的软件架构风格,自从Roy Fielding博士在2000年他的博士论文中提出来到现在已经有了20年的历史。它的简单易用性,可扩展性,伸缩性受到广大Web开发者的喜爱。
9 0
这样统计代码执行耗时,才足够优雅!
代码耗时统计在日常开发中算是一个十分常见的需求,特别是在需要找出代码性能瓶颈时。 可能也是受限于 Java 的语言特性,总觉得代码写起来不够优雅,大量的耗时统计代码,干扰了业务逻辑。特别是开发功能的时候,有个感受就是刚刚开发完代码很清爽优雅,结果加了一大堆辅助代码后,整个代码就变得臃肿了,自己看着都挺难受。因此总想着能不能把这块写的更优雅一点,今天本文就尝试探讨下“代码耗时统计”这一块。
11 0
Google 开源的依赖注入库,比 Spring 更小更快!
Guice是Google开源的一个依赖注入类库,相比于Spring IoC来说更小更快。Elasticsearch大量使用了Guice,本文简单的介绍下Guice的基本概念和使用方式。
10 0
Slf4j 包老冲突,每次排查半天,是什么原因?怎么解决?
一、前言 在进行 Java 开发时,通常我们会选择 Slf4j 作为日志门面,但日志实现却不尽相同。如果系统运行中同时存在多个日志实现,就会出现类似下图的 Warning。
10 0
听说 Spring AOP 有坑?那就来踩一踩
前言 前几日,有朋友分享了这样一个案例: 原来的项目一直都正常运行,突然
10 0
我为什么要放弃 RESTful,选择拥抱 GraphQL
REST作为一种现代网络应用非常流行的软件架构风格,自从Roy Fielding博士在2000年他的博士论文中提出来到现在已经有了20年的历史。它的简单易用性,可扩展性,伸缩性受到广大Web开发者的喜爱。
11 0
【保姆级教程】Spring Boot 单元测试
【保姆级教程】Spring Boot 单元测试 一、 单元测试的概念 概念: \1. 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。在Java中单元测试的最小单元是类。
8 0
+关注
javaedge
关注公众号:JavaEdge,后台回复面试,领取更多大厂求职资源。曾在百度、携程、华为等大厂搬砖,专注Java生态各种中间件原理、框架源码、微服务、中台等架构设计及落地实战,只生产硬核干货!
2316
文章
1
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载