容器--IdentityHashMap
一、概述
IdentityHashMap也是一类特殊的Map,根据其名字,Identity,即同一性,其表现出的具体特点便是,在判断Map中的两个key是否相等时,只通过==来判断,而不通过equals,也就是说,如果两个key相同,那么这两个key必须是同一个对象。
除此之外,虽然这也是HashMap,即至少是Key的存储是基于Hash实现的,但其存储方式跟HashMap还是有很大的不一样。下面详细分几个方面进行说明。
二、实现分析
1. 初始化
可以通过三种方式来构造一个IdentityHashMap, 如下:
public IdentityHashMap() { init(DEFAULT_CAPACITY);//默认32 } public IdentityHashMap(int expectedMaxSize) { if (expectedMaxSize < 0) throw new IllegalArgumentException("expectedMaxSize is negative: " + expectedMaxSize); init(capacity(expectedMaxSize)); } public IdentityHashMap(Map<? extends K, ? extends V> m) { // Allow for a bit of growth this((int) ((1 + m.size()) * 1.1)); putAll(m); }
可以看到,以上三种方式最终都调用了init方法,而传入的参数即是容器的容量,而第二个方法中,在调用init之前会先对参数值进行处理,其目的是根据当前值构造一个最接近该数值的2的指数倍,这样,保证初始化时,容量的大小为2的指数倍。
初始化的具体过程如下:
private void init(int initCapacity) { threshold = (initCapacity * 2)/3; //阈值为初始值的2/3,所以对于默认值32来说, //其大小为32*2/3=21,这是key的个数 table = new Object[2 * initCapacity];//为什么*2,因为值也是放在这个table中的 //所以table的size不等于容量 }
代码很简单,设置阈值为容量的2/3,并申请一个2倍于容量的数组。之所以这里要扩大一倍,是因为Map值也存储于这个数组中,所以,需要与key一一对应。
2.存储
对于存储来说,我们还是来看下put的实现
public V put(K key, V value) { Object k = maskNull(key); Object[] tab = table; int len = tab.length; int i = hash(k, len); Object item; while ( (item = tab[i]) != null) { if (item == k) { V oldValue = (V) tab[i + 1]; tab[i + 1] = value; return oldValue; } i = nextKeyIndex(i, len); } modCount++; //新增加一个 tab[i] = k; tab[i + 1] = value; if (++size >= threshold) resize(len); // len == 2 * current capacity. return null; } private static int nextKeyIndex(int i, int len) { return (i + 2 < len ? i + 2 : 0); }
根据上面的业务逻辑,我们将其用流程图表示如下:
根据流程图我们不难理解这个逻辑,有几点需要注意一下:
1)key和value的值实际上都是存储在数组中的,而且val是挨着key存储的。
2)当发生冲突的时候,这里采用的方式是依次找下一个,直到有空的位置,或者找到key应有的位置。
3)因为在超过阈值后会进行resize的操作,table的长度会被扩大一倍,所以步骤2)一定能找到一个空位置,或者找到之前设置的位置。
如果没有自动扩容机制,则步骤2)很可能会出现死循环。
3. 查找
get方法的实现逻辑如下:
public V get(Object key) { Object k = maskNull(key); Object[] tab = table; int len = tab.length; int i = hash(k, len);//表的长度,2的n次方 while (true) { Object item = tab[i]; if (item == k) return (V) tab[i + 1]; if (item == null) return null; //那么这个表示不存在该key, 所以返回null i = nextKeyIndex(i, len); } }
这个过程很简单,了解了存储机制后,这个就容易理解了,需要说明的是,如果该位置为null,表示该位置未存储key, 返回null.
4. 删除
删除的业务逻辑如下:
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++; size--; V oldValue = (V) tab[i + 1]; tab[i + 1] = null; tab[i] = null; closeDeletion(i); return oldValue; } if (item == null) //未找到该key return null; i = nextKeyIndex(i, len); } }
查找的逻辑还是比较好理解,删除的话,实际上就是把相应位置的值置null,实现引用的消除,以便垃圾回收。
三、总结
上面我们大致分析了这类Map的存储机制,总的来说还是比较简单,和HashMap有类似的地方,但实现方式差别很大。主要差别整理如下 :
1)IdentityHashMap的loadFactor固定为2/3
2)IdentityHashMap的所有key和value都存储在数组中,key后的下一个位置即是对应的value
3)IdentityHashMap的冲突检测方式为线性再探测,即找下一个元素再探测,没有链式结构
4)最重要的一点,判断两个key是否相同,只根据==来判断,不使用equals
如果我们的业务需要有第4)点的需求,则可以使用IdentityHashMap.