作者:炸鸡可乐

原文出处:www.pzblog.cn

一、摘要

在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties等等。

应该有很多人不知道 IdentityHashMap 的存在,其中不乏工作很多年的 Java 开发者,本文主要从数据结构和算法层面,探讨 IdentityHashMap 的实现。

二、简介

IdentityHashMap 的数据结构很简单,底层实际就是一个 Object 数组,但是在存储上并没有使用链表来存储,而是将 K 和 V 都存放在 Object 数组上。

当添加元素的时候,会根据 Key 计算得到散列位置,如果发现该位置上已经有改元素,直接进行新值替换;如果没有,直接进行存放。当元素个数达到一定阈值时,Object 数组会自动进行扩容处理。

打开 IdentityHashMap 的源码,可以看到 IdentityHashMap 继承了AbstractMap 抽象类,实现了Map接口、可序列化接口、可克隆接口。

public class IdentityHashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, java.io.Serializable, Cloneable
{
/**默认容量大小*/
private static final int DEFAULT_CAPACITY = 32; /**最小容量*/
private static final int MINIMUM_CAPACITY = 4; /**最大容量*/
private static final int MAXIMUM_CAPACITY = 1 << 29; /**用于存储实际元素的表*/
transient Object[] table; /**数组大小*/
int size; /**对Map进行结构性修改的次数*/
transient int modCount; /**key为null所对应的值*/
static final Object NULL_KEY = new Object(); ......
}

可以看到类的底层,使用了一个 Object 数组来存放元素;在对象初始化时,IdentityHashMap 容量大小为64

public IdentityHashMap() {
//调用初始化方法
init(DEFAULT_CAPACITY);
}
private void init(int initCapacity) {
//数组大小默认为初始化容量的2倍
table = new Object[2 * initCapacity];
}

三、常用方法介绍

3.1、put方法

put 方法是将指定的 key, value 对添加到 map 里。该方法首先会对map做一次查找,通过==判断是否存在key,如果有,则将旧value返回,将新value覆盖旧value;如果没有,直接插入,数组长度+1,返回null

源码如下:

public V put(K key, V value) {
//判断key是否为空,如果为空,初始化一个Object为key
final Object k = maskNull(key); retryAfterResize: for (;;) {
final Object[] tab = table;
final int len = tab.length;
//通过key、length获取数组小编
int i = hash(k, len); //循环遍历是否存在指定的key
for (Object item; (item = tab[i]) != null;
i = nextKeyIndex(i, len)) {
//通过==判断,是否数组中是否存在key
if (item == k) {
V oldValue = (V) tab[i + 1];
//新value覆盖旧value
tab[i + 1] = value;
//返回旧value
return oldValue;
}
} //数组长度 +1
final int s = size + 1;
//判断是否需要扩容
if (s + (s << 1) > len && resize(len))
continue retryAfterResize; //更新修改次数
modCount++;
//将k加入数组
tab[i] = k;
//将value加入数组
tab[i + 1] = value;
size = s;
return null;
}
}

maskNull 函数,判断 key 是否为空

private static Object maskNull(Object key) {
return (key == null ? NULL_KEY : key);
}

hash 函数,通过 key 获取 hash 值,结合数组长度通过位运算获取数组散列下标

private static int hash(Object x, int length) {
int h = System.identityHashCode(x);
// Multiply by -127, and left-shift to use least bit as part of hash
return ((h << 1) - (h << 8)) & (length - 1);
}

nextKeyIndex 函数,通过 hash 函数计算得到的数组散列下标,进行加2;因为一个 key、value 都存放在数组中,所以一个 map 对象占用两个数组下标,所以加2。

private static int nextKeyIndex(int i, int len) {
return (i + 2 < len ? i + 2 : 0);
}

resize 函数,通过数组长度,进行扩容处理,扩容之后的长度为当前长度的2倍

private boolean resize(int newCapacity) {
//扩容后的数组长度,为当前数组长度的2倍
int newLength = newCapacity * 2; Object[] oldTable = table;
int oldLength = oldTable.length;
if (oldLength == 2 * MAXIMUM_CAPACITY) { // can't expand any further
if (size == MAXIMUM_CAPACITY - 1)
throw new IllegalStateException("Capacity exhausted.");
return false;
}
if (oldLength >= newLength)
return false; Object[] newTable = new Object[newLength];
//将旧数组内容转移到新数组
for (int j = 0; j < oldLength; j += 2) {
Object key = oldTable[j];
if (key != null) {
Object value = oldTable[j+1];
oldTable[j] = null;
oldTable[j+1] = null;
int i = hash(key, newLength);
while (newTable[i] != null)
i = nextKeyIndex(i, newLength);
newTable[i] = key;
newTable[i + 1] = value;
}
}
table = newTable;
return true;
}

3.2、get方法

get 方法根据指定的 key 值返回对应的 value。同样的,该方法会循环遍历数组,通过==判断是否存在key,如果有,直接返回value,因为 key、value 是相邻的存储在数组中,所以直接在当前数组下标+1,即可获取 value;如果没有找到,直接返回null

值得注意的地方是,在循环遍历中,是通过==判断当前元素是否与key相同,如果相同,则返回value。咱们都知道,在 java 中,==对于对象类型参数,判断的是引用地址,确切的说,是堆内存地址,所以,这里判断的是key的引用地址是否相同,如果相同,则返回对应的 value;如果不相同,则返回null

源码如下:

public V get(Object key) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len); //循环遍历数组,直到找到key或者,数组为空为值
while (true) {
Object item = tab[i];
//通过==判断,当前数组元素与key相同
if (item == k)
return (V) tab[i + 1];
//数组为空
if (item == null)
return null;
i = nextKeyIndex(i, len);
}
}

3.3、remove方法

remove 的作用是通过 key 删除对应的元素。该方法会循环遍历数组,通过==判断是否存在key,如果有,直接将keyvalue设置为null,对数组进行重新排列,返回旧 value。

源码如下:

public V remove(Object key) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len); while (true) {
Object item = tab[i];
if (item == k) {
modCount++;
//数组长度减1
size--;
V oldValue = (V) tab[i + 1];
//将key、value设置为null
tab[i + 1] = null;
tab[i] = null;
//删除该元素后,需要把原来有冲突往后移的元素移到前面来
closeDeletion(i);
return oldValue;
}
if (item == null)
return null;
i = nextKeyIndex(i, len);
}
}

closeDeletion 函数,删除该元素后,需要把原来有冲突往后移的元素移到前面来,对数组进行重写排列;

private void closeDeletion(int d) {
// Adapted from Knuth Section 6.4 Algorithm R
Object[] tab = table;
int len = tab.length; Object item;
for (int i = nextKeyIndex(d, len); (item = tab[i]) != null;
i = nextKeyIndex(i, len) ) {
int r = hash(item, len);
if ((i < r && (r <= d || d <= i)) || (r <= d && d <= i)) {
tab[d] = item;
tab[d + 1] = tab[i + 1];
tab[i] = null;
tab[i + 1] = null;
d = i;
}
}
}

四、总结

  1. IdentityHashMap 的实现不同于HashMap,虽然也是数组,不过IdentityHashMap中没有用到链表,解决冲突的方式是计算下一个有效索引,并且将数据keyvalue紧挨着存在map中,即table[i]=keytable[i+1]=value

  2. IdentityHashMap 允许keyvalue都为null,当keynull的时候,默认会初始化一个Object对象作为key

  3. IdentityHashMap在保存、删除、查询数据的时候,以key为索引,通过==来判断数组中元素是否与key相同,本质判断的是对象的引用地址,如果引用地址相同,那么在插入的时候,会将value值进行替换;

IdentityHashMap 测试例子:

public static void main(String[] args) {
Map<String, String> identityMaps = new IdentityHashMap<String, String>(); identityMaps.put(new String("aa"), "aa");
identityMaps.put(new String("aa"), "bb");
identityMaps.put(new String("aa"), "cc");
identityMaps.put(new String("aa"), "cc");
//输出添加的元素
System.out.println("数组长度:"+identityMaps.size() + ",输出结果:" + identityMaps);
}

输出结果:

数组长度:4,输出结果:{aa=aa, aa=cc, aa=bb, aa=cc}

尽管key的内容是一样的,但是key的堆地址都不一样,所以在插入的时候,插入了4条记录。

五、参考

1、JDK1.7&JDK1.8 源码

2、简书 - 骑着乌龟去看海 - IdentityHashMap源码解析

3、博客园 - leesf - IdentityHashMap源码解析

【集合系列】- 深入浅出的分析IdentityHashMap的更多相关文章

  1. 【集合系列】- 深入浅出的分析TreeMap

    一.摘要 在集合系列的第一章,咱们了解到,Map的实现类有HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.Pro ...

  2. 【集合系列】- 深入浅出的分析 Hashtable

    一.摘要 在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.P ...

  3. 【集合系列】- 深入浅出分析HashMap

    一.摘要 在集合系列的第一章,咱们了解到,Map的实现类有HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.Pro ...

  4. 【集合系列】- 深入浅出分析LinkedHashMap

    一.摘要 在集合系列的第一章,咱们了解到,Map的实现类有HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.Pro ...

  5. 【集合系列】- 深入浅出的分析 WeakHashMap

    一.摘要 在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.P ...

  6. 【集合系列】- 深入浅出的分析 Properties

    一.摘要 在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.P ...

  7. 【集合系列】- 深入浅出的分析 Set集合

    一.摘要 关于 Set 接口,在实际开发中,其实很少用到,但是如果你出去面试,它可能依然是一个绕不开的话题. 言归正传,废话咱们也不多说了,相信使用过 Set 集合类的朋友都知道,Set集合的特点主要 ...

  8. 【集合系列】- 深入浅出分析 ArrayDeque

    一.摘要 在 jdk1.5 中,新增了 Queue 接口,代表一种队列集合的实现,咱们继续来聊聊 java 集合体系中的 Queue 接口. Queue 接口是由大名鼎鼎的 Doug Lea 创建,中 ...

  9. Java 集合系列08之 List总结(LinkedList, ArrayList等使用场景和性能分析)

    概要 前面,我们学完了List的全部内容(ArrayList, LinkedList, Vector, Stack). Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例 Ja ...

随机推荐

  1. ESP8266开发之旅 网络篇⑨ HttpClient——ESP8266HTTPClient库的使用

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  2. 刷14道leetcode的总结

    引子 为什么我要刷leetcode?换工作?不是!那是?玩!巴菲特的双目标清单系统,基本方法是列两个清单,一个是职业生涯最重要的目标(不超过5个),另一个是比较重要的目标.对于比较重要的目标,要像躲避 ...

  3. Leetcode(3)无重复字符的最长子串

    Leetcode(3)无重复字符的最长子串 [题目表述]: 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度. 第一种方法:暴力 执行用时:996 ms: 内存消耗:12.9MB 效果: ...

  4. 函数基础(三) 装饰器\闭包 day 12

    目录 昨日回顾 可变长参数 * ** 函数对象 函数嵌套 名称空间和作用域 今日内容 闭包函数 装饰器 语法糖 装饰器模板 迭代器 昨日回顾 可变长参数 * *形参:接受多余的位置实参 *实参(可迭代 ...

  5. 数据结构(四十六)插入排序(1.直接插入排序(O(n²)) 2.希尔排序(O(n3/2)))

    一.插入排序的基本思想 从初始有序的子集合开始,不断地把新的数据元素插入到已排列有序子集合的合适位置上,使子集合中数据元素的个数不断增多,当子集合等于集合时,插入排序算法结束.常用的 插入排序算法有直 ...

  6. [apue] 如何处理 tcp 紧急数据(OOB)?

    在上大学的时候,我们可能就听说了OOB(Out Of Band 带外数据,又称紧急数据)这个概念. 当时老师给的解释就是在当前处理的数据流之外的数据,用于紧急的情况.然后就没有然后了…… 毕业这么多年 ...

  7. centos7 搭建ftp 并配置用户目录

    1.如果是阿里云服务器,登录控制后台,配置规则,开启21端口 (sftp是加密文件传输使用的22端口,我们这几是搭建ftp服务器) 2.安装配置vsftp服务器 一.配置防火墙,开启FTP服务器需要的 ...

  8. asp.net core Api集成Swagger

    当我们通过vs创建了一个api项目后,便可以开始集成swagger了 一.Swagger集成 从“程序包管理器控制台”窗口进行安装,执行Install-Package Swashbuckle.AspN ...

  9. 完美解决移动端H5页面的滑动穿透问题

    同事的分享,记录下来. 代码如下: css: body.modal-open { position: fixed; width: 100%; } js: // 兼容低版本 document.scrol ...

  10. Spring Boot 如何给微信公众号返回消息

    hello 各位小伙伴,今天我们来继续学习如何通过 Spring Boot 开发微信公众号.还没阅读过上篇文章的小伙伴建议先看看上文,有助于理解本文: Spring Boot 开发微信公众号后台 上篇 ...