作者:炸鸡可乐

原文出处: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. 解决Zend OPcache huge_code_pages: mmap(HUGETLB) failed: Cannot allocate memory报错

    前几日看到鸟哥介绍的 <让你的PHP7更快之Hugepage>, 于是想试试手给服务器加上,参照格式安装好扩展,调整好配置文件,然后重启php-fpm,结果启动一直报Zend OPcach ...

  2. iOS cocoapods导入项目 出现 "___gxx_personality_v0", referenced from: 或者 clang: error: linker command failed with exit code 1 (use -v to see invocation) 错误

    今天想导入PNChart 编译的时候出现了  "___gxx_personality_v0", referenced from:  和 clang: error: linker c ...

  3. SQL common keywords examples and tricks

    Case Sensitive Check 1. Return names contain upper case Select id, name from A where name<>low ...

  4. 在SpringBoot中使用flyway进行数据库版本管理

    本文大纲 flyway是什么 能帮助我们解决什么问题 springboot环境下使用flyway flyway的工作原理 一.flyway是什么 Flyway是一个开源的数据库版本管理工具,并且极力主 ...

  5. WebApi -用户登录后SessionId未更新

    描工具检测出.net的程序有会话标识未更新这个漏洞 用户尚未登录时就有session cookie产生.可以尝试在打开页面的时候,让这个cookie过期.等到用户再登陆的时候就会生成一个新的sessi ...

  6. String 和StringBuffe StringBuilder 的区别

    1.可变性:String不可变(适用于做HashMap的键),StringBuffer和StringBuilder可变 2.性能角度:,String在new的时候,会在常量池中开辟空间,比较耗费内存, ...

  7. 泛微e-cology OA系统远程代码执行漏洞及其复现

    泛微e-cology OA系统远程代码执行漏洞及其复现 2019年9月19日,泛微e-cology OA系统自带BeanShell组件被爆出存在远程代码执行漏洞.攻击者通过调用BeanShell组件中 ...

  8. 手把手教你定制标准Spring Boot starter,真的很清晰

    写在前面 我们每次构建一个 Spring 应用程序时,我们都不希望从头开始实现具有「横切关注点」的内容:相反,我们希望一次性实现这些功能,并根据需要将它们包含到任何我们要构建的应用程序中 横切关注点 ...

  9. 基本数据类型(While循环,For循环,列表以及相关用法)

    正常在没有学习循环情况下,我们要输出同样的语句,需要重复打印.相当重要!!!! While循环 将输出放在一行 end=""默认是换行 print("Hello Worl ...

  10. 「分治」-cdq分治

    cdq分治是一种分治算法: 一种分治思想,必须离线,可以用来处理序列上的问题(比如偏序问题),还可以优化1D/1D类型的DP.• 算法的大体思路我们可以用点对来描述.假定我们有一个长度为n的序列,要处 ...