為什么不建議使用Java自定義Object作為HashMap的key
前言
此前部門內(nèi)的一個(gè)線上系統(tǒng)上線后內(nèi)存一路飆高、一段時(shí)間后直接占滿。協(xié)助開(kāi)發(fā)人員去分析定位,發(fā)現(xiàn)內(nèi)存中某個(gè)Object的量遠(yuǎn)遠(yuǎn)超出了預(yù)期的范圍,很明顯出現(xiàn)內(nèi)存泄漏了。
結(jié)合代碼分析發(fā)現(xiàn),泄漏的這個(gè)對(duì)象,主要存在一個(gè)全局HashMap中,是作為HashMap的Key值。第一反應(yīng)就是這里key對(duì)應(yīng)類沒(méi)有去覆寫equals()和hashCode()方法,但對(duì)照代碼仔細(xì)一看卻發(fā)現(xiàn)其實(shí)已經(jīng)按要求提供了自定義的equals和hashCode方法了。進(jìn)一步走讀業(yè)務(wù)實(shí)現(xiàn)邏輯,才發(fā)現(xiàn)了其中的玄機(jī)。
踩坑歷程回顧
鑒于項(xiàng)目代碼相對(duì)保密,這里舉個(gè)簡(jiǎn)單的DEMO來(lái)輔助說(shuō)明下。
場(chǎng)景: 內(nèi)存中構(gòu)建一個(gè)HashMap<User, List<Post>>映射集,用于存儲(chǔ)每個(gè)用戶最近的發(fā)帖信息(只是個(gè)例子,實(shí)際工作中如果遇到這種用戶發(fā)帖緩存的場(chǎng)景,一般都是用的集中緩存,而不是單機(jī)緩存)。
用戶信息User類定義如下:
@Data
public class User {
// 用戶名稱
private String userName;
// 賬號(hào)ID
private String accountId;
// 用戶上次登錄時(shí)間,每次登錄的時(shí)候會(huì)自動(dòng)更新DB對(duì)應(yīng)時(shí)間
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 lastLoginTime == user.lastLoginTime &&
Objects.equals(userName, user.userName) &&
Objects.equals(accountId, user.accountId);
}
@Override
public int hashCode() {
return Objects.hash(userName, accountId, lastLoginTime);
}
}實(shí)際使用的時(shí)候,用戶發(fā)帖之后,會(huì)將這個(gè)帖子信息添加到用戶對(duì)應(yīng)的緩存中。
/**
* 將發(fā)帖信息加入到用戶緩存中
*
* @param currentUser 當(dāng)前用戶
* @param postContent 帖子信息
*/
public void addCache(User currentUser, Post postContent) {
cache.computeIfAbsent(currentUser, k -> new ArrayList<>()).add(postContent);
}當(dāng)實(shí)際運(yùn)行的時(shí)候,會(huì)發(fā)現(xiàn)問(wèn)題就來(lái)了,Map中的記錄越來(lái)越多,遠(yuǎn)超系統(tǒng)內(nèi)實(shí)際的用戶數(shù)量。為什么呢?仔細(xì)看下User類就可以知道了!
原來(lái)編碼的時(shí)候直接用IDE工具自動(dòng)生成的equals和hashCode方法,里面將lastLoginTime也納入計(jì)算邏輯了。這樣每次用戶重新登錄之后,對(duì)應(yīng)hashCode值也就變了,這樣發(fā)帖的時(shí)候判斷用戶是不存在Map中的,就會(huì)再往map中插入一條,隨著時(shí)間的推移,內(nèi)存中數(shù)據(jù)就會(huì)越來(lái)越多,導(dǎo)致內(nèi)存泄漏。
這么一看,其實(shí)問(wèn)題很簡(jiǎn)單。但是實(shí)際編碼的時(shí)候,很多人往往又會(huì)忽略這些細(xì)節(jié)、或者當(dāng)時(shí)可能沒(méi)有這個(gè)場(chǎng)景,后面維護(hù)的人新增了點(diǎn)邏輯,就會(huì)出問(wèn)題 —— 說(shuō)白了,就是埋了個(gè)坑給后面的人踩上了。
hashCode覆寫的講究
hashCode,即一個(gè)Object的散列碼。HashCode的作用:
- 對(duì)于List、數(shù)組等集合而言,HashCode用途不大;
- 對(duì)于HashMap\HashTable\HashSet等集合而言,HashCode有很重要的價(jià)值。
HashCode在上述HashMap等容器中主要是用于尋域,即尋找某個(gè)對(duì)象在集合中的區(qū)域位置,用于提升查詢效率。
一個(gè)Object對(duì)象往往會(huì)存在多個(gè)屬性字段,而選擇什么屬性來(lái)計(jì)算hashCode值,具有一定的考驗(yàn):
- 如果選擇的字段太多,而HashCode()在程序執(zhí)行中調(diào)用的非常頻繁,勢(shì)必會(huì)影響計(jì)算性能;
- 如果選擇的太少,計(jì)算出來(lái)的HashCode勢(shì)必很容易就會(huì)出現(xiàn)重復(fù)了。
為什么hashCode和equals要同時(shí)覆寫
這就與HashMap的底層實(shí)現(xiàn)邏輯有關(guān)系了。
對(duì)于JDK1.8+版本中,HashMap底層的數(shù)據(jù)結(jié)構(gòu)形如下圖所示,使用數(shù)組+鏈表或者紅黑樹(shù)的結(jié)構(gòu)形式:

給定key進(jìn)行查詢的時(shí)候,分為2步:
- 調(diào)用key對(duì)象的hashCode()方法,獲取hashCode值,然后換算為對(duì)應(yīng)數(shù)組的下標(biāo),找到對(duì)應(yīng)下標(biāo)位置;
- 根據(jù)hashCode找到的數(shù)組下標(biāo)可能會(huì)同時(shí)對(duì)應(yīng)多個(gè)key(所謂的hash碰撞,不同元素產(chǎn)生了相同的hashCode值),這個(gè)時(shí)候使用key對(duì)象提供的equals()方法,進(jìn)行逐個(gè)元素比對(duì),直到找到相同的元素,返回其所對(duì)應(yīng)的值。
根據(jù)上面的介紹,可以概括為:
- hashCode負(fù)責(zé)大概定位,先定位到對(duì)應(yīng)片區(qū)
- equals負(fù)責(zé)在定位的片區(qū)內(nèi),精確找到預(yù)期的那一個(gè)
這里也就明白了為什么hashCode()和equals()需要同時(shí)覆寫。
數(shù)據(jù)退出機(jī)制的兜底
其實(shí),說(shuō)到這里,全局Map出現(xiàn)內(nèi)存泄漏,還有一點(diǎn)就是編碼實(shí)現(xiàn)的時(shí)候缺少對(duì)數(shù)據(jù)退出機(jī)制的考慮。 參考下redis之類的依賴內(nèi)存的緩存中間件,都有一個(gè)繞不開(kāi)的兜底策略,即數(shù)據(jù)淘汰機(jī)制。
對(duì)于業(yè)務(wù)類編碼實(shí)現(xiàn)的時(shí)候,如果使用Map等容器類來(lái)實(shí)現(xiàn)全局緩存的時(shí)候,應(yīng)該要結(jié)合實(shí)際部署情況,確定內(nèi)存中允許的最大數(shù)據(jù)條數(shù),并提供超出指定容量時(shí)的處理策略。比如我們可以基于LinkedHashMap來(lái)定制一個(gè)基于LRU策略的緩存Map,來(lái)保證內(nèi)存數(shù)據(jù)量不會(huì)無(wú)限制增長(zhǎng),這樣即使代碼出問(wèn)題也只是這一個(gè)功能點(diǎn)出問(wèn)題,不至于讓整個(gè)進(jìn)程宕機(jī)。
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;
}
/**
* 自定義數(shù)據(jù)淘汰觸發(fā)條件,在每次put操作的時(shí)候會(huì)調(diào)用此方法來(lái)判斷下
*/
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxEntries;
}
}總結(jié)
梳理下幾個(gè)要點(diǎn):
- 最好不要使用Object作為HashMap的Key
- 如果不得已必須要使用,除了要覆寫equals和hashCode方法
- 覆寫的equals和hashCode方法中一定不能有頻繁易變更的字段
- 內(nèi)存緩存使用的Map,最好對(duì)Map的數(shù)據(jù)記錄條數(shù)做一個(gè)強(qiáng)制約束,提供下數(shù)據(jù)淘汰策略。
到此這篇關(guān)于為什么不建議使用Java自定義Object作為HashMap的key的文章就介紹到這了,更多相關(guān)Java HashMap的key內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 遍歷取出Map集合key-value數(shù)據(jù)的4種方法
這篇文章主要介紹了Java 遍歷取出Map集合key-value數(shù)據(jù)的4種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
Java實(shí)現(xiàn)超級(jí)實(shí)用的日記本
一個(gè)用Java語(yǔ)言編寫的,實(shí)現(xiàn)日記本的基本編輯功能、各篇日記之間的上下翻頁(yè)、查詢?nèi)沼泝?nèi)容的程序。全部代碼分享給大家,有需要的小伙伴參考下。2015-05-05
java 完全二叉樹(shù)的構(gòu)建與四種遍歷方法示例
本篇文章主要介紹了java 完全二叉樹(shù)的構(gòu)建與四種遍歷方法示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-03-03
RestTemplate發(fā)送get和post請(qǐng)求,下載文件的實(shí)例
這篇文章主要介紹了RestTemplate發(fā)送get和post請(qǐng)求,下載文件的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09
Spring Boot項(xiàng)目中定制PropertyEditors方法
在本篇文章里小編給大家分享的是一篇關(guān)于Spring Boot定制PropertyEditors的知識(shí)點(diǎn)內(nèi)容,有需要的朋友們可以參考學(xué)習(xí)下。2019-11-11
SpringBoot利用攔截器實(shí)現(xiàn)避免重復(fù)請(qǐng)求
Spring MVC中的攔截器(Interceptor)類似于Servlet中的過(guò)濾器(Filter),它主要用于攔截用戶請(qǐng)求并作相應(yīng)的處理。本文就將利用攔截器實(shí)現(xiàn)避免重復(fù)請(qǐng)求,感興趣的小伙伴可以了解一下2022-11-11

