在Java中常用到的集合如ArrayList,HashMap等,本文针对这些集合做一个简单的总结和分析。
ArrayLIst:基于动态数组的集合,初始大小为10,如果添加元素时长度不够就会扩容。因为是基于数组,所以查询及顺序插入快。而指定位置的插入删除和修改则较慢,且线程不安全。
扩容代码:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// >>1表示除以2,即取原长度的一半
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 扩容后长度不够则直接取添加元素后的长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 长度大于最大长度,则返回Integer的最大值0x7fffffff
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList实现线程安全的方法:
1.使用Collections.synchronizedList(List<T> list)
2.使用Vector
LinkedList:基于双向链表,因此插入,删除较快,查询及顺序插入较慢。且线程不安全。
HashMap:基于数组+链表,用键值对的形式存储元素。具有上述两个集合的优点,线程不安全。(在Java1.8中加入了红黑树优化链表的遍历效率问题)
HashMap相对于上述两个集合优势明显,自然它的实现也较复杂一些。HashMap实际上还是一个数组,不过数组的每一个元素都是一个Node,这是继承了Map.Entry接口的一个内部类。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;// hash值
// key-value 键值对的值
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是一个数组+链表的结构。
然后让我们看看HashMap的put过程:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 非空判断 resize()在这里的作用是重新定义数组,也是hashmap的扩容方法
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过Hash值从数组取值,若不存在则直接将该key-value塞入对应下标内
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 存在的话 则加入链表头部,即取代原位置所在的Node,Java1.8中若长度大于8则把链表转为树
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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);
// 链表长度大于8 转为树
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)
resize();
afterNodeInsertion(evict);
return null;
}
上面这段代码就是HashMapput(key,value)
中调用的方法。
首先是判断数组是否为空,然后判断对应数组下标是否有值。没有则直接插入就行了。如果有值则要判断是否同一个key。因为这个下标位置是根据hash值判定的,hash值存在hash碰撞的可能。也就是不同的key在同一个下表位置。所以这也是为什么HashMap是数组+链表的结构。
有值的情况操作就比较复杂。首先判断是否有一样的key,一样则取代就行。没有则需要往链表中插入。上面注释也提到,在Java1.8中,如果链表长度超过了8,则会转化为树结构。
// 链表转为树的源码
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
思路比较简单,但是源码中做了很多判断和操作。有些还没理解,就不在这里详述以免误导大家。