为什么不建议使用自定义Object作为HashMap的key?
此前部门内的一个线上系统上线后内存一路飙高、一段时间后直接占满。协助开发人员去分析定位,发现内存中某个Object的量远远超出了预期的范围,很明显出现内存泄漏了。
结合代码分析发现,泄漏的这个对象,主要存在一个全局HashMap中,是作为HashMap的Key值。第一反应就是这里key对应类没有去覆写equals()和hashCode()方法,但对照代码仔细一看却发现其实已经按要求提供了自定义的equals和hashCode方法了。进一步走读业务实现逻辑,才发现了其中的玄机。
踩坑历程回顾
鉴于项目代码相对保密,这里举个简单的DEMO来辅助说明下。
场景:
内存中构建一个HashMap<User, List<Post>>映射集,用于存储每个用户最近的发帖信息(只是个例子,实际工作中如果遇到这种用户发帖缓存的场景,一般都是用的集中缓存,而不是单机缓存)。
用户信息User类定义如下:
@Data
public class User {
// 用户名称
private String userName;
// 账号ID
private String accountId;
// 用户上次登录时间,每次登录的时候会自动更新DB对应时间
private long lastLoginTime;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return likedCount == user.likedCount &&
Objects.equals(userName, user.userName) &&
Objects.equals(accountId, user.accountId);
}
@Override
public int hashCode() {
return Objects.hash(userName, accountId, likedCount);
}
}
实际使用的时候,用户发帖之后,会将这个帖子信息添加到用户对应的缓存中。
/**
* 将发帖信息加入到用户缓存中
*
* @param currentUser 当前用户
* @param postContent 帖子信息
*/
public void addCache(User currentUser, Post postContent) {
cache.computeIfAbsent(currentUser, k -> new ArrayList<>()).add(postContent);
}
当实际运行的时候,会发现问题就来了,Map中的记录越来越多,远超系统内实际的用户数量。为什么呢?仔细看下User类就可以知道了!
原来编码的时候直接用IDE工具自动生成的equals和hashCode方法,里面将lastLoginTime也纳入计算逻辑了。这样每次用户重新登录之后,对应hashCode值也就变了,这样发帖的时候判断用户是不存在Map中的,就会再往map中插入一条,随着时间的推移,内存中数据就会越来越多,导致内存泄漏。
这么一看,其实问题很简单。但是实际编码的时候,很多人往往又会忽略这些细节、或者当时可能没有这个场景,后面维护的人新增了点逻辑,就会出问题 —— 说白了,就是埋了个坑给后面的人踩上了。
hashCode覆写的讲究
hashCode,即一个Object的散列码。HashCode的作用:
- 对于List、数组等集合而言,HashCode用途不大;
- 对于HashMap\HashTable\HashSet等集合而言,HashCode有很重要的价值。
HashCode在上述HashMap等容器中主要是用于寻域,即寻找某个对象在集合中的区域位置,用于提升查询效率。
一个Object对象往往会存在多个属性字段,而选择什么属性来计算hashCode值,具有一定的考验:
- 如果选择的字段太多,而HashCode()在程序执行中调用的非常频繁,势必会影响计算性能;
- 如果选择的太少,计算出来的HashCode势必很容易就会出现重复了。
为什么hashCode和equals要同时覆写
这就与HashMap的底层实现逻辑有关系了。
对于JDK1.8+版本中,HashMap底层的数据结构形如下图所示,使用数组+链表或者红黑树的结构形式:

给定key进行查询的时候,分为2步:
- 调用key对象的hashCode()方法,获取hashCode值,然后换算为对应数组的下标,找到对应下标位置;
- 根据hashCode找到的数组下标可能会同时对应多个key(所谓的hash碰撞,不同元素产生了相同的hashCode值),这个时候使用key对象提供的equals()方法,进行逐个元素比对,直到找到相同的元素,返回其所对应的值。
根据上面的介绍,可以概括为:
- hashCode负责大概定位,先定位到对应片区
- equals负责在定位的片区内,精确找到预期的那一个
这里也就明白了为什么hashCode()和equals()需要同时覆写。
数据退出机制的兜底
其实,说到这里,全局Map出现内存泄漏,还有一点就是编码实现的时候缺少对数据退出机制的考虑。
参考下redis之类的依赖内存的缓存中间件,都有一个绕不开的兜底策略,即数据淘汰机制。
对于业务类编码实现的时候,如果使用Map等容器类来实现全局缓存的时候,应该要结合实际部署情况,确定内存中允许的最大数据条数,并提供超出指定容量时的处理策略。比如我们可以基于LinkedHashMap来定制一个基于LRU策略的缓存Map,来保证内存数据量不会无限制增长,这样即使代码出问题也只是这一个功能点出问题,不至于让整个进程宕机。
public class FixedLengthLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = 1287190405215174569L;
private int maxEntries;
public FixedLengthLinkedHashMap(int maxEntries, boolean accessOrder) {
super(16, 0.75f, accessOrder);
this.maxEntries = maxEntries;
}
/**
* 自定义数据淘汰触发条件,在每次put操作的时候会调用此方法来判断下
*/
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxEntries;
}
}
总结
梳理下几个要点:
- 最好不要使用Object作为HashMap的Key
- 如果不得已必须要使用,除了要覆写equals和hashCode方法
- 覆写的equals和hashCode方法中一定不能有频繁易变更的字段
- 内存缓存使用的Map,最好对Map的数据记录条数做一个强制约束,提供下数据淘汰策略。
好啦,关于这个问题的分享就到这里咯,你是否有在工作中遇到此类相同或者相似的问题呢?欢迎一起分享讨论下哦~
我是悟道,聊技术、又不仅仅聊技术~
如果觉得有用,请点个关注,也可以关注下我的公众号【架构悟道】,获取更及时的更新。
期待与你一起探讨,一起成长为更好的自己。

为什么不建议使用自定义Object作为HashMap的key?的更多相关文章
- 一个关于自定义类型作为HashMap的key的问题
在之前的项目需要用到以自定义类型作为HashMap的key,遇到一个问题:如果修改了已经存储在HashMap中的实例,会发生什么情况呢?用一段代码来试验: import java.util.HashM ...
- Java用自定义的类型作为HashMap的key
需要重写hashCode()和equals()方法才可以实现自定义键在HashMap中的查找. public class PhoneNumber { private int prefix; //区 ...
- 向集合中添加自定义类型--建议在自定义类型的时候要重写equals方法
package com.bjpowernode.t01list; import java.util.ArrayList; /* * 向集合中添加自定义类型 */public class TestLis ...
- SpringBoot自定义初始化Bean+HashMap优化策略模式实践
策略模式:定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户. 传统的策略模式一般是创建公共接口.定义公共方法-->然后创建实体类实现公共接口.根据各自的 ...
- JAVA - 如果hashMap的key是一个自定义的类,怎么办?
JAVA - 如果hashMap的key是一个自定义的类,怎么办? 使用HashMap,如果key是自定义的类,就必须重写hashcode()和equals().
- Jackson 通过自定义注解来控制json key的格式
Jackson 通过自定义注解来控制json key的格式 最近我这边有一个需求就是需要把Bean中的某一些特殊字段的值进行替换.而这个替换过程是需要依赖一个第三方的dubbo服务的.为了使得这个转换 ...
- HashMap的key可以是可变的对象吗???
大家都知道,HashMap的是key-value(键值对)组成的,这个key既可以是基本数据类型对象,如Integer,Float,同时也可以是自己编写的对象,那么问题来了,这个作为key的对象是否能 ...
- 不要使用Integer做HashMap的key,尤其在json序列化的时候
使用redisson cache来实现一个缓存功能,缓存省市县的名称,key是区域编码,integer,value是name.结果取的时候,怎么都取不出. Map<Integer, String ...
- 当我们用自定义的类对象作为key时,我们必须在程序中覆盖HashCode()和equals()。
Key值既然可以作为对象,那么也可以用一个自定义的类.比如: m.put(new sutdent(“Liucy”,30),”boss”) 如果没有语句来判定Student类对象是否相同,则会全部打印出 ...
随机推荐
- css 第二排文字居中
text-align: center; 超过长度 自动换行居中
- HCIE笔记-第十节-静态路由
协议 :标识 前方的目的网络 是通过什么协议形成的 优先级:代表形成路由的协议的优先级数值 [厂商规定] 开销值:代表该路由协议形成此路由时的开销 -- 不同的协议计算开销值的方式有区别(越小越优) ...
- HCIE笔记-第四节-MAC地址+网络层
mac地址 = 显示16进制 = 12个16进制数 二进制[逢2进1] 0/1 = 0/1 10=2 11=3 100=4 101=5 110=6 111=7 1000=8 1001=9 1010=1 ...
- Python GDAL矢量转栅格详解
前言:挺久没有更新博客了,前段时间课程实验中需要用代码将矢量数据转成栅格,常见的点栅格化方法通过计算将点坐标(X,Y)转换到格网坐标(I,J),线栅格化方法主要有DDA算法.Bresenham算法等, ...
- MySql免安装版 Error 2003 Can connect to MySQL server on ...
现象描述:mysql只能本地登录,无法远程登录 解决方案: 1. 查看mysql端口(默认端口3306,命令端口根据需要修改),发现只有本地连接端口开放. netstat -an|findstr 33 ...
- XCTF练习题---MISC---Get-the-key.txt
XCTF练习题---MISC---Get-the-key.txt flag:SECCON{@]NL7n+-s75FrET]vU=7Z} 解题步骤: 1.观察题目,下载附件 2.拿到手以后直接惊呆,挺大 ...
- .NET Core 中的 Logging 简单实用 - 记录日志消息显示到控制台
.NET Core 支持适用于各种内置和第三方日志记录提供程序的日志记录 API. 本文介绍了如何将日志记录 API 与内置提供程序一起使用. 本文中所述的大多数代码示例都来自 .Net 5 应用. ...
- Django模板相关
1.母版 想象一个举着火炬的手,除了火炬这个手还能举棒球棍.举雷神之锤.举拖拉机钥匙等等,举得东西不同给人整体感觉就不同. 母版就相当于这个手(实际为一个html文件),其他相关的html文件就相当于 ...
- [STL] map 映射
- [题解][ARC089D] ColoringBalls
题目大意 有 \(n\) 个白色的小球排成一排,有一个长为 \(k\) 的字符串 \(S\).接下来进行 \(k\) 次操作. 第 \(i\) 个操作,选择一段连续的小球(可以为空),若 \(S\) ...