深度解析LinkedHashSet工作原理

一、引言

在 Java 的集合框架中,LinkedHashSet 是一个独特且实用的类。它结合了 HashSet 的元素唯一性和链表的有序性,既能够确保存储的元素不重复,又能按照元素插入的顺序或者访问的顺序来维护元素。本文将深入探讨 LinkedHashSet 的原理,包括其底层数据结构、核心属性、构造方法、常用操作的实现细节,还会结合丰富的代码示例进行详细说明。

二、LinkedHashSet 概述

2.1 定义与用途

LinkedHashSet 是 Java 集合框架中 Set 接口的一个具体实现类,位于 java.util 包下。它继承自 HashSet,并额外维护了一个双向链表,用于记录元素的插入顺序或者访问顺序。这使得 LinkedHashSet 非常适合那些需要保持元素插入顺序,同时又要保证元素唯一性的场景,例如缓存、历史记录管理等。

2.2 继承关系与实现接口

LinkedHashSet 的继承关系如下:

plaintext 复制代码
java.lang.Object
    └─ java.util.AbstractCollection<E>
        └─ java.util.AbstractSet<E>
            └─ java.util.HashSet<E>
                └─ java.util.LinkedHashSet<E>

它实现了 SetCloneablejava.io.Serializable 接口,具备集合的基本操作,支持克隆和序列化。

java 复制代码
import java.util.LinkedHashSet;
import java.util.Set;

public class LinkedHashSetOverview {
    public static void main(String[] args) {
        // 创建一个 LinkedHashSet 对象
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
        // 可以将其赋值给 Set 接口类型的变量
        Set<String> set = linkedHashSet;
    }
}

三、底层数据结构:哈希表与双向链表

3.1 哈希表的作用

LinkedHashSet 基于 HashSet 实现,而 HashSet 底层是基于哈希表(实际上是 HashMap)。哈希表通过哈希函数将元素映射到一个固定大小的数组中的某个位置,这个位置称为桶(Bucket)。当多个元素映射到同一个桶时,就会发生哈希冲突。LinkedHashSet 使用链地址法来解决哈希冲突,即每个桶中存储一个链表,链表中的节点存储元素。

3.2 双向链表的作用

LinkedHashSet 额外维护了一个双向链表,链表中的节点不仅包含元素本身,还包含指向前一个节点和后一个节点的引用。这个双向链表按照元素的插入顺序或者访问顺序将所有元素连接起来,从而保证了元素的有序性。

3.3 节点结构

LinkedHashSet 中的节点是 LinkedHashMap.Entry 类型,它继承自 HashMap.Node,并额外添加了 beforeafter 两个引用,用于维护双向链表的顺序。

java 复制代码
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);
    }
}

四、核心属性

LinkedHashSet 本身没有定义太多的核心属性,主要依赖于 HashSetLinkedHashMap 的属性。其中,LinkedHashMapLinkedHashSet 底层实际使用的数据结构,它有以下几个重要属性:

java 复制代码
// 双向链表的头节点
transient LinkedHashMap.Entry<K,V> head;
// 双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
// 访问顺序标志,默认为 false,表示按照插入顺序
final boolean accessOrder;
  • head:指向双向链表的头节点,即最早插入的元素。
  • tail:指向双向链表的尾节点,即最晚插入的元素。
  • accessOrder:一个布尔值,用于控制双向链表的顺序。如果为 false,则按照元素的插入顺序;如果为 true,则按照元素的访问顺序。

五、构造方法

5.1 无参构造方法

java 复制代码
public LinkedHashSet() {
    super(16, .75f, true);
}

无参构造方法调用父类 HashSet 的构造方法,创建一个初始容量为 16,负载因子为 0.75 的 LinkedHashSet,并将 accessOrder 设置为 true,表示按照插入顺序维护元素。

5.2 指定初始容量的构造方法

java 复制代码
public LinkedHashSet(int initialCapacity) {
    super(initialCapacity, .75f, true);
}

该构造方法允许指定 LinkedHashSet 的初始容量,负载因子默认为 0.75,同样按照插入顺序维护元素。

5.3 指定初始容量和负载因子的构造方法

java 复制代码
public LinkedHashSet(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor, true);
}

此构造方法允许同时指定初始容量和负载因子,accessOrder 仍然为 true

5.4 从其他集合创建 LinkedHashSet 的构造方法

java 复制代码
public LinkedHashSet(Collection<? extends E> c) {
    super(Math.max(2*c.size(), 11), .75f, true);
    addAll(c);
}

该构造方法接受一个集合作为参数,将集合中的元素添加到新创建的 LinkedHashSet 中。初始容量根据传入集合的大小进行调整。

java 复制代码
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;

public class LinkedHashSetConstructors {
    public static void main(String[] args) {
        // 无参构造方法
        LinkedHashSet<String> linkedHashSet1 = new LinkedHashSet<>();

        // 指定初始容量的构造方法
        LinkedHashSet<String> linkedHashSet2 = new LinkedHashSet<>(20);

        // 指定初始容量和负载因子的构造方法
        LinkedHashSet<String> linkedHashSet3 = new LinkedHashSet<>(15, 0.8f);

        // 从其他集合创建 LinkedHashSet 的构造方法
        Collection<String> collection = new ArrayList<>();
        collection.add("apple");
        collection.add("banana");
        LinkedHashSet<String> linkedHashSet4 = new LinkedHashSet<>(collection);
        System.out.println(linkedHashSet4);
    }
}

六、常用操作原理

6.1 添加元素

java 复制代码
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

add 方法实际上是调用 LinkedHashMapput 方法将元素作为键,PRESENT 作为值存储到 LinkedHashMap 中。在插入元素时,LinkedHashMap 会先计算元素的哈希值,找到对应的桶,然后遍历桶中的链表,检查元素是否已经存在。如果元素不存在,则创建一个新的 LinkedHashMap.Entry 节点,并将其插入到哈希表的桶中,同时将该节点添加到双向链表的尾部。

6.2 检查元素是否存在

java 复制代码
public boolean contains(Object o) {
    return map.containsKey(o);
}

contains 方法调用 LinkedHashMapcontainsKey 方法来检查指定元素是否存在于 LinkedHashSet 中。它会根据元素的哈希值找到对应的桶,然后遍历桶中的链表进行查找。

6.3 删除元素

java 复制代码
public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

remove 方法调用 LinkedHashMapremove 方法来删除指定元素。在删除元素时,LinkedHashMap 会先找到该元素对应的节点,然后将其从哈希表的桶中移除,同时将该节点从双向链表中移除。

6.4 获取元素数量

java 复制代码
public int size() {
    return map.size();
}

size 方法调用 LinkedHashMapsize 方法返回 LinkedHashSet 中元素的数量。

java 复制代码
import java.util.LinkedHashSet;

public class LinkedHashSetOperations {
    public static void main(String[] args) {
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();

        // 添加元素
        linkedHashSet.add("apple");
        linkedHashSet.add("banana");
        linkedHashSet.add("cherry");

        // 检查元素是否存在
        System.out.println("Contains apple: " + linkedHashSet.contains("apple"));

        // 删除元素
        linkedHashSet.remove("banana");
        System.out.println("After removing banana: " + linkedHashSet);

        // 获取元素数量
        System.out.println("Size: " + linkedHashSet.size());
    }
}

七、迭代器原理

LinkedHashSet 的迭代器是基于双向链表实现的,因此迭代器会按照元素的插入顺序或者访问顺序依次访问元素。迭代器的实现相对简单,只需要从双向链表的头节点开始,依次访问每个节点即可。

java 复制代码
import java.util.Iterator;
import java.util.LinkedHashSet;

public class LinkedHashSetIterator {
    public static void main(String[] args) {
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
        linkedHashSet.add("apple");
        linkedHashSet.add("banana");
        linkedHashSet.add("cherry");

        Iterator<String> iterator = linkedHashSet.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

八、性能分析

8.1 时间复杂度

  • 插入操作:插入操作的平均时间复杂度为 O(1),因为哈希表可以通过哈希函数快速定位到桶的位置,在没有哈希冲突的情况下,插入操作可以在常数时间内完成。但在极端情况下,当所有元素都映射到同一个桶时,插入操作的时间复杂度会退化为 O(n)。
  • 查找操作:查找操作的平均时间复杂度为 O(1),同样可以通过哈希函数快速定位到桶的位置,然后在桶中查找元素。
  • 删除操作:删除操作的平均时间复杂度为 O(1),通过哈希函数定位到桶的位置,然后在桶中删除元素,并从双向链表中移除该节点。

8.2 空间复杂度

LinkedHashSet 的空间复杂度为 O(n),主要用于存储哈希表和双向链表中的节点。

九、注意事项

9.1 元素的哈希码和相等性

LinkedHashSet 判断元素是否重复是基于元素的 hashCode()equals() 方法。因此,存储在 LinkedHashSet 中的元素必须正确重写这两个方法,否则可能会导致元素重复存储或查找失败。

9.2 线程安全问题

LinkedHashSet 不是线程安全的。如果在多线程环境下需要使用线程安全的集合,可以考虑使用 Collections.synchronizedSet() 方法将 LinkedHashSet 包装成线程安全的集合。

java 复制代码
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

public class LinkedHashSetThreadSafety {
    public static void main(String[] args) {
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
        Set<String> synchronizedSet = Collections.synchronizedSet(linkedHashSet);
    }
}

十、总结

LinkedHashSet 是 Java 集合框架中一个非常实用的类,它结合了 HashSet 的元素唯一性和链表的有序性。通过哈希表和双向链表的组合,LinkedHashSet 能够高效地进行元素的插入、查找和删除操作,同时保持元素的插入顺序或访问顺序。在使用 LinkedHashSet 时,需要注意元素的哈希码和相等性的重写,以及线程安全问题。深入理解 LinkedHashSet 的原理和性能特点,有助于我们在实际开发中合理地使用它,提高程序的性能和可靠性。

相关推荐
恋猫de小郭1 小时前
Android Studio Cloud 正式上线,不只是 Android,随时随地改 bug
android·前端·flutter
我命由我123452 小时前
35.Java线程池(线程池概述、线程池的架构、线程池的种类与创建、线程池的底层原理、线程池的工作流程、线程池的拒绝策略、自定义线程池)
java·服务器·开发语言·jvm·后端·架构·java-ee
CopyLower3 小时前
分布式ID生成方案的深度解析与Java实现
java·开发语言·分布式
m0_684598536 小时前
如何开发英语在线训练小程序:从0到1的详细步骤
java·微信小程序·小程序·小程序开发
ml130185288746 小时前
开发一个环保回收小程序需要哪些功能?环保回收小程序
java·大数据·微信小程序·小程序·开源软件
匹马夕阳6 小时前
(十八)安卓开发中的后端接口调用详讲解
android
zybishe7 小时前
免费送源码:Java+ssm+MySQL 酒店预订管理系统的设计与实现 计算机毕业设计原创定制
java·大数据·python·mysql·微信小程序·php·课程设计
Pigwantofly8 小时前
鸿蒙ArkTS实战:从零打造智能表达式计算器(附状态管理+路由传参核心实现)
android·华为·harmonyos
anlogic8 小时前
Java基础 4.12
java·开发语言
weisian1518 小时前
Java常用工具算法-7--秘钥托管云服务2(阿里云 KMS)
java·安全·阿里云