您的位置:首页 > 其它

HashMap分析

2016-07-20 23:38 337 查看
hashMap与TreeMap一样继承AbstractMap,实现了Map的一些方法,

主要分析它的get,put,remove等方法。

public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable


HashMap是在bucket中储存键对象和值对象,作为Map.Entry。数据结构是基于数组存储,每个元素是一个Entry,Entry其实是一个链表结构,当hash碰撞时候就通过链地址方式解决了。



构造器及初始化:

不传任何参数的构造器,系统默认容量为16,hash因子0.75

public HashMap() {
//不传任何参数,默认DEFAULT_INITIAL_CAPACITY=16;DEFAULT_LOAD_FACTOR=0.75
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}


//设置hashMap大小 2的倍数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}


//设置size,和hash因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//size超过最大值,设置为 MAXIMUM_CAPACITY =1 << 30
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支持key,value为null,这也是与hashTable不同的点

public V get(Object key) {
if (key == null)
return getForNullKey();
//找到对应的Entry
Entry<K,V> entry = getEntry(key);
//返回value
return null == entry ? null : entry.getValue();
}


get方法中主要有两个函数 getForNullkey和getEntry,看方法名字可以大概知道意思

private V getForNullKey() {
if (size == 0) {
//如果hashMap为null 则返回为空
return null;
}
//从下面循环看出key为null的对象放在table[0]的位置
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}


getEntry才是真正的通过key找到对应的value的关键

final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//计算hash,这里也可以看出来key为null 放在table[0]
int hash = (key == null) ? 0 : hash(key);
//通过indexFor找到key在table中的位置,for循环找到hash值相等,且key相等的Entry
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;
}


getEntry看似简单,里面藏着几个重要的方法hash函数和indexFor函数

final int hash(Object k) {
//hashSeed一个随机值,减少hash碰撞
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).
//通过上面注释可以知道也是减少has冲突
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}


hash算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突,,h的高12与h的中12异或运算等到h’,再异或下一次的高12与h的中12异或运算等到h”,return同理。

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);
}


这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的 n 次方,这是HashMap在速度上的优化。

为什么hashMap的size是2的n次方呢?

测试两组的hashcode均为8(1000)和9(1001)

左边length=16 右边length=15



从上图结果看出(可以多测试几组数据),当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

public V put(K key, V value) {
if (table == EMPTY_TABLE) {
// 创建新数组
inflateTable(threshold);
}
//key为null 插入table[0]位置
if (key == null)
return putForNullKey(value);
//hash得到table下标
int hash = hash(key);
int i = indexFor(hash, table.length);
//添加到数组中
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果key值已经存在,则用新值覆盖
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//与LRU算法有关,如果accessOrder为true时,即使用的是最近最少使用的次序,则将当前被修改的节点移动到header节点之前,即链表的尾部
e.recordAccess(this);
return oldValue;
}
}

modCount++;
//创建一个Entry
addEntry(hash, key, value, i);
return null;
}


void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//超过阀值2倍扩容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}

createEntry(hash, key, value, bucketIndex);
}


可以发现,put是先查找,找到key对应的Entry就直接修改Entry的value值,否则就增加一个元素。增加的元素是在链表的头部,也就是占据table中的元素,如果table中对应索引原来有元素的话就将整个链表添加到新增加的元素的后面。也就是说新增加的元素再次查找的话是优于在它之前添加的同一个链表上的元素。这里涉及到就是扩容,也就是一旦元素的个数达到了扩容因子规定的数量(threhold=table.length*loadFactor),就将数组扩大1倍。

扩容的过程。可以看到扩充过程会导致元素数据的所有元素进行重新hash计算,这个过程很耗时,故尽可能的选择合适的初始化大小是有效提高HashMap效率的关键。

假设100数存储,hash因子为0.75,则100 > 128 * 0.75 ,所以选择256,

如果确定数据不会很多,不超过100,那么设置hash因子为1是不错的选择,这样就不会浪费空间。

public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}

final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
//计算hash 找到key对应位置i
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);

Entry<K,V> prev = table[i];
//当前节点
Entry<K,V> e = prev;

while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
// Hash表中链表节点被正常删除后,调用该方法修正由于节点被删除后链表的前后指向关系
e.recordRemoval(this);
return e;
}
//当前节点指向前置节点
prev = e;
//后置节点指向当前节点,后移
e = next;
}

return e;
}


删除操作分两个过程――首先找到适当的 Entry 对象并把其值字段设为 null,然后对链中从头元素到要删除的元素的部分进行克隆,再连接到要删除的元素之后的部分。因为值字段是易变的,如果另外一个线程正在过时的链中查找那个被删除的元素,它会立即看到一个空值,并知道使用同步重新进行检索。最终,原始 hash 链中被删除的元素将会被垃圾收集。

迭代器后面单独分析这里就不做分析了。

总结:

主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本原因,以及将普通的域模型对象作为key的基本要求。尤其是hash函数的实现,可以说是整个HashMap的精髓所在。

其实hashMap还有很多知识点,有时间可以细细的读一下,比如

hash算法研究

equals() 和 hashCode()

hash碰撞
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: