序:
在java语言中,业务开发人员经常会使用map来做内存缓存,但是hashmap里面的坑是有很多的,比如内存溢出啦,死循环啦,丢数据啦……,是不是说的HashMap一无是处呢!也不是这样,它使用起来是很方便的。java源码系列
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
1.实现了Map接口,可以用k-v来存储数据。
2.实现了Serializable接口,可以序列化。
3.实现了Cloneable接口,支持克隆方法。
一 数据结构
HashMap采用了一种数组加链表的方式存储数据。
数组:
查找和添加数据比较方便,效率高。
删除数据需要移动数据,效率低。
链表:
删除和添加只需要移动指针的指向,效率高。
查找需要整链表查找,效率低。
那HashMap的链表结构是怎样的呢?如下图:
HashMap模式数组大小是16,每个数组里面存的Entry里面有4个值(key,value,hash,next),装载因子是0.75 阈值大小是16*0.75 =12,也就是说第一个扩容是在第12个数据赋值的时候。
解释一下为什么HashMap采用了数组+链表的形式?
答:首先之前说过了,数组和链表各有优缺点,数组加链表的方式把他们的有点结合了,其次里面涉及了hash冲突,比如 哈希算法是%2运算,3%2 =1 ,5%2=1,他们得到的都是同一hash值,所以存的时候3的next指向5(5是跟在3的后面)。
二 运行原理
HashMap put方法判断逻辑运行图
步骤详解:
1.判断是否是空表,如果是空表,初始化这个表。
2 如果key是null,将null的这个key保存在table中。
2.hashmap中是否有这个key,如果有就用新的值替换掉旧的值,返回旧值。
3.判断是否超过阈值,如果超过就扩容,方式采用2的倍数来扩。为什么是2的倍数,源码里面有一个取数组下标的方法用的是hash&(length-1) 其中hash就是hash值,length就是数组的长度。hash&(length-1) 和 hash%length的 效果是一样的。&比%的性能更好,好多少?可以看这个博客。 只有是2的倍数才符合这个规律,所以扩容用了2的倍数。
4.添加新值。
三 源码注释
put方法的源码解析,上面的步骤只是说了一个大概,现在源码详细分析每一步。
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); //若果是空表,还没有数值,就初始化这个 } if (key == null) return putForNullKey(value); //如果key是null,就将null添加进table int hash = hash(key); //获取hashMap定义的一种hash方法 int i = indexFor(hash, table.length); //取数组的下标 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //将数组下面的链表循环,看有没有key相等,如果有,新值替换旧值。 Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //判断如果是存在key,新值替换,为什么用==判断了又用equals?想知道详细信息看这篇博客 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); //添加一个新的元素 return null; }
private void inflateTable(int toSize) { //初始化这个表 // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); //取2的n次方要》=初始化这个值 如果 toSize 为 12,13,14,15,16,capacity=16 若果toSize =17 那么capacity=32 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //阈值是 capacity * loadFactor 默认capacity=16,loadFactor=0.75 table = new Entry[capacity]; initHashSeedAsNeeded(capacity); //初始化hash掩码,在hash函数里面会用到,知道是hash里面的就可以了 }
private V putForNullKey(V value) { //把key为null的值放到表里面 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { //如果只要找到有key为null的就直接替换,这就是说HashMap里面最多只能有一个key为null的键值对。 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); //添加这个值,第一个参数是hash值,第二个参数是key,第三个参数是value,第四个参数是在表里面的位置,默认放在了第一个数组里面 return null; }
final int hash(Object k) { //哈希函数 int h = hashSeed; //hashSeed 就是之前在initHashSeedAsNeeded方法里面设置的值 if (0 != h && k instanceof String) { //看到没,如果是String,用的还是不一样的哈希方法 return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // 获取key的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); //右移位 20,12,中间再异或,你可以看做特定的hash方法就可以了。 },
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); }
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; //因为之前key为null,添加元素也是调用了addEntry方法,所以这个对key进行了判null。有没有人会问,这个为什么又进行hash计算,因为数组扩容了。 bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
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]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); //将老表转成新表 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
void transfer(Entry[] newTable, boolean rehash) { //转换函数 int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; //使用头插法插入数据,也就是说如果老表中存在hash值一样三个值a,b,c ,链的顺序是a->b->c,到新的表里面hash值也一样的话,数值的顺序变成了c->b->a newTable[i] = e; e = next; } } }
转换的前后关系图。
至此,put方法就讲完了。
下面来讲讲get方法,流程就不说了,直接讲解源码
public V get(Object key) { //获取值 if (key == null) return getForNullKey(); //如果key为空, Entry<K,V> entry = getEntry(key); //获取值 return null == entry ? null : entry.getValue(); }
final Entry<K,V> getEntry(Object key) { if (size == 0) { //表的长度为0 直接返回size,里面有些东西没有讲,size用的是volatile修饰,如果在获取的时候有一个线程把数据删除了,它是可以立马知道的,由于篇幅限制,这版就不讲了 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)))) //一步步查找,找到key相等的就将值返回 return e; } return null; }
好了,hashmap的put和get方法到这里就全讲完了。
HashMap的使用时需要有几个注意的地方:
1.HashMap是不安全的,在扩容的时候回造成死循环和数据丢失。解决办法1.不用再多线程中使用,2 Collections.synchronizedMap(Map)来做同步限制,性能较差,可以用ConcurrentHashMap替代。
2.注意HashMap使用的时候最好对数据量做一个评估。