map 集合为我们提供了一种映射关系,其中是以元素对的形式储存的(entry<k,v>对象),每个键对应着一个值。在实际应用中为了让我们能够更为了好的处理这种数据形式,java jdk提供了很多对应不同场景的map集合类。其中主要有hashmap,treemap,linkhashmap,concurrenthashmap这四个是我们今天主要分析的集合类。
map类的定义如下:
- 每个元素释义键值对的形式存在的,集合通过键去访问值。
- 每个键只能对应一个值,但是每个值能对应多个键
- 储存的每个键不能相同,相同则会将所对应的值进行覆盖
开始前我们先那其中一个类看他们的继承关系
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
Map<K,V> ,AbstractMap 作为接口和继承它的抽象方法,为map集合类提供了一系列规范。
1.AbstractMap
作为map的抽象类,为map实现了大部分的方法,大部分的方法都是通过调用一个叫entrySet的抽象方法来获取一个Set<Entry<K,V>>集合进行操作的,所以我们只需要继承它并实现entrySet方法,就可以创建一个map类。
2.Entry<k,v>
Map.Entry<k,v> 中的内嵌接口类,实现它用于保存键与值的关系。通过读取entry对象能一次性返回键值对
2.HashMap
hashmap适用于常规的删除插入操作,但输出的顺序是无序的,可插入null值。
hashMap集合中以node对象实例为存储单元,node类是Entry<k,v>类的实现,是一个静态内部类。node源码如下:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //哈希值
final K key; //键
V value; //值
Node<K,V> next; //指向下一个元素
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//....
public final boolean equals(Object o) {
if (o == this) //地址是否相等
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue())) //判断键和值是否相等
return true;
}
return false;
}
}
在hashmap中当我们要插入一个键值对时,先计算出key中的哈希值,通过哈希值去计算出数组的下标并定位到对应对象中,如果当前对象不存在则新建一个node对象,反之存在便进行判断执行更新操作。其put源码和注释如下:
/**
* Implements Map.put and related methods
*
* @param hash key的哈希值
* @param key the key
* @param value
* @param onlyIfAbsent 如果是true 不能替换存在值
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) { //
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) //如果为空,则调用resize创建集合对象,初始化阀值
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //根据hash值算出数组下标并返回table中对应的对象,为空则创建新的节点
tab[i] = newNode(hash, key, value, null);
else { //如果不为空
Node<K,V> e; K k; //e定义为需要更新的节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //判断当前节点是否是需要更新的节点,如果是则使e指向当前节点
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //如果当前节点不是需要更新的节点,则遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //如果遍历到了链表尾节点则在尾部插入新节点
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 当链表内的元素大于等于TREEIFY_THRESHOLD(默认值为8) 会将其转换成树结构
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) //如果当前节点的hash和key相等则停止循环
break;
p = e;
}
}
if (e != null) { // existing mapping for key e指向节点不为空
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) //如果可以覆盖旧节点值则更新
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) //threshold为需要扩容的阀值
resize();
afterNodeInsertion(evict);
return null;
}
以上为jdk8中hashmap的源码,和jdk7主要的区别就是链表部分,当元素超过TREEIFY_THRESHOLD 个时会将链表改成树结构(treeifyBin方法),是对jdk7版本的优化
除了hashmap 还有一个HashTable的集合类,他的用法很类似也是根据hash值和key对元素进行定位存储,不同的是HashTable没有链表结构,只有线性结构,并且他是线程安全的。这个类是为了兼容旧版本功能才会保留至今,所以官方注解有了下面这段话:
* Java Collections Framework</a>. Unlike the new collection
* implementations, {@code Hashtable} is synchronized. If a
* thread-safe implementation is not needed, it is recommended to use
* {@link HashMap} in place of {@code Hashtable}. If a thread-safe
* highly-concurrent implementation is desired, then it is recommended
* to use {@link java.util.concurrent.ConcurrentHashMap} in place of
* {@code Hashtable}.
大致意思是 HashTable是同步的,如果不需要线程安全的话推荐使用HashMap,如果是希望在高并发环境下使用的推荐用ConcurrentHashMap中代替HashTable
3.LinkHashMap
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>{
/**
* 记录链表头部
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 链表尾部
*/
transient LinkedHashMap.Entry<K,V> tail;
...
}
继承hashMap实现了双链表功能,他的元素node类里增加了before和after两个变量用于记录上个和下个元素。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
重写了HashMap中newCode方法,插入时额外增加了完成记录链表的操作
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p); //记录链表操作
return p;
}
linkNodeLast 方法如下
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p; //设置链表尾部位置
if (last == null) //如果是第一个则记录头部
head = p;
else { //记录节点的父节点和子节点
p.before = last;
last.after = p;
}
}
完成链表操作。
4.TreeMap
TreeMap类是有序的,内部元素按照自然顺序或比较器进行排序。key不能为null值。数据结构为红黑二叉树。红黑二叉树主要特征有:
1.根节点(root)为黑色
2.叶子节点总为黑色(默认null为黑色)
3.某一节点为红色那么他的父节点和子节点不能为红色
4.某一节点到任意的子树叶子节点的路径都包含相同数目的黑色节点
TreeMap类put的思路主要是:先遍历红黑二叉树;查询是否存在含有Key的节点,如果存在则更新节点的value,如果不存在则插入叶节点,并调用 fixAfterInsertion(Entry<k,v> e) 方法调整树结构;put代码主要如下:
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) { //如果是第一个
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) { //如果设置了比较器遍历树 查找等于key的节点
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null) //如果没有比较器则key不能为null
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key; //获取key的Comparable对象
do { //查找对于key的节点
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//如果没有找到key的节点则创建一个节点
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e); //平衡红黑二叉树
size++;
modCount++; //操作次数
return null;
}
fixAfterInsertion 方法
每当插入一个节点时会有以下情况发生
情况一:父节点为黑色,则直接插入无需操作
情况二:当父节点为红色,其兄弟节点为红色时,此时插入点为红色违反特征3,则将父节点及其兄弟节点设置成黑色,爷爷节点设置成红色,
保证子树规则成立
情况三:当父节点为红色,其兄弟节点为黑色时,设置父节点为黑色,爷爷节点为红色,则将爷爷节点向兄弟节点的方向旋转(如果兄弟节点
是右节点,则需要插入节点是左节点才能对爷爷节点做旋转,如果节点是右节点则需要父节点进行一次旋转),此时会得到一个黑色
节点下有两个红色节点的结果,保证子树规则成立。
/** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED; //第一次插入的是红色
while (x != null && x != root && x.parent.color == RED) { //向下遍历不符合特征3的节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { //如果父节点是左边节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) { //父节点的兄弟节点是红色时无需旋转操作
setColor(parentOf(x), BLACK); //将父节点和父节点的兄弟节点设置成黑色,爷爷节点设置成红色,保证符合特征3
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else { //如果是黑色证明为null节点,因为如果不是null就违反特征4
if (x == rightOf(parentOf(x))) { //当新节点是右叶子节点插入时旋转父节点
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x))); //旋转爷爷节点,结果变成了一个黑节点下存在两个红节点的子树
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK; //设置根节点为黑色,有可能会应为多次变换后根节点变成红色
}
以上就是我针对map几个常用的map插入方法的分析,不足之处还请大家指点一下,谢谢