先说结论:
LinkedHashSet与HashSet的区别,就是LinkedHashSet存在有序性,即数据存入的顺序与取出的顺序一致
LinkedHashSet是HashSet的子类,其构造方法内部均调用其父类HashSet的构造方法;而被调用的HashSet的构造方法内部调用LinkedHashMap的构造方法。所以LinkedHashSet的底层,即为LinkedHashMap
LinkedHashSet中数据的存储方式为:数组 + 单向链表 + 双向链表
数组:指table数组,继承自HashMap,类型为HashMap$Node,存放的数据类型为LinkedHashSet$Entry
单项链表:数组中,一个索引处存在一个单向链表;next属性指向下一结点
双向链表:构成一个个单向链表的结点,一同构成的双向链表;before属性指向前一个被添加进来的元素,after指向后一个被添加进来的结点;LinkedHashSet中存在两个属性:head与tail,head指向双向链表的头结点,tail指向双向链表的尾结点。正是该双向链表,实现了LinkedHashSet的有序性
文末有对于数组 + 单向链表 + 双向链表模式的比喻
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
关于Entry与Node:
LinkedHashMap中存在静态内部类Entry,其内部维护的数组table中存放的数据类型即为Entry;数组table继承自其父类HashMap,类型为HashMap$Node,而之所以table数组能存放Entry类型的数据,是因为Entry继承了Node
Entry中除了继承自其父类Node的属性和方法外,自身拥有两个属性:before与after。其中,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);
}
}
关于head与tail及双向链表的链接:
LinkedHashMap中,存在两个属性:head与tail,其中head指向最先被添加进来的元素,tail指向最后被添加进来的元素
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
双向链表的链接,是通过linkNodeLast方法实现的
进入方法后,先创建局部变量last,并将tail的引用赋值给last,此时last即为旧尾结点
而后将新尾结点p的引用赋值给tail,完成了tail的更新
接下来进行判断,新尾结点p是table数组中的第一个元素时,将head与tail一同指向p,否则进行链表链接:将新尾结点p的before属性指向旧尾结点last,将旧尾结点last的after属性指向新尾结点p
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;
}
}
LinkedHashMap中重写了newNode方法,让该方法在返回一个新建结点的同时,调用linkNodeLast方法(newNode方法在HashMap的putVal方法中被调用,putVal方法在put方法中被调用,put方法在HashSet的add方法中被调用)
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;
}
此时有趣的现象出现了,数组的一个索引对应着一条单向链表(若该索引处存储了元素的话),而组成一条条单向链表的结点,一同组成了一条双向链表,正是这条双向链表让LinkedHashSet存在有序性。
关于数组 + 单向链表 + 双向链表的比喻:
一批求职者(元素)来到工厂(集合)面试(尝试向集合中添加元素),工厂根据求职者的求职意向(hash)将他们分配到不同的车间(索引)面试。面试合格者(准备添加的元素与索引对应的单向链表中的元素不重复)将被招入工厂,并分配工号(链接双向链表)与车间工号(链接单向链表),车间工号分配到9时,车间将会升级为大车间(树化)。工厂有时会进行点名(输出集合中的元素),点名的顺序依工号而定