一:hashMap的数据结构
HashMap储存的是键值对,并允许使用null值和null键,不保证映射的顺序。HashMap实际上是一个“链表散列”的数据结构,即数组和链表和红黑树的结合体。
数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;
链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
Hashmap综合应用了这两种数据结构,实现了寻址容易,插入删除也容易
在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)。
二:源码中的常用方法
(1):get()方法
- bucket里的第一个节点,直接命中
- 如果有冲突,则通过key.equals(k)去查找对应的entry
- 若为树,则在树中通过key.equals(k)查找,O(logn);
- 若为链表,则在链表中通过key.equals(k)查找,O(n)。
get值方法的过程是:
1、获取key
2、通过hash函数得到hash值
int hash=key.hashCode();
3、得到桶号(一般都为hash值对桶数求模)
int index =hash%Entry[].length;
4、比较桶的内部元素是否与key相等,若都不相等,则没有找到。
5、取出相等的记录的value。
(2):put()方法
put函数大致的思路为:
- 对key的hashCode()做hash,然后再计算index
- 如果没碰撞直接放到bucket里
- 如果碰撞了,以链表的形式存在buckets后
- 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD)就把链表转换成红黑树
- 如果节点已经存在就替换old value(保证key的唯一性)
- 如果bucket满了(超过load factor*current capacity)就要resize
put键值对的方法的过程是:
1、获取key ;
2、通过hash函数得到hash值;
int hash=key.hashCode(); //获取key的hashCode,这个值是一个固定的int值
3、得到桶号(一般都为hash值对桶数求模) ,也即数组下标int index=hash%Entry[].length。//获取数组下标:key的hash值对Entry数组长度进行取余
4、 存放key和value在桶内。
table[index]=Entry对象;
(3):resize()方法
当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程,简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中。
(4):remove()方法
删除操作就是一个查找+删除的过程,相对于添加操作其实容易一些,但那是你基于上述添加方法理解的不错的前提下。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
根据键值删除指定节点,这是一个最常见的操作了。显然,removeNode 方法是核心。
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {
if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
删除操作需要保证在表不为空的情况下进行,并且 p 节点根据键的 hash 值对应到数组的索引,在该索引处必定有节点,如果为 null ,那么间接说明此键所对应的结点并不存在于整个 HashMap 中,这是不合法的,所以首先要在这两个大前提下才能进行删除结点的操作。
第一步,
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
node = p;
需要删除的结点就是这个头节点,让 node 引用指向它。否则说明待删除的结点在当前 p 所指向的头节点的链表或红黑树中,于是需要我们遍历查找。
第二步,
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
如果头节点是红黑树结点,那么调用红黑树自己的遍历方法去得到这个待删结点。否则就是普通链表,我们使用 do while 循环去遍历找到待删结点。找到节点之后,接下来就是删除操作了。
第三步,
if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
删除操作也很简单,如果是红黑树结点的删除,直接调用红黑树的删除方法进行删除即可,如果是待删结点就是一个头节点,那么用它的 next 结点顶替它作为头节点存放在 table[index] 中,如果删除的是普通链表中的一个节点,用该结点的前一个节点直接跳过该待删结点指向它的 next 结点即可。
最后,如果 removeNode 方法删除成功将返回被删结点,否则返回 null。
这样,相对复杂的 put 和 remove 方法的内部实现,我们已经完成解析了。下面看看其他常用的方法实现,它们或多或少都于这两个方法有所关联。
(5)clear
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
该方法调用结束后将清除 HashMap 中存储的所有元素。
(6)keySet
//实例属性 keySet
transient volatile Set<K> keySet;
public Set<K> keySet() {
Set<K> ks;
return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
}
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
}
HashMap 中定义了一个 keySet 的实例属性,它保存的是整个 HashMap 中所有键的集合。上述所列出的 KeySet 类是 Set 的一个实现类,它负责为我们提供有关 HashMap 中所有对键的操作。
可以看到,KeySet 中的所有的实例方法都依赖当前的 HashMap 实例,也就是说,我们对返回的 keySet 集中的任意一个操作都会直接映射到当前 HashMap 实例中,例如你执行删除一个键的操作,那么 HashMap 中将会少一个节点。
(7)values
public Collection<V> values() {
Collection<V> vs;
return (vs = values) == null ? (values = new Values()) : vs;
}
values 方法其实和 keySet 方法类似,它返回了所有节点的 value 属性所构成的 Collection 集合,此处不再赘述。
(8)entrySet
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
它返回的是所有节点的集合,或者说是所有的键值对集合。