数据结构简介
jdk1.7的HashMap的基本数据结构是数组与链表的组合方式。如下图所示(图片来源于网络)。
重要属性介绍
final float loadFactor;//用于计算阈值,默认0.75
int threshold;
//主要用于判断是否需要扩容的阈值
//计算方式threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
int capacity;//基本含义就是数组的size大小,数值总是2的次幂,总是刚好大于我们新建HashMap传入的数组大小,例如传入20,capacity就是32
new 操作
public HashMap(int initialCapacity, float initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
HashMap具有四个构造器,最终调用都是上面这个,默认initialCapacity,initialCapacity的值分别是16和0.75
还有在新建HashMap的时候是不会为数组分配空间,在第一次进行put操作时才会分配。
put操作
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
//第一次put时初始化数组
inflateTable(threshold);
}
if (key == null)
//key为null直接放在table[0]的数组位置
return putForNullKey(value);
//对key进行hash处理
int hash = hash(key);
//计算key应该对应table数组的下标
int i = indexFor(hash, table.length);
//覆盖旧的值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//添加到数组中或者对应位置的链表上
addEntry(hash, key, value, i);
return null;
}
put操作主干代码如上所示。逐个步骤分析。
inflateTable(threshold)
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//计算上面说到的2的次幂
int capacity = roundUpToPowerOf2(toSize);
//计算阈值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//实际初始化数组
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
hash(key)
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
对key值做hash处理,可以让key更均匀分布在数组中。
indexFor(hash, table.length)
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
这个函数两个作用。
- 可以得出一个在数组size之内的下标
- 在数组扩容时,h & (length-1)可以保证一个均匀的分布
对于第一点。例如table长度16,hash值是17二进制是10001,length-1是15二进制是1111两者与操作,得出
00001,所以对应数组的下标就是1,保证了下标不会越界。
对于第二点,在数组扩容时,总是按照2的次幂扩容,本身长度也总是2的次幂。一般我们计算下标需要保证在数组内部的话最直观就是使用求余,但是效率太低;于是用位运算,同时要保证达到类似求余的效果,均匀分布,所以必须是2的n次方。
例如table长度16,hash值是17二进制是10001,length-1是15二进制是1111两者与操作,得出
00001,所以对应数组的下标就是1。
table长度16,hash值是18二进制是10010,length-1是15二进制是1111两者与操作,得出
00010,所以对应数组的下标就是2。
table长度16,hash值是19二进制是10011,length-1是15二进制是1111两者与操作,得出
00011,所以对应数组的下标就是3。
…
呈现出均匀分布的态势。减少碰撞。
addEntry(hash, key, value, i)
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断是否需要扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容,重新索引Entry
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//添加到数组
createEntry(hash, key, value, bucketIndex);
}
(size >= threshold) && (null != table[bucketIndex]),也就是说当数组中键值对的个数大于等于阈值并且数组对应下标已经有Entry那么可以进行扩容。
resize(2 * table.length)
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
//重新索引Entry到新数组
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
createEntry(hash, key, value, bucketIndex)
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
添加Entry到数组,可以看出是从头部插入的。
get操作
get操作分析就简单多了
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
getEntry(key);
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
非常简单,就是根据key算出下标,并且遍历下标对应链表,找到对应key的Entry,返回值即可。
扩展-ConcurrentHashMap实现原理简单分析(jdk1.7)
ConcurrentHashMap属于线程安全的类,相比jdk中另一个线程安全的键值对工具类HashTable,ConcurrentHashMap的优势在于减少的锁的粒度。在HashTable中是对整个table进行加锁,但是在ConcurrentHashMap中是把table分成一个个segment,每个segment继承ReentrantLock,当需要进行数据插入时,必须获得对应segment的锁才能进行操作,但是仅仅对当前segment上锁,不影响其他segment操作。一个segment基本上就是类似一个HashMap的结构进行操作。
put操作
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
//定位对应segment
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
//对segment进行put操作
return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//获取锁,获取不到则自旋获取,重试几次后挂起
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
//插入entry操作,基本类似HashMap
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}