Java中级编程大师班<第一篇:初识数据结构与算法-数组(3)>

在上一章中,我们深入了解了 HashMapHashTable 的底层代码原理,包括它们的数据结构和工作原理。在本章中,我们将继续探讨 ConcurrentHashMapHashSetLinkedHashMap 这些与 HashMap 有关的关键数据结构,深入了解它们的实现方式以及各自的特性和用途。让我们继续探索这些重要的 Java 集合类。如果您有任何特定的问题或需要更多详细信息,请随时提出。

ConcurrentHashMap

ConcurrentHashMap 是 Java 集合框架中的一个重要类,它提供了高度并发的哈希表实现。与普通的 HashMap 不同,ConcurrentHashMap 允许多个线程同时读取和写入,而不会导致数据不一致或死锁。它在多线程环境中提供了出色的性能和可伸缩性,并且在 Java 5 及以后的版本中引入。

以下是 ConcurrentHashMap 的一些关键特性和用途:

  1. 线程安全性ConcurrentHashMap 是线程安全的数据结构。它通过使用分段锁机制来实现高度并发的读取和写入操作。这意味着多个线程可以同时读取不同部分的哈希表,而不会互相阻塞。这种设计使得在多线程环境中可以实现更高的性能。

  2. 分段锁ConcurrentHashMap 内部使用多个独立的哈希表段(segment),每个段可以看作是一个小的哈希表。每个段都有自己的锁,当线程访问某一段时,只有该段被锁定,其他段仍然可以被并发访问。这种分段锁设计允许多个线程同时执行读操作,只有写操作需要锁定对应的段。

  3. 高性能:由于并发读取操作不会阻塞,ConcurrentHashMap 在读多写少的场景中表现出色。它是处理高并发读操作的理想选择。

  4. 支持高并发写操作:虽然 ConcurrentHashMap 的主要优势在于读操作的高并发性能,但它仍然支持高并发的写操作,因为不同的段可以被不同线程同时锁定。

  5. 可伸缩性ConcurrentHashMap 具有良好的可伸缩性。您可以根据需要增加哈希表的段数,以适应更多的并发访问。这使得它适用于不同规模的应用。

  6. 迭代性能ConcurrentHashMap 的迭代性能也得到了优化,使得在迭代哈希表中的元素时能够获得高效的性能。

  7. 高级功能ConcurrentHashMap 提供了许多高级功能,如支持自定义并发级别、compute 方法用于原子更新、merge 方法用于原子合并等。

总之,ConcurrentHashMap 是一个强大的多线程环境中的哈希表实现,它允许高度并发的读取和写入操作,并具有出色的性能和可伸缩性。它通常用于需要高并发访问的场景,例如多线程应用程序中的缓存、计数器、任务分配等。

示例代码 - 使用 ConcurrentHashMap

以下是一个简单的示例代码,演示如何使用 ConcurrentHashMap 来管理多个线程安全的计数器。在这个示例中,我们创建一个 ConcurrentHashMap,其中键是字符串,值是整数计数器。多个线程可以同时增加和获取计数器的值,而不会发生竞争条件。

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {

    public static void main(String[] args) {
        // 创建一个 ConcurrentHashMap
        ConcurrentHashMap<String, Integer> counterMap = new ConcurrentHashMap<>();

        // 启动多个线程来增加计数器的值
        for (int i = 0; i < 5; i++) {
            String key = "counter" + i;
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    // 使用 compute 方法来原子地更新计数器
                    counterMap.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
                }
            });
            thread.start();
        }

        // 等待所有线程完成
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印每个计数器的值
        counterMap.forEach((key, value) -> {
            System.out.println(key + ": " + value);
        });
    }
}

在这个示例中,我们创建了一个 ConcurrentHashMap 来存储多个计数器,每个计数器都有一个唯一的键(例如,“counter0”、“counter1” 等)。然后,我们启动了多个线程,每个线程对计数器进行增加操作。使用 compute 方法来原子地更新计数器的值,确保线程安全。最后,我们打印出每个计数器的最终值。

ConcurrentHashMap 在这种情况下非常适合,因为它允许多个线程同时访问和修改不同的计数器,而不会出现竞争条件。这是一个简单的示例,演示了如何在多线程环境中使用 ConcurrentHashMap

ConcurrentHashMap底层代码解析

ConcurrentHashMap 是 Java 集合框架中的一个关键类,它提供了高并发性能的哈希表实现。在内部,ConcurrentHashMap 使用了分段锁机制,允许多个线程同时读取和写入,而不会出现锁竞争的情况。

以下是 ConcurrentHashMap 的主要代码片段,我将添加注释来解释其关键部分:

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
    implements ConcurrentMap<K, V>, Serializable {

    // 哈希表的默认初始容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    // 哈希表的最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 段的数量,默认为 16
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    // 一个段的最小容量
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    // 每个段内部存储键值对的数组
    transient volatile Segment<K, V>[] segments;

    // ...

    // 内部类,表示哈希表的段
    static final class Segment<K, V> extends ReentrantLock implements Serializable {
        // ...

        // 存储键值对的数组
        transient volatile HashEntry<K, V>[] table;

        // ...

        // 内部类,表示哈希表的节点
        static final class HashEntry<K, V> {
            final int hash;
            final K key;
            volatile V value;
            final HashEntry<K, V> next;

            // ...
        }

        // ...
    }

    // ...

    // 内部方法,用于获取段的索引
    final int segmentFor(int hash) {
        return (hash >>> segmentShift) & segmentMask;
    }

    // 内部方法,用于插入键值对
    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        // ...
    }

    // ...

    // 其他方法,如get, remove, size, clear, keySet, values, entrySet 等
}

上述代码摘录展示了 ConcurrentHashMap 的主要结构和关键部分。它使用了分段锁的思想,将整个哈希表划分为多个段(Segment),每个段都是一个独立的哈希表,具有自己的锁。这意味着不同的线程可以同时访问不同的段,而不会相互阻塞。每个段内部使用数组来存储键值对,具有自己的哈希冲突解决机制。

ConcurrentHashMap 在并发读取和写入的场景中表现出色,因为不同的线程可以同时操作不同的段,而不会出现锁竞争。这使得它成为处理高并发情况下的理想选择。

是对其中一部分核心代码进行更详细的分析和讲解。

1. put 方法核心代码分析

put 方法用于向 ConcurrentHashMap 中插入键值对。以下是 put 方法的核心代码,我将逐行进行讲解和分析:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    Segment<K, V> s; // 获取要操作的段
    if (value == null)
        return null;
    if ((s = segmentFor(hash)) == null)
        throw new NullPointerException();
    return s.put(key, hash, value, onlyIfAbsent); // 调用段的 put 方法
}
  • Segment s;:首先声明了一个 Segment 对象 s,用于表示要操作的段。

  • if (value == null) return null;:检查传入的值是否为 null,如果是 null,则直接返回 null

  • if ((s = segmentFor(hash)) == null):通过 segmentFor 方法根据键的哈希值确定要操作的段,并将其赋值给 s。如果 segmentFor 返回 null,则抛出 NullPointerException 异常。

  • return s.put(key, hash, value, onlyIfAbsent);:最后,调用确定的段 sput 方法进行实际插入操作,传递键 key、哈希值 hash、值 valueonlyIfAbsent 参数。

2. get 方法核心代码分析

get 方法用于根据键获取值。以下是 get 方法的核心代码,我将逐行进行讲解和分析:

//会发现源码中没有一处加了锁
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //计算hash
    int h = spread(key.hashCode()); 
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
        //读取首节点的Node元素
        if ((eh = e.hash) == h) {
            //如果该节点就是首节点就返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        //eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
        //eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
        //eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
            if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
  • int hash = hash(key.hashCode());:首先,根据传入键的哈希码计算哈希值 hash

  • return segmentFor(hash).get(key, hash);:然后,通过 segmentFor 方法根据哈希值确定要操作的段,然后调用该段的 get 方法,传递键 key 和哈希值 hash 来获取值。

3. remove 方法核心代码分析

remove 方法用于删除指定键的键值对。以下是 remove 方法的核心代码,我将逐行进行讲解和分析:

public V remove(Object key) {
    int hash = hash(key.hashCode());
    return segmentFor(hash).remove(key, hash, null);
}
  • int hash = hash(key.hashCode());:首先,根据传入键的哈希码计算哈希值 hash

  • return segmentFor(hash).remove(key, hash, null);:然后,通过 segmentFor 方法根据哈希值确定要操作的段,然后调用该段的 remove 方法,传递键 key、哈希值 hash 和一个 null 参数来删除键值对。

4. resize 方法核心代码分析

resize 方法用于扩容哈希表。以下是 resize 方法的核心代码,我将逐行进行讲解和分析:

final void reinitialize() {
    if (table != null) {
        for (int i = 0; i < segments.length; ++i)
            segments[i].clear(); // 清空各个段
    }
    table = null;
    sizeCtl = 0;
}
  • if (table != null):首先,检查哈希表是否已经创建(table 不为 null)。

  • for (int i = 0; i < segments.length; ++i):然后,遍历各个段,执行清空操作。

  • segments[i].clear();:通过调用 clear 方法清空各个段内的数据。

  • table = null;:最后,将哈希表 table 置为 null

  • sizeCtl = 0;:重置哈希表的控制变量 sizeCtl

这些是 ConcurrentHashMap 中的一些核心方法的关键代码分析。理解这些代码有助于了解 ConcurrentHashMap 的内部实现和线程安全机制。
ConcurrentHashMap 中的分段锁是通过将整个哈希表分成多个段(Segment)来实现的,每个段都拥有自己的锁。这样,多个线程可以同时访问不同段的数据,而不会相互阻塞,从而提高了并发性能。

分段锁

下面是分段锁的原理分析和代码讲解:

原理分析:

  1. 分段:ConcurrentHashMap 通过将哈希表分成多个段来实现并发控制。每个段是一个独立的哈希表,拥有自己的锁。这样,不同段之间的操作可以并行进行,提高了并发性能。

  2. 锁粒度:每个段内部的锁粒度较小,仅锁定该段内的数据。这允许多个线程在不同段上并发执行操作,减小了锁的竞争。

  3. 分段数:分段的数量通常与 ConcurrentHashMap 的初始化容量相关,每个段负责管理一部分键值对。随着数据的增加,ConcurrentHashMap 可以动态地增加段的数量,以保持并发性能。

代码讲解:

以下是 ConcurrentHashMap 中分段锁的代码示例:

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
    implements ConcurrentMap<K, V>, Serializable {
    
    // ...

    static final class Segment<K, V> extends ReentrantLock implements Serializable {
        // ...

        // 存储键值对的数组
        transient volatile HashEntry<K, V>[] table;

        // ...

        // 内部类,表示哈希表的节点
        static final class HashEntry<K, V> {
            final int hash;
            final K key;
            volatile V value;
            final HashEntry<K, V> next;

            // ...
        }

        // ...
    }

    // ...
}
  • ConcurrentHashMap 中的每个段是 Segment 类的实例,它继承自 ReentrantLock,表示一个可重入锁。

  • Segment 内部有一个存储键值对的数组 table,用于存储数据。不同段拥有不同的 table

  • 在每个段内部,有一个内部类 HashEntry,表示哈希表的节点。键值对被存储在 HashEntry 中。哈希冲突时,会形成链表或红黑树结构。

  • Segment 内部的锁用于保护该段的数据结构,以确保多个线程同时访问该段时的线程安全性。

总结来说,ConcurrentHashMap 通过将哈希表分成多个段,并为每个段提供独立的锁,实现了分段锁机制。这种设计允许多个线程在不同段上并发执行操作,提高了并发性能,同时保持了线程安全。

哈希桶/链表/红黑树

ConcurrentHashMap 内部使用了哈希桶、链表或红黑树等数据结构来存储键值对,具体的数据结构选择取决于哈希冲突的情况。以下是这些数据结构的原理讲解和代码分析:

1. 哈希桶 (Hash Bucket) 原理:

  • 哈希桶是一种数组结构,用于存储键值对。ConcurrentHashMap 将哈希表的每个段分成多个哈希桶,每个哈希桶负责存储一部分键值对。

  • 哈希函数将键映射到特定的哈希桶,通常是通过取哈希码的某个部分再取模操作来实现的。

  • 哈希冲突:当不同键映射到同一个哈希桶时,发生哈希冲突。此时,键值对将以链表的形式存储在该哈希桶中。

2. 链表 (Linked List) 原理:

  • 链表是一种线性数据结构,用于存储键值对。当哈希冲突发生时,新的键值对将被追加到链表的末尾。

  • 链表的查询操作需要顺序扫描链表,时间复杂度为 O(n),其中 n 是链表的长度。

3. 红黑树 (Red-Black Tree) 原理:

  • 红黑树是一种自平衡二叉搜索树,用于存储键值对。当链表中的键值对数量达到一定阈值时,链表会被转换为红黑树,以提高查询效率。

  • 红黑树的查询操作具有较低的时间复杂度 O(log n),其中 n 是树的高度。

下面是相关代码分析:

哈希桶的代码示例:

static final class Segment<K, V> extends ReentrantLock implements Serializable {
    // 存储键值对的哈希桶
    transient volatile HashEntry<K, V>[] table;

    // ...
}

在每个 Segment 内部,有一个数组 table 用于存储键值对。不同的哈希桶可能有不同的 table

链表的代码示例:

static final class HashEntry<K, V> {
    final int hash;
    final K key;
    volatile V value;
    final HashEntry<K, V> next;

    // ...
}

HashEntry 中,next 字段用于链接下一个哈希冲突的键值对,从而形成链表结构。

红黑树的代码示例:

static final class TreeNode<K, V> extends Node<K, V> {
    TreeNode<K, V> parent;  // 父节点
    TreeNode<K, V> left;    // 左子节点
    TreeNode<K, V> right;   // 右子节点
    TreeNode<K, V> prev;    // 链表中的前一个节点
    boolean red;            // 是否为红色节点

    // ...
}

ConcurrentHashMap 中,链表可以在键值对数量达到一定阈值后转换为红黑树。TreeNode 表示红黑树中的节点,其中的 parentleftright 等字段用于表示树结构。

这些数据结构的选择和管理使 ConcurrentHashMap 能够在高并发场景中高效地存储和检索键值对,并且在哈希冲突较多时自动升级为红黑树以提高查询性能。这些结构的组合是 ConcurrentHashMap 实现高效并发操作的关键。

和HashMap区别

ConcurrentHashMapHashMap 都是 Java 中用于存储键值对的集合类,但它们在并发性和线程安全性上有显著的区别。以下是它们的主要区别:

  1. 线程安全性

    • ConcurrentHashMap 是线程安全的数据结构,它可以被多个线程同时访问和修改,而不需要额外的同步操作。它通过使用分段锁(Segment)来实现并发控制,每个段拥有自己的锁,不同段的操作可以并行执行。

    • HashMap 不是线程安全的,如果多个线程同时访问和修改一个 HashMap 实例,可能会导致数据不一致和线程安全问题。在多线程环境下,必须使用额外的同步机制(例如,Collections.synchronizedMap() 或显式的锁)来保证线程安全性。

  2. 性能

    • 在低并发情况下,HashMap 的性能通常比 ConcurrentHashMap 略好,因为不需要额外的并发控制开销。但在高并发环境中,ConcurrentHashMap 的性能通常更好,因为它可以允许多个线程同时读取和写入数据。
  3. 迭代器的弱一致性

    • ConcurrentHashMap 的迭代器提供了弱一致性的保证,即迭代器可以同时遍历正在修改的和未修改的元素,但不保证迭代器在某个特定时间点的视图是一致的。这对于并发迭代非常有用。

    • HashMap 的迭代器在并发环境中可能会导致 ConcurrentModificationException 异常,因为它不提供并发迭代的支持,必须在多线程环境中进行额外的同步。

  4. 初始容量和负载因子

    • ConcurrentHashMap 在初始化时可以指定多个段(Segment),每个段内有自己的初始容量和负载因子。这允许更好地控制并发度和内存使用。

    • HashMap 则只有一个全局的初始容量和负载因子,不能针对不同需求进行调优。

  5. null 值和键

    • ConcurrentHashMap 不允许存储 null 值和 null 键。如果尝试存储 null 值或键,将会抛出 NullPointerException

    • HashMap 允许存储 null 值和键。

综上所述,ConcurrentHashMap 适用于高并发环境,提供了线程安全性和性能的平衡。而 HashMap 适用于单线程或低并发环境,需要额外的同步来确保线程安全。选择合适的集合类取决于应用程序的并发需求和性能要求。

HashSet

HashSet 是 Java 中的一个集合类,它是基于哈希表的数据结构实现的,用于存储一组唯一的对象。以下是关于 HashSet 的介绍:

  1. 唯一性HashSet 中的元素是唯一的,不允许重复。如果尝试将重复元素添加到 HashSet 中,它将被忽略。

  2. 无序性HashSet 中的元素没有特定的顺序,不保证元素的存储顺序和插入顺序相同。如果需要有序性,可以考虑使用 LinkedHashSet

  3. 基于哈希表HashSet 的内部实现是基于哈希表,这使得查找元素的性能非常高效。平均情况下,插入、删除和查找操作的时间复杂度都是 O(1)。

  4. 线程不安全HashSet 不是线程安全的集合,如果多个线程同时访问和修改一个 HashSet 实例,可能会导致并发问题。在多线程环境中,可以考虑使用 Collections.synchronizedSet()ConcurrentHashSet 来确保线程安全性。

  5. 允许存储 null 元素HashSet 允许存储一个 null 元素。

  6. 用途HashSet 常用于需要存储一组元素并且需要快速查找元素的场景。它适用于大多数集合需求,例如去重、成员检查等。

需要注意的是,HashSet 不保证元素的顺序,因此遍历时元素的顺序可能不同于插入的顺序。如果需要有序性,可以考虑使用 LinkedHashSet,它保留了元素的插入顺序。

示例代码 - 使用 HashSet

以下是一个示例代码,演示如何使用 HashSet 存储一组唯一的整数元素,并执行一些基本操作:

import java.util.HashSet;

public class HashSetExample {
    public static void main(String[] args) {
        // 创建一个 HashSet 实例,用于存储整数
        HashSet<Integer> set = new HashSet<>();

        // 添加元素
        set.add(5);
        set.add(10);
        set.add(15);
        set.add(20);
        set.add(25);

        // 尝试添加重复元素(不会成功,HashSet 不允许重复)
        set.add(10);

        // 查找元素
        boolean contains15 = set.contains(15);
        boolean contains30 = set.contains(30);

        // 删除元素
        set.remove(20);

        // 遍历元素并打印
        System.out.println("HashSet 中的元素:");
        for (Integer num : set) {
            System.out.println(num);
        }

        // 打印查找结果
        System.out.println("HashSet 中是否包含 15:" + contains15); // true
        System.out.println("HashSet 中是否包含 30:" + contains30); // false
    }
}

在这个示例中,我们创建了一个 HashSet 实例 set,并向其添加了一些整数元素。我们尝试添加了重复的元素 10,但由于 HashSet 不允许重复,它只会保留一个 10。我们还查找了元素 1530 的存在,并删除了元素 20。最后,我们遍历 HashSet 并打印其中的元素,以及查找结果。

HashSet底层代码解析

HashSet 的核心原理是基于哈希表实现的,它的内部使用哈希表来存储元素,并保持元素的唯一性。以下是 HashSet 的核心原理讲解和代码分析:

  1. 哈希表原理

    • 哈希表是一种高效的数据结构,它通过将元素的键(key)映射到哈希桶(buckets)上来存储元素。每个桶可以包含多个元素,当多个元素映射到同一个桶时,它们会形成一个链表或树的结构,用于处理哈希冲突。
  2. HashSet 的内部结构

    • HashSet 内部使用了一个 HashMap 实例来存储元素。元素值作为键,而一个固定的占位对象作为值。这意味着 HashSet 实际上是一个键集合,值是固定的,不重要,只有键才具有意义。
  3. 添加元素的过程

    • 当向 HashSet 添加元素时,实际上是将元素值作为键,占位对象 PRESENT 作为值添加到内部的 HashMap 中。PRESENT 对象在 HashSet 内部是共享的,只有一个实例,用于节省内存。
  4. 保持唯一性

    • HashSet 通过哈希表来快速查找元素,确保元素的唯一性。在添加元素时,如果元素值已经存在于 HashSet 中,新的元素将不会被添加,从而保持了唯一性。

以下是 HashSet 的关键代码示例:

public class HashSet<E>  
    extends AbstractSet<E>  
    implements Set<E>, Cloneable, java.io.Serializable  
{  
    static final long serialVersionUID = -5024744406713321676L;  
  
    // 底层使用HashMap来保存HashSet中所有元素。  
    private transient HashMap<E,Object> map;  
      
    // 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。  
    private static final Object PRESENT = new Object();  
  
    /** 
     * 默认的无参构造器,构造一个空的HashSet。 
     *  
     * 实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。 
     */  
    public HashSet() {  
    map = new HashMap<E,Object>();  
    }  
  
    /** 
     * 构造一个包含指定collection中的元素的新set。 
     * 
     * 实际底层使用默认的加载因子0.75和足以包含指定 
     * collection中所有元素的初始容量来创建一个HashMap。 
     * @param c 其中的元素将存放在此set中的collection。 
     */  
    public HashSet(Collection<? extends E> c) {  
    map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));  
    addAll(c);  
    }  
  
    /** 
     * 以指定的initialCapacity和loadFactor构造一个空的HashSet。 
     * 
     * 实际底层以相应的参数构造一个空的HashMap。 
     * @param initialCapacity 初始容量。 
     * @param loadFactor 加载因子。 
     */  
    public HashSet(int initialCapacity, float loadFactor) {  
    map = new HashMap<E,Object>(initialCapacity, loadFactor);  
    }  
  
    /** 
     * 以指定的initialCapacity构造一个空的HashSet。 
     * 
     * 实际底层以相应的参数及加载因子loadFactor为0.75构造一个空的HashMap。 
     * @param initialCapacity 初始容量。 
     */  
    public HashSet(int initialCapacity) {  
    map = new HashMap<E,Object>(initialCapacity);  
    }  
  
    /** 
     * 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。 
     * 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。 
     * 
     * 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。 
     * @param initialCapacity 初始容量。 
     * @param loadFactor 加载因子。 
     * @param dummy 标记。 
     */  
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {  
    map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);  
    }  
  
    /** 
     * 返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。 
     *  
     * 底层实际调用底层HashMap的keySet来返回所有的key。 
     * 可见HashSet中的元素,只是存放在了底层HashMap的key上, 
     * value使用一个static final的Object对象标识。 
     * @return 对此set中元素进行迭代的Iterator。 
     */  
    public Iterator<E> iterator() {  
    return map.keySet().iterator();  
    }  
  
    /** 
     * 返回此set中的元素的数量(set的容量)。 
     * 
     * 底层实际调用HashMap的size()方法返回Entry的数量,就得到该Set中元素的个数。 
     * @return 此set中的元素的数量(set的容量)。 
     */  
    public int size() {  
    return map.size();  
    }  
  
    /** 
     * 如果此set不包含任何元素,则返回true。 
     * 
     * 底层实际调用HashMap的isEmpty()判断该HashSet是否为空。 
     * @return 如果此set不包含任何元素,则返回true。 
     */  
    public boolean isEmpty() {  
    return map.isEmpty();  
    }  
  
    /** 
     * 如果此set包含指定元素,则返回true。 
     * 更确切地讲,当且仅当此set包含一个满足(o==null ? e==null : o.equals(e)) 
     * 的e元素时,返回true。 
     * 
     * 底层实际调用HashMap的containsKey判断是否包含指定key。 
     * @param o 在此set中的存在已得到测试的元素。 
     * @return 如果此set包含指定元素,则返回true。 
     */  
    public boolean contains(Object o) {  
    return map.containsKey(o);  
    }  
  
    /** 
     * 如果此set中尚未包含指定元素,则添加指定元素。 
     * 更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : e.equals(e2)) 
     * 的元素e2,则向此set 添加指定的元素e。 
     * 如果此set已包含该元素,则该调用不更改set并返回false。 
     * 
     * 底层实际将将该元素作为key放入HashMap。 
     * 由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key 
     * 与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true), 
     * 新添加的Entry的value会将覆盖原来Entry的value,但key不会有任何改变, 
     * 因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中, 
     * 原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。 
     * @param e 将添加到此set中的元素。 
     * @return 如果此set尚未包含指定元素,则返回true。 
     */  
    public boolean add(E e) {  
    return map.put(e, PRESENT)==null;  
    }  
  
    /** 
     * 如果指定元素存在于此set中,则将其移除。 
     * 更确切地讲,如果此set包含一个满足(o==null ? e==null : o.equals(e))的元素e, 
     * 则将其移除。如果此set已包含该元素,则返回true 
     * (或者:如果此set因调用而发生更改,则返回true)。(一旦调用返回,则此set不再包含该元素)。 
     * 
     * 底层实际调用HashMap的remove方法删除指定Entry。 
     * @param o 如果存在于此set中则需要将其移除的对象。 
     * @return 如果set包含指定元素,则返回true。 
     */  
    public boolean remove(Object o) {  
    return map.remove(o)==PRESENT;  
    }  
  
    /** 
     * 从此set中移除所有元素。此调用返回后,该set将为空。 
     * 
     * 底层实际调用HashMap的clear方法清空Entry中所有元素。 
     */  
    public void clear() {  
    map.clear();  
    }  
  
    /** 
     * 返回此HashSet实例的浅表副本:并没有复制这些元素本身。 
     * 
     * 底层实际调用HashMap的clone()方法,获取HashMap的浅表副本,并设置到HashSet中。 
     */  
    public Object clone() {  
        try {  
            HashSet<E> newSet = (HashSet<E>) super.clone();  
            newSet.map = (HashMap<E, Object>) map.clone();  
            return newSet;  
        } catch (CloneNotSupportedException e) {  
            throw new InternalError();  
        }  
    }  
} 
  • mapHashSet 内部用于存储元素的 HashMap 实例。

  • PRESENT 是一个占位对象,用于作为 HashMap 的值,不重要,只有键(元素值)才有意义。

  • add(E e) 方法用于向 HashSet 中添加元素。它实际上是通过调用 HashMapput 方法来将元素值作为键,PRESENT 对象作为值存储到 map 中。如果添加成功,表示元素之前不在 HashSet 中,返回 true;如果添加失败,表示元素已经存在,返回 false

总的来说,HashSet 利用了 HashMap 的高效性能和哈希表的特性,实现了高效的元素存储和查找,同时保持了元素的唯一性。这使得 HashSet 成为了一个常用的集合类,适用于需要存储一组唯一元素的情况。

注意事项

在使用 HashSet 时,有一些注意事项需要考虑,以确保正确和高效地利用这个集合类:

  1. 唯一性HashSet 中的元素是唯一的,不允许重复。要确保元素的唯一性,需要正确实现元素的 hashCode()equals() 方法。如果不重写这两个方法,会使用默认的对象引用比较,导致无法识别相同内容的对象。

  2. 线程安全性HashSet 不是线程安全的集合。在多线程环境下,如果多个线程同时访问和修改同一个 HashSet 实例,可能会导致并发问题。如果需要在多线程环境中使用,可以考虑使用 Collections.synchronizedSet()ConcurrentHashSet

  3. null 元素HashSet 允许存储一个 null 元素。如果需要存储多个 null 元素,要格外小心,因为它们会被视为同一个元素。

  4. 元素顺序HashSet 中的元素没有特定的顺序,不保证元素的存储顺序和插入顺序相同。如果需要有序性,可以考虑使用 LinkedHashSet

  5. 性能和容量:在创建 HashSet 实例时,可以指定初始容量和负载因子。合理选择这些参数可以提高性能。如果容量不足,会导致扩容操作,影响性能。

  6. 迭代器HashSet 的迭代器提供了弱一致性的保证,允许在迭代过程中进行修改。但要注意,修改集合时需要小心,可能会导致 ConcurrentModificationException 异常。

  7. 哈希冲突:由于 HashSet 使用哈希表存储元素,可能会发生哈希冲突。良好的哈希函数和适当的容量选择可以减少冲突。

  8. 性能优化:在大规模数据集合中,为了提高性能,可以通过适当的容量、负载因子和哈希函数来优化 HashSet 的性能。

  9. 不可变对象:推荐使用不可变对象作为 HashSet 中的元素,以避免意外修改元素导致集合不稳定。

  10. 遍历元素:遍历 HashSet 中的元素时,可以使用增强型 for 循环或迭代器。注意,在迭代期间不要修改集合的内容,除非使用迭代器的 remove() 方法。

总之,了解这些注意事项有助于正确、高效地使用 HashSet,并避免潜在的问题。根据具体的应用场景和需求,选择合适的集合类是非常重要的。

LinkedHashMap

LinkedHashMap 是 Java 中的一个集合类,它是 HashMap 的子类,提供了有序性的特性。与 HashMap 不同,LinkedHashMap 保持了元素的插入顺序或访问顺序,这使得你可以以特定顺序遍历其中的元素。以下是关于 LinkedHashMap 的介绍:

  1. 有序性LinkedHashMap 保持了元素的有序性,这意味着你可以按照元素插入的顺序或访问的顺序(最近访问的元素排在前面)来遍历元素。有序性是通过一个双向链表来实现的。

  2. 基于哈希表:与 HashMap 相似,LinkedHashMap 也是基于哈希表来存储元素的。每个哈希桶(bucket)中存储的是一个链表,链表节点按照元素的插入或访问顺序连接起来。

  3. 线程不安全:与 HashMap 一样,LinkedHashMap 也不是线程安全的集合类。如果在多线程环境中访问和修改 LinkedHashMap,可能需要进行额外的同步措施来确保线程安全性。

  4. 允许存储 null 元素LinkedHashMap 允许存储一个 null 键和多个 null 值。

  5. 用途LinkedHashMap 常用于需要按照插入顺序或访问顺序遍历元素的场景。它适用于构建缓存、LRU(Least Recently Used)算法等。

需要注意的是,LinkedHashMap 通常比 HashMap 稍微消耗更多的内存,因为它需要维护一个额外的链表结构来保持有序性。

示例代码 - 使用 LinkedHashMap

以下是一个示例代码,演示如何使用 LinkedHashMap 存储一组键值对,并按照插入顺序或访问顺序来遍历元素:

import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapExample {
    public static void main(String[] args) {
        // 创建一个 LinkedHashSet 实例,按照插入顺序排序
        LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);

        // 添加元素
        linkedHashMap.put("one", 1);
        linkedHashMap.put("two", 2);
        linkedHashMap.put("three", 3);

        // 模拟访问元素
        linkedHashMap.get("two");

        // 遍历元素按照插入顺序或访问顺序
        System.out.println("按照插入顺序或访问顺序遍历元素:");
        for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

在这个示例中,我们创建了一个 LinkedHashMap 实例 linkedHashMap,并向其中添加了一些键值对。我们还模拟了访问元素 "two",这会改变元素的顺序。最后,我们遍历 linkedHashMap,观察到元素的顺序可以按照插入顺序或访问顺序来输出,具体取决于构造函数中的参数。

如果构造函数中传递了 true,则按照访问顺序排序,最近访问的元素排在前面。如果构造函数中传递了 false 或不传递参数,则按照插入顺序排序。在示例中,我们传递了 true,所以元素 "two" 被访问后排在第一位。

LinkedHashMap底层代码解析

LinkedHashMap 的核心代码分析和讲解涉及其内部数据结构和关键方法。以下是 LinkedHashMap 的核心代码分析和讲解:

  1. 数据结构LinkedHashMap 使用了两个数据结构来维护元素的有序性:

    • 哈希表(table):与 HashMap 相似,用于存储键值对,并用于快速查找元素。
    • 双向链表(entrySet):用于维护元素的顺序,链表中的每个节点包含一个键值对。
  2. 构造函数LinkedHashMap 有多个构造函数,但最常用的构造函数接受三个参数:初始容量(initialCapacity)、负载因子(loadFactor)和是否按照访问顺序排序(accessOrder)。构造函数会根据传递的参数来初始化内部数据结构。

  3. accessOrder 属性LinkedHashMap 中有一个布尔属性 accessOrder,用于表示是否按照访问顺序排序。如果 accessOrdertrue,则按照访问顺序排序;如果为 false,则按照插入顺序排序。

  4. get 方法:当调用 get 方法获取元素时,如果 accessOrdertrue,则该元素会被移到链表的尾部,表示最近访问的元素。

  5. put 方法:当调用 put 方法添加元素时,元素会被添加到哈希表中,并根据 accessOrder 的值来决定是否将元素添加到链表的尾部。

  6. remove 方法:当调用 remove 方法删除元素时,会同时从哈希表和链表中删除该元素。

  7. afterNodeAccess 方法:该方法用于在访问元素后调整链表中的元素顺序,确保按照访问顺序排序。

  8. afterNodeInsertion 方法:该方法用于在插入元素后调整链表中的元素顺序,确保按照插入顺序排序。

  9. 扩容机制:与 HashMap 类似,LinkedHashMap 也具有扩容机制,当元素数量达到容量乘以负载因子时,会触发扩容操作。

  10. 遍历元素:通过迭代器遍历 LinkedHashMap 时,可以按照插入顺序或访问顺序遍历元素,具体取决于 accessOrder 的值。

以下是一个简化的 LinkedHashMap 的源码示例,包含了关键逻辑的代码和注释:

import java.util.HashMap;
import java.util.Map;

public class MyLinkedHashMap<K, V> {
    private static final int INITIAL_CAPACITY = 16;
    private static final float LOAD_FACTOR = 0.75f;
    
    private int capacity; // 哈希表容量
    private Map.Entry<K, V>[] table; // 哈希表数组
    private int size; // 元素数量
    private final boolean accessOrder; // 是否按照访问顺序排序
    private Map.Entry<K, V> head; // 链表头部
    private Map.Entry<K, V> tail; // 链表尾部

    public MyLinkedHashMap(boolean accessOrder) {
        this.capacity = INITIAL_CAPACITY;
        this.table = new Map.Entry[capacity];
        this.accessOrder = accessOrder;
    }

    // 哈希函数,根据键计算哈希值
    private int hash(Object key) {
        return key.hashCode() % capacity;
    }

    // 获取键对应的哈希表索引
    private int indexFor(int hash, int length) {
        return hash & (length - 1);
    }

    // 获取键对应的节点
    private Map.Entry<K, V> getNode(int hash, Object key) {
        int index = indexFor(hash, table.length);
        Map.Entry<K, V> e = table[index];
        while (e != null && !(e.getKey() == key || e.getKey().equals(key))) {
            e = e.getNext();
        }
        return e;
    }

    // 在链表头部添加节点
    private void addFirst(Map.Entry<K, V> node) {
        if (accessOrder) {
            node.setNext(head);
            head = node;
        }
    }

    // 删除节点
    private void removeNode(Map.Entry<K, V> node) {
        if (accessOrder) {
            Map.Entry<K, V> prev = head;
            Map.Entry<K, V> current = head;
            while (current != null) {
                if (current == node) {
                    if (prev == current) {
                        head = current.getNext();
                    } else {
                        prev.setNext(current.getNext());
                    }
                    if (tail == current) {
                        tail = prev;
                    }
                    return;
                }
                prev = current;
                current = current.getNext();
            }
        }
    }

    // 获取元素值
    public V get(Object key) {
        int hash = hash(key);
        Map.Entry<K, V> node = getNode(hash, key);
        if (node != null) {
            if (accessOrder) {
                removeNode(node); // 先移除再添加到头部,表示最近访问
                addFirst(node);
            }
            return node.getValue();
        }
        return null;
    }

    // 添加元素
    public void put(K key, V value) {
        int hash = hash(key);
        int index = indexFor(hash, table.length);
        Map.Entry<K, V> node = table[index];
        while (node != null) {
            if (node.getKey() == key || node.getKey().equals(key)) {
                node.setValue(value);
                if (accessOrder) {
                    removeNode(node); // 先移除再添加到头部,表示最近访问
                    addFirst(node);
                }
                return;
            }
            node = node.getNext();
        }
        // 创建新节点并添加到哈希表
        Map.Entry<K, V> newNode = new Node<>(hash, key, value, null);
        newNode.setNext(table[index]);
        table[index] = newNode;
        size++;
        if (accessOrder) {
            addFirst(newNode);
        }
        // 判断是否需要进行扩容操作
        if (size > capacity * LOAD_FACTOR) {
            resize();
        }
    }

    // 扩容操作
    private void resize() {
        // 省略扩容逻辑
    }

    // 其他方法和逻辑

    private static class Node<K, V> implements Map.Entry<K, V> {
        private final int hash;
        private final K key;
        private V value;
        private Map.Entry<K, V> next;

        public Node(int hash, K key, V value, Map.Entry<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public Map.Entry<K, V> getNext() {
            return next;
        }

        public void setNext(Map.Entry<K, V> next) {
            this.next = next;
        }
    }

    // 其他内部类和方法
}
LinkedHashMap 内部实现分析
    1. 初始化

在 LinkedHashMap 的构造方法中,实际上调用了父类 HashMap 的相关构造方法来创建一个底层存储的 table 数组。

public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

LinkedHashMap 的 Entry 元素继承自 HashMap 的 Entry,并提供了双向链表的功能。在上述 HashMap 的构造器中,最后会调用 init() 方法,进行相关的初始化调用。尽管 init() 方法在 HashMap 的实现中并无实际意义,它仅提供给子类来实现相关的初始化调用。

void init() {  
    header = new Entry<K,V>(-1, null, null, null);  
    header.before = header.after = header;  
}
    1. 存储

LinkedHashMap 并未重写父类 HashMap 的 put 方法,而是重写了父类 HashMap 的 put 方法调用的子方法 void addEntry(int hash, K key, V value, int bucketIndex)void createEntry(int hash, K key, V value, int bucketIndex),以提供自己特有的双向链接列表实现。

void addEntry(int hash, K key, V value, int bucketIndex) {  
    // 调用 create 方法,将新元素以双向链表的形式加入到映射中。  
    createEntry(hash, key, value, bucketIndex);  
  
    // 删除最近最少使用元素的策略定义  
    Entry<K,V> eldest = header.after;  
    if (removeEldestEntry(eldest)) {  
        removeEntryForKey(eldest.key);  
    } else {  
        if (size >= threshold)  
            resize(2 * table.length);  
    }  
}  

void createEntry(int hash, K key, V value, int bucketIndex) {  
    HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
    table[bucketIndex] = e;  
    // 调用元素的 addBefore 方法,将元素加入到哈希、双向链接列表。  
    e.addBefore(header);  
    size++;  
} 

private void addBefore(Entry<K,V> existingEntry) {  
    after  = existingEntry;  
    before = existingEntry.before;  
    before.after = this;  
    after.before = this;  
}
    1. 读取

LinkedHashMap 重写了父类 HashMap 的 get 方法。在调用父类的 getEntry() 方法取得查找的元素后,它再判断排序模式 accessOrder 是否为 true,如果是,则记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于链表的增加、删除操作是常量级的,因此不会带来性能损失。

public V get(Object key) {  
    // 调用父类 HashMap 的 getEntry() 方法,获取要查找的元素。  
    Entry<K,V> e = (Entry<K,V>)getEntry(key);  
    if (e == null)  
        return null;  
    // 记录访问顺序。  
    e.recordAccess(this);  
    return e.value;  
}  

void recordAccess(HashMap<K,V> m) {  
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  
    // 如果定义了 LinkedHashMap 的迭代顺序为访问顺序,  
    // 则删除以前位置上的元素,并将最新访问的元素添加到链表表头。  
    if (lm.accessOrder) {  
        lm.modCount++;  
        remove();  
        addBefore(lm.header);  
    }  
}

总之,LinkedHashMap 利用哈希表和双向链表的结合实现了有序性,可以根据需要选择按照插入顺序或访问顺序排序元素。这使得 LinkedHashMap 成为了一种常用的集合类,适用于需要保持元素顺序的场景,例如实现 LRU 缓存算法。核心代码分析有助于理解其工作原理和用法。

注意事项

在使用 LinkedHashMap 时,有一些注意事项需要考虑:

  1. 有序性LinkedHashMap 可以按照插入顺序或访问顺序排序元素。在创建 LinkedHashMap 实例时,根据需要选择合适的排序方式,使用构造函数中的 accessOrder 参数来指定。

  2. 性能LinkedHashMap 的性能通常比 HashMap 稍差,因为它需要维护额外的链表结构。在大规模数据集合中,性能可能会受到影响,需要根据具体需求进行评估。

  3. 迭代顺序:如果使用迭代器遍历 LinkedHashMap,注意遍历的顺序可能会影响性能。按照插入顺序遍历性能较好,按照访问顺序遍历会涉及链表操作。

  4. 线程安全性LinkedHashMap 不是线程安全的,如果在多线程环境中使用,需要采取适当的同步措施,或者考虑使用 ConcurrentHashMap

  5. 内存消耗:由于 LinkedHashMap 需要维护额外的链表结构,可能占用更多的内存空间。在内存有限的情况下,要谨慎使用。

  6. LRU 缓存LinkedHashMap 的有序性使其非常适合实现LRU(最近最少使用)缓存策略。通过设置 accessOrdertrue,可以实现按照访问顺序排序的缓存。

  7. 元素的 equalshashCode 方法:为了正确存储和检索元素,确保元素的 equalshashCode 方法正确实现。否则,可能导致元素无法正确匹配。

  8. 键和值的类型LinkedHashMap 可以存储任意类型的键和值,但要确保键的类型具有合适的 hashCodeequals 实现,以避免哈希冲突和不一致的行为。

  9. 容量和负载因子:可以根据数据集的大小和访问模式来调整初始容量和负载因子,以优化性能。默认值通常适用于多数情况。

总之,了解 LinkedHashMap 的特性和注意事项,根据具体需求和场景来选择合适的数据结构和配置参数,可以更好地利用它的功能。

应用场景

当涉及到具体的应用场景时,这些集合类的应用会更加清晰。以下是一些示例,展示了这些集合类在不同应用场景中的用法:

  1. 秒杀活动(秒杀系统)

    • 使用 ConcurrentHashMap 存储商品库存信息,以便多个用户同时查询库存和进行秒杀。
    • 使用 HashMap 存储用户订单信息,以便记录和查询用户的秒杀订单。
    • 使用 HashSet 存储用户已秒杀商品的记录,用于去重,以确保用户不重复秒杀。
    • 使用 LinkedHashMap 存储用户请求记录,以便按照请求顺序记录用户的请求,便于监控和分析。
  2. 在线聊天应用

    • 使用 HashMap 存储在线用户的信息,以便快速查找和管理在线用户。
    • 使用 ConcurrentHashMap 存储聊天室的消息记录,支持多用户同时发送和接收消息。
    • 使用 HashSet 存储用户的好友列表,用于快速查找和管理好友关系。
    • 使用 LinkedHashMap 存储聊天消息的发送顺序,以便按照发送顺序显示消息。
  3. 电子商务网站的购物车

    • 使用 HashMap 存储用户购物车中的商品和数量。
    • 使用 HashSet 存储用户喜欢的商品,以便用户添加到购物车或收藏夹。
    • 使用 LinkedHashMap 存储用户的浏览记录,以便按照浏览顺序展示商品。
  4. 数据统计应用

    • 使用 HashMap 存储数据统计的结果,以便快速查找和查询统计数据。
    • 使用 ConcurrentHashMap 存储多个线程并发更新的数据统计信息。
    • 使用 LinkedHashMap 存储按照时间顺序记录的数据,以便生成时间序列图表。

这些示例只是一些应用场景的代表性示例,实际应用中的用法会根据具体需求和业务逻辑的复杂性而变化。根据你的应用需求,可以选择合适的集合类来实现所需功能。

结语

好的,本期我们的数组数据类型的讲解就到此结束。下期,我们将继续深入探讨链表数据结构,它将带领我们进入更加复杂和灵活的数据结构世界。如果您在学习过程中有任何问题或疑问,都欢迎在评论中提出或者私信联系我。我将竭尽全力为您提供更全面和详细的讲解,以确保您的学习得以顺利进行。期待下期的精彩内容!

你可能感兴趣的:(技能晋级,java,开发语言)