Java中关于HashMap源码的研究
1.基础知识
1.数组
数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难。
2.链表
链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。
3.哈希表
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表。哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。
2.具体实现
由于HashMap使用的是数组+链表的方式来存储数据的。那么我们先研究下每一个元素存放数据的数据结构--HashMap的内部类。
1.基本元素
Entry<K,V>是HashMap的基本元素单位其本身就是一个链表存储方式。
//定义为静态内部类,使用时不需要外部类的对象
static class Entry<K, V> implements Map.Entry<K, V> {
//Key为HashMap定义的key,为保证key的稳定性定义为不可修改的final类型
final K key;
//value为HashMap的value
V value;
//存储的是如果哈希值相同下一个元素的引用。这是一个典型的链表结构
Entry<K, V> next;
//hash为hash(key)%length(hashMap长度默认为16)运算后的结果
int hash;
//默认构造方法
Entry(int h, K k, V v, Entry<K, V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//后面是一些重写toString、equals、 hashCode等操作就省略了。
}
上面的代码多一句嘴,在JDK1.8中以上的版本中我们会看到Node和TreeNode的基础元素类型是因为JDK1.8版本的HashMap采用数组+链表+红黑树来实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
2.基础结构
下面我们看看HashMap的基础结构Entry数组。
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry数组的长度必须是2的幂。至于为什么是2的幂这个问题不是本文的重点。
可以参考
HashMap实现原理及源码分析|
HashMap剖析
3.存取实现。
1.put
既然HashMap的基础是数组那么为什么能够随机存取。而不是数组那样一个一个add存储呢。
为了解释清楚这个概念。需要了解下HashMap内部的一些属性(成员变量)
1.size 这个属性表示了HashMap中所有KV对的数量,包含挂在链表中的KV对。
2.capacity 这个属性表示HashMap的哈希表的长度,也就是table的长度。
3.loadFactor
这个属性表示装载因子(用来形容是否装满,默认为0.75f),用来当HashMap的哈希表是否需要扩容的最大比例。当前的实装的因子为size/capacity。
4.threshold
这个属性表示HashMap的哈希表是否需要扩容的阈值。一般的来说当size大于这个值时会出发resize()操作(哈希散列表扩容的操作)。一般计算方法为capacity*loadFactor
5.modCount
这个属性表示HashMap表修改次数。给迭代器使用以保证Map迭代的完整性。
在项目第一次put是如果发现table的值为空那么就会启动一个初始化table的方法inflateTable(),这个名称很形象叫充气或者叫可以填充的。
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//table初始化方法
private void inflateTable(int toSize) {
// 将其扩大值2的幂
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建了一个大小为最大长度的entry数字
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
数组建立完成后数组是有下标的。
我们只需要将key的哈希值与数组的最大长度取余。得出的结果作为存储的下标位置存入该数组。
具体实现如下:
//存值过程
int index = key.hashCode() % table.length;
Entry[index] = value;
具体是怎么返回索引的呢,h是key的哈希码 length 是table.length由于是全部填充故table的长度大约等于capacity.
& 与运算:参加运算的两个数据,按二进制位进行“与”运算。 运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
这里用了一个巧妙的算法。应为之前的约定length必定为2的幂。那么如果将length-1的到的结果一定是全1的二进制数字例如15(1111)、 7(111)、 3(11)、1(1)等。
那么将哈希值与这样的值做与运算得出的结果为h对length取余数。下面我举个栗子说明这一点。
我们用长度为16的length举例 16-1=15用二进制表示为(1111)
依据运算规则 0&1 = 0 1&1=1那么我们只要保证,二进制的数值最后4位为0那么他的余数一定是零。只要后面四位有任意一位是1数值都会被过滤出来。成为余数。
11110000(240)、10000(16)、100000(32)、110000(48)等一定是16的倍数。也就是说无论高于4位的数值是什么对余数结果都没有干扰。
对于2的幂作为模数取模,可以用&(2n-1)来替代%2n,位运算比取模效率高很多,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。
static int indexFor(int h, int length) {
return h & (length-1);
}
这里会出现一个问题如果2个key的哈希值冲突那么会出现什么结果呢。
这时HashMap的链表就登场了。当时我们在研究哈希表存储结构的时候有一个next属性。作用是指向下一个Entry,那么这两个Entry就以链表的形式存储在了一个哈希值下。
public V put(K key, V value) {
//如果为空即第一次存储执行初始化数组table方法
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null这时就调用putForNullKey来存储value
//这就是hashMap支持null key的原因。
if (key == null)
return putForNullKey(value);
//上文讲到的计算index
int hash = hash(key);
int i = indexFor(hash, table.length);
//遍历链表
//这时一个非常漂亮的递归遍历方式
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果hash 、 key相同则覆盖原值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//如果key不相同则执行添加(也包含了第一次添加的逻辑)
addEntry(hash, key, value, i);
return null;
}
//如果key是null
private V putForNullKey(V value) {
//如果发现table[0]发现有key等于null的值则覆盖
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++;
//在table[0]添加一个新的KV
addEntry(0, null, value, 0);
return null;
}
//添加新元素
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);
}
//这时创建新的ENtry
void createEntry(int hash, K key, V value, int bucketIndex) {
//首先将当前节点的元素存储起来
Entry<K,V> e = table[bucketIndex];
//创建一个新对象存储当前元素,将当原本元素存储到next中
//如果两个元素碰撞那么后来者居上。
table[bucketIndex] = new Entry<>(hash, key, value, e);
//将元素长度增加
size++;
}
说下这个扩容的逻辑,就是这个方法resize 需要传入一个容量大小。每次扩容都是前一次容量的两倍。
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];
newTable[i] = e;
e = next;
}
}
}
2.get
取值逻辑
//存值过程
int index = key.hashCode() % table.length;
return Entry[index]
获取的逻辑就没有存储这么复杂了。
public V get(Object key) {
//key为null时单独调用获取null的逻辑
if (key == null)
return getForNullKey();
//获取value值
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
//key为null值得取值方法
private V getForNullKey() {
if (size == 0) {
return null;
}
//村吃时候是存在固定位置取时直接从table[0]位置读取
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
//取值逻辑 此方法不能重写。
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
//循环遍历链表查找到值后返回 如果没有返回null
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;
}
3.remove、clear、containsValue、containsKey
//依据key移除元素
public V remove(Object key) {
//依据key的哈希遍历链表然后移除元素
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
//clear调用了arrays全填充操作
public void clear() {
modCount++;
Arrays.fill(table, null);
size = 0;
}
//简单粗暴的遍历全部元素判断是否有该value.效率极其低下
public boolean containsValue(Object value) {
if (value == null)
return containsNullValue();
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
//判断Key是否存在.很高效
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
4.Iterator
首先我们先看下一个抽象哈希迭代器
private abstract class HashIterator<E> implements Iterator<E> {
Entry<K,V> next; // 下一个迭代的元素
int expectedModCount; // 开始迭代修改书
int index; // 当前的标记
Entry<K,V> current; // 当前的实例
//初始化迭代器给next赋值
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
//读取下一个元素
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
//将当前元素赋值给当前元素属性
current = e;
return e;
}
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
}
HashMap提供了3中迭代器遍历方式
1.值遍历(values)
//对外提供的方法
//这里的values是Values这个内部类的实例
public Collection<V> values() {
Collection<V> vs = values;
return (vs != null ? vs : (values = new Values()));
}
//这是一个内部类实现了一个迭代器Collection<V>能接收valus这个实例这是向上造型
//这个实例返回的实际上是一个Map元素的映射因为基于map所以数值是动态变化的
private final class Values extends AbstractCollection<V> {
public Iterator<V> iterator() {
return newValueIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsValue(o);
}
public void clear() {
HashMap.this.clear();
}
}
//返回一个迭代器对象
Iterator<V> newValueIterator() {
return new ValueIterator();
}
//迭代器内部类
//当不断调用next()该方法时 元素就一个接一个呗读取出来了
private final class ValueIterator extends HashIterator<V> {
public V next() {
return nextEntry().value;
}
}
具体在代码中的用法
//第一种
Collection<String> vs = m.values();
System.out.println(vs);
//第二种
Iterator<String> vs2 = m.values().iterator();
while(vs2.hasNext()){
System.out.println(vs2.next());
}
2.键遍历(keySet)
迭代方式和值遍历略有不同本质上还是使用HashIterator来迭代。只不过由取value变成了取key
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
private final class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
}
Iterator<K> newKeyIterator() {
return new KeyIterator();
}
private final class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
实际迭代用法,雷同与值遍历
Set<String> keys = m.keySet();
System.out.println(keys);
Iterator<String> keys2 = m.keySet().iterator();
while (keys2.hasNext()) {
System.out.println(keys2.next());
}
3.键值对遍历(entrySet)
迭代方式相同此处就不在赘述。
public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
}
private Set<Map.Entry<K,V>> entrySet0() {
Set<Map.Entry<K,V>> es = entrySet;
return es != null ? es : (entrySet = new EntrySet());
}
private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
return newEntryIterator();
}
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<K,V> e = (Map.Entry<K,V>) o;
Entry<K,V> candidate = getEntry(e.getKey());
return candidate != null && candidate.equals(e);
}
public boolean remove(Object o) {
return removeMapping(o) != null;
}
public int size() {
return size;
}
public void clear() {
HashMap.this.clear();
}
}
Iterator<Map.Entry<K,V>> newEntryIterator() {
return new EntryIterator();
}
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
用法和上面两个并无差别
Set<Entry<String, String>> es = m.entrySet();
System.out.println(es);
Iterator<Map.Entry<String, String>> it = m.entrySet().iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
3.HashMap的问题
HashMap的线程安全问题一直为人所诟病,幸好我们有了Hashtable、ConcurrentHashMap等安全的hashmap。
4.总结
-
允许以Key为null的形式存储<null,Value>键值对。
-
HashMap的查找效率非常高,因为它使用Hash表对进行查找,可直接定位到Key值所在的链表中;
-
使用HashMap时,要注意HashMap容量和加载因子的关系,这将直接影响到HashMap的性能问题。加载因子过小,会提高HashMap的查找效率,但同时也消耗了大量的内存空间,加载因子过大,节省了空间,但是会导致HashMap的查找效率降低。需要使用接从中权衡利弊。