详解HashMap
一、数据结构
HashMap是由Hash表(散列表)维护的一个数据结构模型,什么是Hash表呢?
哈希表,是根据Key-value直接进行访问的数据结构,也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表:记录的存储位置=f(关键字)。
首先我们来看看HashMap源码中的“静态类”Entry:
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; }
源码过多,我们只展示其数据结构,是一个典型的链式数据结构,完全可以推测出HashMap解决Hash冲突的方式可能为链地址法。
二、HashMap中的主要方法
1.put()方法
依旧根据源码进行分析,对其数据结构进行更深入的验证和分析:
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); //当table为空时,传入一个临界值,构造一个新的table,table为一个数组,存放多个Entry,该方法只允许最终数组大小为2的幂 } if (key == null) return putForNullKey(value); //允许加入一个key为空的value int hash = hash(key); //获取key的hash值,通过hash函数对key的HashCode进行处理得到的值 int i = indexFor(hash, table.length); //根据Key的HashCode找到其在数组中的index:return h & (length-1);也就是使用数组长度求余。 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //循环遍历数组中指定index中的Entry链表 Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//当key的HashCode值相同,且两者equals为true,开始覆盖原来的key V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } //否则表示key为空,插入新值 modCount++; addEntry(hash, key, value, i);//加入一个新的Entry return null; }
2.get()方法:
源码:public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
get方法相对简单,如果key为null则调用getForNullKey方法,否则getEntry,获取指定entry。
3.总结
由以上源码我们可以得出,HashMap解决冲突的方式是链地址法。Hash函数,为key的HashCode值,与Hash表表长求余,余数为插入表的index,若有冲突,则在该表的Entry后面链式插入。同时,可以根据源码细节,看出HashMap允许key为null。
三、addEntry()细节和HashMap的扩容
addEntry()源码://在table指定位置新增Entry, 这个方法很重要 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { //table容量不够, 该扩容了(两倍table),重点来了,下面将会详细分析 resize(2 * table.length); //计算hash, null为0 hash = (null != key) ? hash(key) : 0; //找出指定hash在table中的位置 bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } //扩容方法 (newCapacity * loadFactor) void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //如果之前的HashMap已经扩充打最大了,那么就将临界值threshold设置为最大的int值 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //根据新传入的capacity创建新Entry数组,将table引用指向这个新创建的数组,此时即完成扩容 Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; //扩容公式在这儿(newCapacity * loadFactor) //通过这个公式也可看出,loadFactor设置得越小,遇到hash冲突的几率就越小 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } //扩容之后,重新计算hash,然后再重新根据hash分配位置, //由此可见,为了保证效率,如果能指定合适的HashMap的容量,会更合适 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]; newTable[i] = e; e = next; } } }
在addEntry时,会判断是否到了当前HashMap的容量临界值,如果到了,则进行扩容:
扩容方式是直接将HashMap的数组长度翻倍,默认数组的长度为16,负载因子为0.75f,临界值为负载因子乘以数组长度。扩容时机为key-value键值对也就是Entry的数量大于临界值时,进行扩容,并在扩容后将原来的元素重写排版。