本篇分析Set、Map。
先看下Map
常见的Map,有HashMap、Hashtable、ConcurrentHashMap、TreeMap。本篇重点了解HashMap,其它的Map,重点比较下其与HashMap的异同。
HashMap:java.util.HashMap
构造器:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
最常用的就是默认构造方法。默认构造方法中定义了一个loadFactor,其作用我们可以从resize()方法中的三行代码看出:用于定义HashMap极值。
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
极值主要用于HashMap的扩容,后续再提。
构造器中另一个参数是initialCapacity,作用是定义初始HashMap的长度。但是需要注意的是,HashMap内部有将该值进行转换为2的幂次方,详情请看tableSizeFor():
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
节点:
transient Node<K,V>[] table;
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 K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
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中,数据元素使用了Node数组来保存,需要注意的是,并非HashMap的所有元素都用这个数组来保存,这里仅仅保存用于定位用的Node集合,也就是桶排序里面所有首节点的数组集合,详情可以看后面“如何通过Key定位Node”这部分内容。Node继承于Map.Entry<K,V>,如此,我们想得到Key的set集合,可以通过Map的keyset方法获取。其次,在HashMap中,我们查找下一个节点,可以通过一个节点的next节点,依次查找下去,具体怎么定位,后面会解释。
Put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 1
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) // 2
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 3
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) // 4
resize();
afterNodeInsertion(evict);
return null;
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
put中我们重点需要了解的是两点,一点是关于Key的如何处理的,第二点是如何动态扩容的。
第一点:我们都知道HashMap的Key是可以为null,同时具备Set的特性,唯一性。
key可以为null:我们可以从hash()中可以看到,仅将key==null时,hash值置为0,但后续put操作依然并没有任何阻拦的地方(注意可能是版本问题,之前的HashMap针对key==null并非如此处理)。
唯一性:从putVal()的注释1处,可以看出,找到Key的下标,我们是通过(n - 1) & hash计算出来的。如果有看过排序算法中桶排序的同学,就可以知道,这里的下标其实就是桶的概念。至于桶中怎么放元素,可以从注释3看到,从这个下标节点第一个元素开始进行next遍历,如果next发现有空位,我们就将该值插入,若有tab上的key hash值和待插入的key一模一样,则是真正找到了key,直接修改该key对应的value。
第二点:从putVal()的注释4处可以看出,每次put,size有自加,再与极值进行比较,判断是否进行扩容。具体扩容,我们可以查看resize()方法:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { // 1
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 2
newCap = oldThr;
else { // 3
newCap = DEFAULT_INITIAL_CAPACITY; // DEFAULT_INITIAL_CAPACITY = 1 << 4;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { // 4
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) { // 5
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
...
}
}
}
}
return newTab;
}
如果我们直接 new HashMap(),同时第一次插入的话,代码回执行注释3处,初始创建一个长度16的Node[],极值为12。集合我们上面说的扩容情况,即当我们数组内部使用量达到极值12时,会再次进行扩容。
如果构造器带参传入,则会执行注释2\4处代码。
如果是第二次进行扩容,则会执行注释1,可以看出新的数组长度newCap = oldCap << 1,为原先的2倍,极值newThr = oldThr << 1;也是原先的2倍。
Remove方法:
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
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) { // 1
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) // 2
node = p;
else if ((e = p.next) != null) { // 3
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do { // 4
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) // 5
tab[index] = node.next;
else // 6
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
Remove方法,我们需要注意的一个是如何定位节点,另一个是如果实现移除的。
关于如何定位:我们从注释1处可以发现,我们先通过hash值初步定位数组下标,即找到桶;通过注释2,判断hash值、引用、equal来确定是否直接就是该节点;如果不是则进入注释3/4进行next依次遍历查找。
移除:从注释5可以看到,如果通过下标找到的刚好就是该节点,则将该下标位置,指向该节点的next位置;从注释6可以看出,如果不是刚好下标位置,是直接将该节点的上一个元素的next,指向该节点的next节点。
其它的Map方法因为篇幅有限就不展开分析了。
关于HashMap和Hashtable的区别,可以看这里http://www.importnew.com/24822.html,这篇文章讲的很好,不过他的HashMap源码,和我的有点小出路,应该是版本问题,可以先忽略。
ConcurrentHashMap,是目前代替Hashtable的方案,源码和HashMap长的类似,其不同点是,在操作数据时,会针对节点进行加锁(Hashtable是使用方法锁),确保同步。
再看下TreeMap
TreeMap是有序的,有序的体现在Map.Entry
static final class TreeMapEntry<K,V> implements Map.Entry<K,V> {
K key;
V value;
TreeMapEntry<K,V> left;
TreeMapEntry<K,V> right;
TreeMapEntry<K,V> parent;
boolean color = BLACK;
/**
* Make a new cell with given key, value, and parent, and with
* {@code null} child links, and BLACK color.
*/
TreeMapEntry(K key, V value, TreeMapEntry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
...
}
可以看出,TreeMapEntry有左孩子、右孩子,双亲节点。就是典型的二叉树,注意还有个color字段,所以还是红黑数。
其次,TreeMap的Key不能为null。可以看下put()源码,有几次针对Key进行判空抛出空指针异常的地方。
public V put(K key, V value) {
TreeMapEntry<K,V> t = root;
if (t == null) { // 1
if (comparator != null) {
if (key == null) {
comparator.compare(key, key);
}
} else {
if (key == null) {
throw new NullPointerException("key == null");
} else if (!(key instanceof Comparable)) {
throw new ClassCastException(
"Cannot cast" + key.getClass().getName() + " to Comparable.");
}
}
root = new TreeMapEntry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
TreeMapEntry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) { // 2
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 { // 3
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do { // 4
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);
}
// 5
TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
针对于TreeMap的put源码,我们分析下其实现过程。首先依然是查看如果定位Key的,
从注释1看,是检测根结点是否为空,为空则创建二叉树的根结点;从注释2/4看,查找Key使用了遍历二叉树中的先序遍历,如果找到(即最后一个alse,其实是if(cmp == 0)),则setValue()。注释5则是通过找到其双亲节点,再插入到双亲节点的孩子节点。
Remove方法依然是通过Key,使用先序遍历二叉树的方法找到指定的节点(TreeMapEntry):
final TreeMapEntry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
TreeMapEntry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
最后进行删除,具体删除过程就不进行了,其实也就是红黑数删除节点的概念,有兴趣的同学可以看下deleteEntry(TreeMapEntry<K,V> p)方法,篇幅有限,这里就不展开了。
Set集合
set集合,典型的有HashSet,TreeSet。
但是看完HashSet、TreeSet,大吃一惊,HashSet和TreeSet的代码简直少的可怜。HashSet居然内部持有HashMap,然后让Value使用同一个Object:
private static final Object PRESENT = new Object();
TreeSet同理。
通过上面我们对HashMap和TreeMap源码的研究,也发现,它们内部已经对Key按照Set集合的方式实现了,而通过keyset()获取到的也是set集合。