作者:炸鸡可乐

原文出处: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. 探究JavaScript闭包

    什么是JavaScript闭包? 刚开始知道这个词,就误以为是自动执行的匿名函数块. 比如说+function(){}(); 然而并不是,那么请看下面的例子: function init() { va ...

  2. 关于javascript闭包的最通俗易懂的理解

    这两天在研究闭包,网上一通找,有牛人写的帖子,有普通人写的帖子,但是大多没戳中本小白所纠结的点,而且大多插入了立即执行函数,其实根本不需要的,反而让人产生了误解.这里我用我的方式讲解一下闭包. 1.目 ...

  3. 面对对象高阶+反射+魔法方法+单例(day22)

    目录 昨日内容 组合 封装 property装饰器 多态 鸭子类型 今日内容 classmethod staticmethod 面对对象高级 isinstance issubclass 反射(重要) ...

  4. spring boot 面试题详解

    1.什么是springboot 用来简化spring应用的初始搭建以及开发过程 使用特定的方式来进行配置(properties或yml文件) 创建独立的spring引用程序 main方法运行 嵌入的T ...

  5. NodeJs 实现 WebSocket 即时通讯(版本一)

    服务端代码 var ws = require("nodejs-websocket"); console.log("开始建立连接...") var game1 = ...

  6. Vue中Form表单验证无法消除验证问题

    iView的表单api给出了一个resetFields方法,用于重置整个表单输入的内容并清除验证提示. 但是有时候需要只消除部分的iview的resetFields方法源码是这样的resetField ...

  7. Flask:Flask的模板系统和静态文件

    1.Flask模板系统 Django框架有自己独立的模板系统,而Flask是没有的,Flask默认采用jinjia2模板系统,jinjia2是仿写Django模板系统的一个第三方模块,但性能上要比Dj ...

  8. C语言博客作业006

    问题 答案 这个作业属于那个课程 C语言程序设计1 这个作业要求在哪里 https://edu.cnblogs.com/campus/zswxy/CST2019-2/ 我在这个课程的目的是 学习并掌握 ...

  9. USACO 07DEC 道路建设(Building Roads)

    Farmer John had just acquired several new farms! He wants to connect the farms with roads so that he ...

  10. [考试反思]1030csp-s模拟测试94:未知

    排名也未知.第1或第5. 分数也未知,300或260. 人生真是大起大落... 啊啊啊啊啊我好感动啊竟然重测了一次----- 评测机怎么测怎么RE,本机怎么测怎么AC(任意编译指令,任意评测平台) 结 ...