默认初始容量,必须是2的n次方 static final int DEFAULT_INITIAL_CAPACITY = 16; 最大容量,当经过构造方法传入的容量比它还大时,就用这个最大容量,必须是2的n次方 static final int MAXIMUM_CAPACITY = 1 << 30; 默认负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; 用来存储键值对,能够看到键值对都是存储在Entry中的 transient Entry<K,V>[] table; //capacity * load factor,超过这个数就会进行再哈希 int threshold;
HashMap
中的元素是用名为table
的Entry
数组来保存的,默认大小是16java
capacity:数组的容量
load_factor:负载因子
threshold:实际能承载的容量,等于上面两个相乘,当size大于threshold时,就会进行rehash数组
jdk7中在面对
key
为String的时候采用了区别对待,会有alternative hashing
,可是这个在jdk8中已经被删除了性能
Entry
是一个链表结构,不只包含key
和value
,还有能够指向下一个的next
ui
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } ...
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key); 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; }
首先经过hash
方法对hashcode
进行处理:this
final int hash(Object k) { int h = 0; h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
能够看到只是在key
的hashcode
值上作了一些处理,经过hash
计算出来的值将会使用indexFor
方法找到它应该所在的table
下标:spa
static int indexFor(int h, int length) { return h & (length-1); }
这个方法其实至关于对table.length
取模。.net
当须要插入的key
为null
时,调用putForNullKey
方法处理:指针
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
putForNullKey
方法只从table[0]
这个位置开始遍历,由于key
为null
只放在table
中的第一个位置,下标为0,在遍历中若是发现已经有key
为null
了,则替换新value
,返回旧value
,结束;若是尚未key
为null
,调用addEntry
方法增长一个Entry
:code
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
能够看到jdk7中resize
的条件已经发生改变了,只有当 size>=threshold
而且 table
中的那个槽中已经有Entry
时,才会发生resize
。即有可能虽然size>=threshold
,可是必须等到每一个槽都至少有一个Entry时,才会扩容。还有注意每次resize
都会扩大一倍容量对象
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++; }
最后看createEntry
,它先保存这个桶中的第一个Entry
,建立新的Entry
放入第一个位置,将原来的Entry
接在后面。这里采用的是头插法插入元素。
其实get
方法和put
方法一模一样,怎么放的怎么拿
public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
key为null时,仍是去table[0]
去取:
private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
不然调用getEntry
方法:
final Entry<K,V> getEntry(Object key) { 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的hashcode
计算出它应该所在的下标,再遍历这个下标的Entry链,若是key的内存地址相等(即同一个引用)或者equals
相等,则说明找到了
A、等幂性。无论执行多少次获取Hash值的操做,只要对象不变,那么Hash值是固定的。若是第一次取跟第N次取不同,那就用起来很麻烦.
B、对等性。若两个对象equal方法返回为true,则其hash值也应该是同样的。举例说明:若你将objA做为key存入HashMap中,而后new了一个objB。在你看来objB和objA是一个东西(由于他们equal),可是使用objB到hashMap中却取不出来东西。
C、互异性。若两个对象equal方法返回为false,hash值有可能相同,但最好是不一样的,这个不是必须的,只是这样作会提升hash类操做的性能(碰撞概率低)。
解决hash碰撞的方法:
开放地址法
链地址法
hashmap采用的就是链地址法,这种方法好处是无堆积现象,可是next指针会占用额外空间
在jdk8中,仍然会根据key.hashCode()计算出hash值,再经过这个hash值去定位这个key,可是不一样的是,当发生冲突时,会采用链表和红黑树两种方法去处理,当结点个数较少时用链表(用Node存储),个数较多时用红黑树(用TreeNode存储),同时结点也不叫Entry了,而是分红了Node和TreeNode。再最坏的状况下,链表查找的时间复杂度为O(n),而红黑树一直是O(logn),这样会提升HashMap的效率。
jdk8中的HashMap中定义了一个变量TREEIFY_THRESHOLD,当节点个数>= TREEIFY_THRESHOLD - 1时,HashMap将采用红黑树存储
参考资料:jdk8 HashMap