集合拾遗篇
前言
之前对一些常用的集合进行了源码阅读,但是没想到现在已经有大厂开始问LinkedHashMap
的底层源码了。
这也实在是太卷了吧,所以这篇文章主要对之前遗留的一些Set
集合、LinkedMap
集合以及Vector
集合的源码进行粗略的阅读。
Map
LinkedHashMap
LinkedHashMap是一种比较特殊的HashMap,它本身继承与HashMap,在HashMap的基础上将Map中的节点使用双向链表的方式连接起来,这使得它能够按插入顺序访问Map中的节点,可以比较方便的实现LRU算法。
除此之外由于HashMap的节点是由双向链表连接起来的,所以在迭代的时候不会遍历table(HashMap中需要遍历table数组中的每一个桶),直接遍历所有的Entry。
继承和实现
LinkedHashMap继承了HashMap类,并且实现了Map接口,这两者我们已经非常熟悉了,所以就不再仔细展开了。
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
内部类
LinkedHashMap的Entry<K,V>继承了父类的Node,并且在此之上增加了before和after两个成员变量。
顺便一说,红黑树的TreeNode继承自LinkedHashMap的Entry<K,V>。
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);
}
}
成员变量
LinkedHashMap只有4个成员变量。
private static final long serialVersionUID = 3801124242820219131L;
//双向链表的头结点
transient LinkedHashMap.Entry<K,V> head;
//双向链表的尾结点
transient LinkedHashMap.Entry<K,V> tail;
//此链接哈希映射的迭代排序方法:<tt>true</tt>表示访问顺序,<tt>false</tt>表示插入顺序。
final boolean accessOrder;
构造方法
我们来挑一个构造方法查看一下,发现其实是直接调用了父类(HashMap)的构造方法,所以说LinkedHashMap除了节点之外和HashMap没什么区别。
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
Get方法
首先来看get方法,我们可以看到调用的就是HashMap中的getNode方法,在这个方法中会迭代链表查找传入的节点,查找完了之后如果Map是按照访问顺序排序的话还需要移动该节点至末尾。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
//查找节点
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
//如果LinkedHashMap中的节点要求是按照访问顺序排序,需要在每次访问之后将该节点移动到双向链表的末尾。
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
小结
本来还想看一下put方法的,结果没想到这个方法都是直接调用的父类,所以就直接总结一下吧,总来的来说LinkedMap只是在父类的基础上封装了一层,给节点增加了指向前驱和后继节点的指针,其他的地方没有太大变化。
虽然叫做LInkedHashMap,但是它的底层还是依赖于一个名为table的Node数组,依然需要扩容。
WeakHashMap
总的来说,弱引用的HashMap其实没什么好说的,因为它里面的成员变量、方法都和1.7中的HashMap相类似。
它和HashMap的主要区别就在于key是弱引用,所以当key只有弱引用的时候会被GC直接回收,但是value是一个强引用,所以可能会造成内存泄漏。
不过这么设计也存在好处,WeekHashMap 的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。
所以著名的ThreadLocal就是通过WeakHashMap实现的。
Set
其实Set没什么好说的,虽然我们经常说HashMap的key是一个Set集合,但是事实上,我们最常使用HashSet其实是一个HashMap,这大概就是虽然我看起来是你爸爸,其实你是我爸爸的这种感觉。
Set接口
总来的来说Set接口看起来平平无奇,没有什么特别的地方。
public interface Set<E> extends Collection<E> {
int size();
boolean isEmpty();
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean retainAll(Collection<?> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
//分割迭代器,可用于并行迭代
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
}
AbstractSet
Set的抽象类中只有4个方法(包括构造方法)。
我们重点关注一下这里的equals和hashCode,Set对这两个方法进行了覆盖,equals用于比较两个Set是否相等,这里用了一个很巧妙的方法,首先判断两者的元素个数是否相同,然后判断是否包含,如果都是则说明这两个set的所有元素相同。
hashCode也进行了重写,由Set中所有的元素的hashCode重写而成。
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {
protected AbstractSet() {
}
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection<?> c = (Collection<?>) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
public int hashCode() {
int h = 0;
Iterator<E> i = iterator();
while (i.hasNext()) {
E obj = i.next();
if (obj != null)
h += obj.hashCode();
}
return h;
}
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
if (size() > c.size()) {
for (Iterator<?> i = c.iterator(); i.hasNext(); )
modified |= remove(i.next());
} else {
for (Iterator<?> i = iterator(); i.hasNext(); ) {
if (c.contains(i.next())) {
i.remove();
modified = true;
}
}
}
return modified;
}
}
HashSet
Set其实讲这一个就够了,因为剩下的都是一样的套路。
HashSet里面有两个重要的成员变量(剩下那个是序列化ID),一个是HashMap,一个是被声明为私有的静态final对象,用来填充value。
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
然后来看一下add和remove方法,这两个方法其实就是对Map中的put和remove方法进行了一次封装。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
古老的容器
让我们最后来学习三种古老的容器。
Vector
Vector是一种线程安全的容器,它实现线程安全的方法比较粗暴就是直接在方法上声明synchronized,这样会锁住调用这个方法的对象。
但是这样也带来了一个问题,单个方法执行时具有原子性,但是多个方法连续执行就没有原子性了,所以如果你想一次性执行多个方法,那么很有可能会导致错误。
这个容器和ArrayList比较相似,而且现在已经完全被ArrayList所取代了。
所以我们就粗看一下吧。
构造方法
由于Vector的成员变量比较少,只有一个Object数组和这个数组的容量以及当前元素的个数,所以就不贴出来了。
直接来看构造方法, 可以看到这个构造方法和ArrayList比较相似,但是没有那么多花里胡哨,还分为空白数组和默认空白数组。
在Vector默认就是new一个新的Object[]
。
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
扩容
对于数组来说插入和删除其实都没什么好看的,我们还是关注一下他是如何扩容的吧。
//扩容,传入最小容量,跟 ArrayList.grow(int) 很相似,只是扩大量不同
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//如果增长量 capacityIncrement 不大于 0 ,就扩容 2 倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
Stack
Stack也是一种古老的容器了,现在基本已经被stack模式的Deque所代替了。
Stack的实现比较简单,就是对Vector进行了一层封装。大部分的方法就是直接改了名字调用Vector的方法,并且由于是继承关系,所以Stack甚至能够执行stack.get(i)
方法。
public
class Stack<E> extends Vector<E> {
public Stack() {
}
public E push(E item) {
addElement(item);
return item;
}
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
public boolean empty() {
return size() == 0;
}
public synchronized int search(Object o) {
int i = lastIndexOf(o);
if (i >= 0) {
return size() - i;
}
return -1;
}
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = 1224463164541339165L;
}
Hashtable
Hashtable也是一个古老的容器,它和Vector一样直接在方法上加synchronized,这就导致了它的性能比较低下,所以现在基本被concurrentHashMap取代了。
其实从继承上就感觉是一个古老的类了,因为它继承自Dictionary<K,V>
,虽然我在C#里经常用字典(C#中没有HashMap),但是听说在Java中已经很少使用字典了。
对于这个类,我们重点关注一下它是如何hash、如何get、如何put的。
对于取Hash操作来说,Hashtable直接使用了Object的hashcode,而非像HashMap那样需要右移,所以基本能看出来已经很久没有优化过了。
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
写时复制
CopyOnWriterArrayList
CopyOnWriter,一般称为写时复制,看着玄乎,其实原理很简单,就是在写入的时候不写入原来的数组而是生成一个新的数组,最后替换引用(修改引用是原子操作,不会存在并发问题)。总的来说这是一个弱一致性的集合,并不能保证读的数据实时一致,但是能够保证数据的最终一致。
基于他的这个特性,那么我们来看一下他的源码,主要关注他是如何进行修改的。
先来看读的源码,非常简单,就是直接读取:
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
return get(getArray(), index);
}
总的来说,读的时候是直接读取旧数组,所以不需要获取锁,但是写的时候由于每次都要生成新的数组,所以必须加上锁(想一想如果不加锁会发生什么)。
public boolean add(E e) {
// 可重入锁
final ReentrantLock lock = this.lock;
// 获取锁
lock.lock();
try {
// 元素数组
Object[] elements = getArray();
// 数组长度
int len = elements.length;
// 复制数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 存放元素e
newElements[len] = e;
// 设置数组
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
除此之外,我们再来学习一下这个集合的迭代器,迭代的时候首先会对这个数组进行一个快照,然后在这个快照中进行迭代,由于是在快照上进行迭代,所以也不支持添加、删除和替换操作。
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
// 快照
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
// 游标
private int cursor;
// 构造函数
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 是否还有下一项
public boolean hasNext() {
return cursor < snapshot.length;
}
// 是否有上一项
public boolean hasPrevious() {
return cursor > 0;
}
// next项
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext()) // 不存在下一项,抛出异常
throw new NoSuchElementException();
// 返回下一项
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
// 下一项索引
public int nextIndex() {
return cursor;
}
// 上一项索引
public int previousIndex() {
return cursor-1;
}
// 不支持remove操作
public void remove() {
throw new UnsupportedOperationException();
}
// 不支持set操作
public void set(E e) {
throw new UnsupportedOperationException();
}
// 不支持add操作
public void add(E e) {
throw new UnsupportedOperationException();
}
@Override
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
Object[] elements = snapshot;
final int size = elements.length;
for (int i = cursor; i < size; i++) {
@SuppressWarnings("unchecked") E e = (E) elements[i];
action.accept(e);
}
cursor = size;
}
}
Join/Fork
Join/Fork是一种技术的名字,其实现原理的是把大的任务拆分成多个小任务,然后交由多个线程执行,最后把结果组合起来。除此之外,Join/Fork框架还有一个很重要的算法:工作窃取算法,如果一个子线程完成了自己的所有任务,它会去其他未完成的所有任务的线程的任务队列中从队列末尾窃取任务,非常形象。
在Java中,Join/Fork技术主要由两个类来实现:
- ForkJoinTask:是一个提供fork、join方法的类,一般我们不需要直接继承这个类,一般我们会选择它的两个子类:RecursiveAction用于没有返回结果的任务;RecursiveTask用于有返回结果的任务。
- ForkJoinPool:也是一个线程池,任务分割出的子任务会加入到双端队列中,如果一个线程完成了它自己的任务就会进行工作窃取。
后记
这篇文章主要对之前遗留的一些集合类进行了粗略的源码阅读,主要关注这些类的添加和删除,以及和之前详细说明的那些类相比有什么不同。
写完了集合拾遗之后,这个Java基础部分的源码阅读就暂时告一段落了,之后开始准备Spring的部分源码阅读 ,希望能够在之后的面试中取得好成绩。
Spring的源码也太难了吧,完全看不懂,还是慢慢来吧。
参考文章:
Java全栈知识体系
《Java并发编程的艺术》