Java基础 集合框架 之Set框架之LinkedHashSet

LinkedHashSet

  • LinkedHashSet构造方法
    • LinkedHashSet 底层数据结构及实现原理
  • LinkedHashSet核心特性
    • 有序性
      • 插入顺序排序
      • 访问顺序排序(LRU)
    • 元素唯一性
    • 底层数据结构:[哈希桶+(链表或红黑树)] + 追加的双向链表
    • 允许null值
    • LinkedHashSet线程不安全
      • LinkedHashSet线程不安全体现
      • 解决方案
  • LinkedHashSet 优缺点以及适用场景

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {}

它继承了 HashSet 类并实现了Set 接口,结合了 哈希表(Hash Table)双向链表(Doubly-Linked List) 的特性,简单来说 LinkedHashMap = HashMap的哈希桶 + 追加的双向链表,底层实现依靠LinkedHashMap

LinkedHashSet构造方法

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 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);
    }

返回一个具有 ORDEREDDISTINCT 特性的 Spliterator,用于遍历元素(通常用于 Stream API)

LinkedHashSet 底层数据结构及实现原理

底层是调用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核心特性

有序性

LinkedHashSet底层实现依靠LinkedHashMapLinkedHashMap 直接支持插入顺序和访问顺序两模式

插入顺序排序

元素按照插入顺序排序(不是元素大小的自然排序哦),遍历顺序与添加顺序一致

    public static void main(String[] args) {
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
        linkedHashSet.add("B");
        linkedHashSet.add("C");
        linkedHashSet.add("A");
        System.out.println(linkedHashSet);
    }

访问顺序排序(LRU)

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)(理想哈希条件下)。

底层数据结构:[哈希桶+(链表或红黑树)] + 追加的双向链表

双向链表:节点的先后插入顺序,通过前后指针链接
哈希桶+(链表或红黑树):数据存储的位置

两者的数据结构是并行存在的
如果是新增节点 双向链表的添加是优于 哈希桶的
如果是删除节点 先哈希桶移除节点 再从双向链表中移除 并更新维护前后指针

允许null值

        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
        linkedHashSet.add(null);

底层原理:
键为null的条目,被固定存储在数组索引0的桶bucket(即table[0]),始终以链表节点 Node 形式存储,永远不会转换为树节点 TreeNode

追加的双向链表是不关系节点中键属性是否为null的节点,总体来说只有树结构节点才关系键为空,因为二叉搜索树需要比较key值定位查找树节点,使用TreeMap和TreeSet这样只有树结构数据的不允许null键值(null元素)

LinkedHashSet线程不安全

LinkedHashMap是线程不安全,所以LinkedHashSet也是线程不安全 的

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

LinkedHashSet 优缺点以及适用场景

优点:在 Set 唯一性基础上提供 插入顺序遍历,查找性能高效。
缺点:内存占用稍高,不适用于排序需求(需排序时改用 TreeSet)。
适用场景:当需要 去重+保持插入顺序 时,替代 ArrayList 去重+保序的场景(避免手动去重)LinkedHashSet 是最佳选择。

你可能感兴趣的:(集合框架之Set,java,开发语言)