public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {}
它继承了 HashSet 类并实现了Set 接口,结合了 哈希表(Hash Table)
和 双向链表(Doubly-Linked List)
的特性,简单来说 LinkedHashMap = HashMap的哈希桶 + 追加的双向链表
,底层实现依靠LinkedHashMap
1.LinkedHashSet()
创建一个空的 LinkedHashSet,默认初始容量为16,负载因子为0.75,按照插入顺序维护元素
public LinkedHashSet() {super(16, .75f, true);}
2.LinkedHashSet(int initialCapacity)
创建一个具有指定初始容量的 LinkedHashSet,默认负载因子为0.75,按插入顺序维护元素
public LinkedHashSet(int initialCapacity) {super(initialCapacity, .75f, true);}
3.LinkedHashSet(int initialCapacity, float loadFactor)
创建一个具有指定初始容量和负载因子的 LinkedHashSet,按插入顺序维护元素
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);}
4.LinkedHashSet(Collection extends E> c)
创建一个包含指定集合元素的 LinkedHashSet,初始容量足够容纳指定集合的元素,负载因子为0.75,按插入顺序维护元素
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
这里顺便介绍下Spliterator spliterator()方法(分割流:并行流操作
)
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this,
Spliterator.DISTINCT |Spliterator.ORDERED);
}
返回一个具有 ORDERED
和 DISTINCT
特性的 Spliterator,用于遍历元素(通常用于 Stream API)
底层是调用LinkedHashMap的移除新增节点方法LinkedHashMap核心操作介绍
这里单独简单介绍下LinkedHashSet有序性底层实现
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add(null);
linkedHashSet.add("1");
// 当展示linkedHashSet集合时候 会按照插入顺序输出
System.out.println(linkedHashSet);
//实际上是调用了.toString()方法
linkedHashSet.toString();
//对应调用的是抽象类AbstractCollection.toString() 从而调用对于的子迭代器
// E e = it.next();
// LinkedHashSet 的迭代器实现(实际继承自LinkedHashMap)
Iterator<E> iterator() {
return new LinkedKeyIterator(); // 基于链表的迭代器
}
最终执行的迭代方法:迭代器按链表指针顺序从头开始移动
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next = head; // 从头部开始
public final boolean hasNext() { return next != null; }
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
next = e.after; // 关键:始终通过after指针移动到下一个节点 直到尾
return e;
}
}
LinkedHashSet
底层实现依靠LinkedHashMap
,LinkedHashMap 直接支持插入顺序和访问顺序两模式
元素按照插入顺序排序(不是元素大小的自然排序哦),遍历顺序与添加顺序一致
public static void main(String[] args) {
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("B");
linkedHashSet.add("C");
linkedHashSet.add("A");
System.out.println(linkedHashSet);
}
LinkedHashSet 本身不支持访问顺序 (Access-Order),它的设计仅基于插入顺序, 没有提供构造时通过参数切换为访问顺序模式方法
但是LinkedHashMap
可直接支持访问顺序
// LinkedHashMap 支持访问顺序
LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
LinkedHashSet底层实现是LinkedHashMap,所以可通过组合LinkedHashMap模拟访问顺序的Set
// 使用newSetFromMap构造方法:从已只的LinkedHashMap集合创建set集合方法
Set<String> accessOrderSet = Collections.newSetFromMap(
//AccessOrder 设置为true 表明访问顺序
new LinkedHashMap<String, Boolean>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Boolean> eldest) {
return size() > 3; // 可选:LRU 缓存淘汰策略
}
}
);
//测试访问顺序
accessOrderSet.add("A");
accessOrderSet.add("B");
accessOrderSet.add("C");
accessOrderSet.contains("A"); // 访问 A,将其移到末尾
System.out.println(accessOrderSet); // 输出 [B, C, A](最新访问的在末尾)
本质上还是LinkedHashMap使用的访问顺序
这点是set集合中所有实现类通用特性
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
System.out.println( linkedHashSet.add("1"));// 添加成功 true
System.out.println( linkedHashSet.add("1")); // 添加失败 false
原因在于底层调用的LinkedHashMap数据实现是[哈希桶+(链表或红黑树)] + 追加的双向链表
在存储时候抛开追加的双向链表不谈,在存储哈希桶+(链表或红黑树)时候 需要先进行查找定位和数据存在,不存在新增节点 存在则更新旧value值,从而保证键的唯一性质,而判断这个唯一性的规则需要两步骤:1.先哈希导航定位对比哈希值,2.其次在哈希值相同情况下(哈希冲突) 对比具体值
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return true; // 判定为重复元素
所以这也要求了 所有自定义类需要实现对象属性值唯一性
就重写实现 HashCode
方法和equals
方法 保证唯一性,不然会无法保证唯一性
class SetItemTest {
private String id;
private String name;
public SetItemTest(String id, String name) {
this.id = id;
this.name = name;
}
}
// 自定义对象SetItemTest 属性两个字段
LinkedHashSet<SetItemTest> linkedHashSet = new LinkedHashSet<>();
//创建两个对象,对象内容一样
SetItemTest setItemTest1 = new SetItemTest("1","测试用例1");
SetItemTest setItemTest2 = new SetItemTest("1","测试用例1");
// 添加成功时候如果没有重写 hashCode,这两个对象hashCode基于内存是不一样的
//如果没有 重写hashCode,所有对象属性值一样的对象都会认为不一样的
System.out.println( linkedHashSet.add(setItemTest1));// 添加成功 true
System.out.println( linkedHashSet.add(setItemTest2));//添加成功 true
自定义类hashCoed值的重写简单示例
@Override
public int hashCode(){
return id.hashCode() + name.hashCode();
}
//再来测试
System.out.println( linkedHashSet.add(setItemTest1));// 添加成功 true
System.out.println( linkedHashSet.add(setItemTest2));//添加失败 false
3.高效性
基于哈希表实现,查找/删除操作时间复杂度接近 O(1)(理想哈希条件下)。
双向链表:节点的先后插入顺序,通过前后指针链接
哈希桶+(链表或红黑树):数据存储的位置
两者的数据结构是并行存在的
如果是新增节点 双向链表的添加是优于 哈希桶的
如果是删除节点 先哈希桶移除节点 再从双向链表中移除 并更新维护前后指针
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add(null);
底层原理:
键为null的条目,被固定存储在数组索引0的桶bucket(即table[0]),始终以链表节点 Node 形式存储,永远不会转换为树节点 TreeNode
追加的双向链表是不关系节点中键属性是否为null的节点
,总体来说只有树结构节点才关系键为空,因为二叉搜索树需要比较key值定位查找树节点,使用TreeMap和TreeSet这样只有树结构数据的不允许null键值(null元素)
LinkedHashMap是线程不安全,所以LinkedHashSet也是线程不安全 的
1.数据不一致:由于缺乏同步,多个线程的修改可能导致内部链表和哈希表状态不一致
2.迭代时抛出异常 ConcurrentModificationException:一个线程迭代时,另一个线程修改了集合
3.丢失更新:多个线程同时添加元素,可能导致只有一个线程的添加操作生效
方式一:使用 Collections.synchronizedSet 包装 LinkedHashSet
Set<String> synchronizedSet = Collections.synchronizedSet(new LinkedHashSet<>());
// 使用示例
synchronized(synchronizedSet) {
synchronizedSet.add("item");
// 所有操作必须在同步块内
}
每次方法调用都会在同一个锁上同步。但需要注意的是,在迭代时必须手动加锁
,否则仍然可能抛出ConcurrentModificationException
方式二:使用并发集合类ConcurrentHashMap的实现类
a.使用ConcurrentLinkedHashMap
原理:通过自增序号保证顺序,ConcurrentHashMap 提供并发安全
// 使用 ConcurrentHashMap + AtomicLong 模拟有序集合
Map<Long, E> orderedMap = new ConcurrentHashMap<>();
AtomicLong counter = new AtomicLong();
// 添加元素
void add(E e) {
orderedMap.put(counter.getAndIncrement(), e);
}
// 遍历(保留插入顺序)
List<E> orderedList = new ArrayList<>(orderedMap.values());
b.使用CopyOnWriteArraySet
底层实现基于 CopyOnWriteArrayList:元素存储使用常规数组,严格按添加顺序存储
public static void main(String[] args) {
Set<String> safeSet = new CopyOnWriteArraySet<>();
safeSet.add("A");
safeSet.add("B");
safeSet.add("C");
System.out.println(safeSet);//[A, B, C]
// 迭代器定义在前 添加元素BreakIteration在后
Iterator<String> it = safeSet.iterator();
safeSet.add("BreakIteration"); // 新增元素不影响迭代器it 即迭代器的不可变性
while(it.hasNext()) {
System.out.println(it.next()); // 不会看到新元素 只输出A B C
// it.remove() // 抛出 UnsupportedOperationException
}
System.out.println(safeSet);//[A, B, C, BreakIteration]
safeSet.remove("A");
System.out.println(safeSet);//[B, C, BreakIteration]
}
注意事项:
1.CopyOnWriteArraySet虽然 严格保证插入顺序 并且完全线程安全
, 但它的致命缺陷是空间消耗大
写请求性能贼差
:每次修改产生 O(n) 内存分配,触发频繁GC(尤其是大集合
2.迭代器的不可变性:迭代过程中,即使集合被修改,迭代器也不会反映这些修改。因此,从迭代器的视角看,它遍历的是一个不可变的快照
方式三:使用显式锁(如ReentrantLock)或synchronized关键字手动控制
public class SafeLinkedHashSet<E> {
private final LinkedHashSet<E> set = new LinkedHashSet<>();
//使用显式锁(如ReentrantLock)确保在任何时候只有一个线程操作集合
private final ReentrantLock lock = new ReentrantLock();
public void add(E e) {
lock.lock();
try {
set.add(e);
} finally {
lock.unlock();
}
}
// 其他方法类似实现...
}
方式四:使用线程安全的ConcurrentSkipListSet的替代方案 (最佳实践)
可以使用ConcurrentSkipListSet
,基于跳表结构 无锁基于CAS操作,但它只能按自然顺序或Comparator排序,而不是插入顺序
// 使用 ConcurrentSkipListSet (基于跳表)
Set<E> safeSet = new ConcurrentSkipListSet<>(Comparator.comparing(System::identityHashCode));
选型建议 与总结
低频修改+强顺序要求 → CopyOnWriteArraySet
高频并发+弱顺序容忍 → ConcurrentSkipListSet
高频并发+强顺序要求 → ConcurrentHashMap+序号 方案
兼容旧代码 → Collections.synchronizedSet + 严格同步控制
总结原则:
读多写少选 CopyOnWriteArraySet
写多读少选 ConcurrentHashMap 方案
均衡负载选 ConcurrentSkipListSet
优点:在 Set 唯一性基础上提供 插入顺序遍历,查找性能高效。
缺点:内存占用稍高,不适用于排序需求(需排序时改用 TreeSet)。
适用场景:当需要 去重+保持插入顺序
时,替代 ArrayList 去重+保序的场景(避免手动去重)LinkedHashSet 是最佳选择。