五、Java集合

71 阅读20分钟

1、 文章背景

工作已有五年之久,回望过去,没有在一线城市快节奏下学习成长,只能自己不断在工作中学习进步,最近一直想写写属于自己的文章,记录学习的内容和知识点,当做一次成长。

2、 集合的概述

摘要:Java 集合(Java Collections)是一组用于存储和操作对象的类库。

Java 的集合框架主要在 java.util 包中,它提供了一套设计良好的接口和实现类,这些接口和实现类用于存储和处理对象组。集合框架的设计提高了操作集合的性能,增加了操作的灵活性,并简化了编程工作。

7F44F9B3-DDB4-4CED-ADBB-77382C40CAE1.png

根据上面框架图,抓住主干,即CollectionMap

  • Collection是一个接口,是高度抽象出来的集合,它包含了集合的基本操作和属性。Collection分为两大分支ListSet

    • List是一个有序的队列,每一个元素都有它的索引。第一个元素的索引值是0。List的实现类有LinkedListArrayListVectorStack
    • Set是一个不允许有重复元素的集合。Set的实现类有HastSetTreeSetHashSet依赖于HashMap,它实际上是通过HashMap实现的;TreeSet依赖于TreeMap,它实际上是通过TreeMap实现的。
  • Map是一个映射接口,即key-value键值对。Map中的每一个元素包含一个keykey对应的valueAbstractMap是个抽象类,它实现了Map接口中的大部分API。而HashMaptreeMapWeakHashMap都是继承于AbstractMapHashtable虽然继承于Dictionary,但它实现了Map接口。

接下来,再看Iterator。它是遍历集合的工具,即我们通常通过Iterator迭代器来遍历集合。我们说Collection依赖于Iterator,是因为Collection的实现类都要实现iterator()函数,返回一个Iterator对象。ListIterator是专门为遍历List而存在的。

Java 集合框架主要分为几个核心部分:

  1. 接口(Interfaces)

    • Collection:是最基础的集合表示方式,包含基本的添加、删除、遍历等操作。SetList 和 Queue 等接口都继承自 Collection 接口。

      • Set:不允许有重复元素的集合。
      • List:有序集合,允许有重复的元素。
      • Queue:一种用于在处理之前保持元素的集合,提供了额外的插入、提取和检查操作。
    • Map:不属于 Collection 接口的一部分,它表示键值对的集合。每个键最多只能映射到一个值。

  2. 实现类(Implementations)

    • ArrayListLinkedList:List 接口的实现。
    • HashSetLinkedHashSetTreeSet:Set 接口的实现。
    • PriorityQueue:Queue 接口的实现。
    • HashMapLinkedHashMapTreeMap:Map 接口的实现。
  3. 算法(Algorithms)

    • 集合框架提供了一些实现算法的静态方法,如排序、搜索、乱序、比较等,这些方法主要在 Collections 类中。
  4. 辅助类(Utilities)

    • 如 Collections 和 Arrays,提供了一系列的静态方法用于操作集合或数组。

3、 集合和数组的区别

056E0B36-F8C2-468C-A9B8-186484887866.png

Java集合和数组的区别主要体现在以下几个方面:

  • 长度与可变性‌:

    • 数组‌:长度固定,一旦创建无法改变。
    • 集合‌:长度可变,可根据需要动态添加或删除元素。
  • 类型限制‌:

    • 数组‌:必须声明元素类型,且只能存储一种类型的数据。
    • 集合‌:可存储任意类型的对象,增加灵活性。
  • 访问方式‌:

    • 数组‌:通过索引访问元素。
    • 集合‌:提供多种访问方式,如迭代器、foreach循环等。
  • 效率与内存‌:

    • 数组‌:在连续内存块中存储,访问速度快。
    • 集合‌:基于链表或树等非连续结构,存储方式灵活但访问速度相对较慢。
  • 初始化‌:

    • 数组‌:创建后需手动初始化。
    • 集合‌:创建时即可添加元素,无需单独初始化。

综上所述,Java集合和数组各有优势,选择哪种取决于具体需求和设计目标‌

4、 Java集合框架的主要特点

Java集合框架的主要特点可以概括为以下几点:

  • 动态大小‌:集合可以根据需要动态调整大小,无需预先指定容量,使用更加灵活。

  • 泛型支持‌:集合框架利用泛型机制,提供类型安全的操作,避免类型转换错误和运行时错误。

  • 高性能‌:提供高效的数据结构和算法实现,满足大部分场景需求,如ArrayList实现快速随机访问,LinkedList实现快速插入和删除。

  • 统一接口‌:集合框架中的类和接口遵循统一的接口规范,方便切换不同的集合实现,便于代码编写和理解。 ‌接口与实现分离‌:设计模式使得不同数据结构的实现可以相互替换,增加了框架的灵活性和可扩展性。

  • 迭代支持‌:集合提供了迭代器来遍历元素,方便进行操作和遍历‌。

这些特点使得Java集合框架成为Java开发中处理数据集合的强大工具。

5、 常用集合的分类

  1. Collection接口的对象的集合(单列集合)

    • List 接口:元素按进入先后有序保存,可重复

      • LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全。
      • ArrayList 接口实现类, 数组(动态数组), 随机访问, 没有同步, 线程不安全。
      • Vector 接口实现类, 数组, 同步, 线程安全。Stack 是Vector类的实现类。
    • Set 接口: 仅接收一次,不可重复,并做内部排序

      • HashSet 使用hash表(数组)存储元素
        • LinkedHashSet 链表维护元素的插入次序
      • TreeSet 底层实现为二叉树,元素排好序
  2. Map 接口 键值对的集合 (双列集合):

    • Hashtable 接口实现类, 同步, 线程安全
    • HashMap 接口实现类 ,没有同步, 线程不安全
      • LinkedHashMap 双向链表和哈希表实现
      • WeakHashMap 集合框架中的一个特殊成员,其键为弱引用,可能会在垃圾回收(GC)时被自动删除
    • TreeMap 红黑树对所有的key进行排序
    • ConcurrentHashMap 接口实现类,同步,线程安全

6、 List和Set的区别

ListSet集合的区别主要体现在以下几个方面:

  • 元素重复性‌List允许存储重复的元素,而Set不允许存储重复元素。‌
  • 元素顺序‌List是有序的集合,维护了元素的插入顺序,可以通过索引访问元素;Set是无序的集合(除了特定实现类),元素没有特定的索引,不能通过索引访问。‌
  • 实现类‌List接口的主要实现类有ArrayListLinkedListVectorSet接口的主要实现类有HashSetLinkedHashSetTreeSet。‌
  • 元素唯一性判断‌:Set集合通过元素的哈希码(hashCode())和equals()方法来保证元素的唯一性;而List集合则完全依赖于元素的顺序,不涉及唯一性判断。‌

综上所述,List和Set集合在元素重复性、元素顺序、实现类以及元素唯一性判断等方面存在明显的区别,适用于不同的使用场景。‌

7、 常用集合的底层实现

  1. ArrayList实现

    • 定义与位置‌:ArrayList是Java集合框架中的一部分,位于java.util包下,是一个动态数组。

    • 底层结构‌:ArrayList底层采用数组数据结构存储元素,默认初始容量为10,扩容因子为1.5。扩容公式:原始长度*0.5+1。

    • 特性‌:

      • ‌动态性:数组长度可以随着容量的增长而增长,但不会自动缩小,除非调用trimToSize方法。
      • ‌非线程安全:ArrayList不是线程安全的,多线程环境下需要手动同步。
      • ‌泛型支持:ArrayList支持泛型,可以在编译阶段约束集合对象只能操作某种数据类型。
    • 使用场景‌:适用于元素个数不确定且需要进行增删操作的场景,以及需要动态扩容的情况。

  2. LinkList实现

    1.webp

    • 定义与位置‌:LinkList是Java集合框架中的一部分,位于java.util包下,是一个双向链表。这意味着每个节点(Node)都包含两部分:一部分是存储的实际数据(称为item或element),另一部分是两个指针(称为next和prev),分别指向列表中的下一个节点和上一个节点。

    • 底层结构‌:LinkedList没有初始默认长度。它是一个基于双向链表的数据结构,可以动态地增删节点,不需要预先定义大小‌。LinkedList不需要扩容。由于其底层是链表结构,每次添加或删除元素时,只需调整链表中的节点指针即可,无需像数组那样重新分配内存或复制数据‌。

    • 特性‌:

      • ‌动态性:LinkedList是动态分配内存的,它没有固定的容量限制。
      • ‌非线程安全:LinkedList不是线程安全的,如果需要在多线程环境中使用,必须采用适当的同步机制,或者使用Collections.synchronizedList方法来包装LinkedList对象。
      • ‌泛型支持:LinkedList支持泛型,可以在编译阶段约束集合对象只能操作某种数据类型。
    • 使用场景‌:

      • 在需要频繁在列表中间插入或删除元素时,LinkedList性能优于ArrayList,如任务调度系统。
      • LinkedList可用作队列(FIFO)实现,支持高效的入队和出队操作,如打印任务队列系统。‌
      • LinkedList实现了Deque接口,可以在两端进行插入和删除操作,如浏览器历史记录。‌
  3. Vector实现

    • 定义与位置‌:Vector在Java中是一个类,它实现了List接口。与ArrayList类似,Vector也是一个动态数组,但它与ArrayList有几个关键的区别。

    • 底层结构‌:Vector的默认容量为10。这意味着在没有指定初始容量的情况下,创建一个新的Vector对象时,它将能够容纳10个元素‌。扩容公式:原始长度*2

    • 特性‌:

      • ‌动态性:当Vector达到其当前容量时,它会自动扩容。Vector的扩容策略是将当前容量增加一倍。例如,如果Vector的容量为10,一次扩容后容量将变为20‌。
      • ‌线程安全:Vector是线程安全的,因为它的方法被同步,以防止并发访问导致的数据不一致。然而,这种同步机制会导致性能上的开销,特别是在高并发环境中‌。
      • ‌泛型支持:Vector支持泛型,可以在编译阶段约束集合对象只能操作某种数据类型。
    • 使用场景‌:

      • Vector的所有方法都是同步的,因此它适用于多线程环境下需要安全访问和修改集合的场景。
      • 当需要一个能够自动调整大小的数组时,可以选择VectorVector会根据元素的增加自动扩容,无需手动管理数组大小。‌
      • Vector支持通过索引顺序存储和访问元素,适用于需要频繁按索引访问元素的场景。
      • Vector提供了在末尾添加和删除元素的方法,可以方便地实现栈(后进先出)或队列(先进先出)的数据结构
  4. HashSet实现

    • 定义与位置‌:HashSet(哈希集合)是Java编程语言中的一个集合类,它实现了Set接口并继承自AbstractSet类。HashSet通过使用哈希表来存储元素,提供了快速的插入、删除和查询操作。

    • 底层结构‌:

      • HashSet底层采用哈希表实现,具体是基于HashMap存储元素。在HashMap中,元素存储在key中,而value则使用统一的PRESENT对象。默认初始容量为16。加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容。扩容增量:原容量的 1 倍。
      • 在JDK 7中,哈希表采用数组+链表实现;JDK 8则引入红黑树,当链表长度超过8且数组长度大于64时,链表转化为红黑树,以提高性能。
    • 特性‌:

      • ‌动态性:默认初始容量为16。加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容。扩容增量:原容量的 1 倍。如 HashSet的容量为16,一次扩容后是容量为32
      • ‌非线程安全:线程不安全,存取速度快。
      • ‌泛型支持:HashSet支持泛型,可以在编译阶段约束集合对象只能操作某种数据类型。
      • 无序性:HashSet中的元素没有特定的顺序,存储和检索顺序是不确定的,与插入顺序无关‌。
      • 不允许重复元素:HashSet不允许存储相同的元素,如果尝试向HashSet中添加相同的元素,它将忽略该操作。
    • 使用场景‌:

      • 去重操作:由于HashSet不允许存储重复元素,因此常用于去除集合中的重复项。
      • 快速查找:HashSet基于哈希表实现,提供了快速的查找性能,适用于需要频繁判断元素是否存在的场景。
      • 集合运算:HashSet支持集合的并集、交集、差集等运算,方便进行集合间的操作。
      • 缓存:HashSet可以作为缓存的一种数据结构,存储需要快速访问的数据。
      • 词频统计:在处理文本数据时,HashSet可用于统计单词的出现次数,通过存储不重复的单词并遍历文本进行计数。
  5. LinkHashSet实现

    • 定义与位置‌:LinkedHashSet 是 Java 集合框架中的一个具体类,它继承自 HashSet 并且实现了 Set 接口。与 HashSet 不同的是,LinkedHashSet 维护了一个双向链表来记录元素的插入顺序,因此它能够保证迭代时按照元素插入的顺序进行。

    • 底层结构‌:

      • LinkedHashSet 底层基于 HashMap 实现,但它额外维护了一个双向链表(LinkedHashMap 中的 Entry 实际上是一个双向链表的节点)。
      • 这个双向链表记录了元素插入的顺序,使得 LinkedHashSet 能够按照插入顺序进行迭代。
    • 特性‌:

      • ‌动态性:默认初始容量为16。加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容。扩容增量:原容量的 1 倍。如 LinkedHashSet的容量为16,一次扩容后是容量为32
      • ‌非线程安全:线程不安全,存取速度快。
      • ‌泛型支持:LinkedHashSet支持泛型,可以在编译阶段约束集合对象只能操作某种数据类型。
      • 有序性:哈希表保证了元素的唯一性,而链表则维护了元素的插入顺序。
      • 不允许重复元素:LinkedHashSet不允许存储相同的元素,如果尝试向LinkedHashSet中添加相同的元素,它将忽略该操作。
    • 使用场景‌:

      • LinkedHashSet 适用于需要既保持集合特性(不重复元素)又需要按照插入顺序迭代元素的场景。
  6. TreeSet实现

TreeSet是Java集合框架中的一个有序且不可重复的集合类‌。以下是关于TreeSet的详细总结:

  • 底层实现‌TreeSet是基于红黑树(Red-Black Tree)的数据结构实现的,这使得TreeSet能够保持元素的排序顺序,并且具有较好的增删改查性能。
  • 排序方式‌TreeSet中的元素会按照自然顺序进行排序,或者根据创建TreeSet时提供的Comparator进行自定义排序。
  • 元素唯一性‌TreeSet存储的元素是唯一的,即不允许存储重复的元素。
  • 应用场景‌TreeSet适用于需要保持元素排序顺序且不允许重复元素的场景,如按照生日对用户信息进行排序展示等。

综上所述,TreeSet是一个功能强大的有序集合类,它结合了红黑树的优点,实现了元素的自然排序和快速查找,同时保证了元素的唯一性‌

  1. Hashtable实现
  • 线程安全性‌Hashtable是线程安全的,其方法通过synchronized关键字实现同步,适合在多线程环境中使用。但同步机制也带来了性能开销‌。 ‌
  • 空键值‌Hashtable不允许键和值为null。如果尝试插入空键或值,会抛出NullPointerException‌。
  • 继承关系‌Hashtable继承自Dictionary类,并实现了Map接口‌。
  • 初始容量与扩容‌Hashtable的初始容量为11,扩容时是容量翻倍+1,即capacity(2^n+1),且填充因子默认为0.75‌。
  • 底层实现‌Hashtable的底层实现是数组+链表结构,与HashMap类似。但在计算hash时,Hashtable直接使用key的hashcode对table数组的长度进行取模‌

综上所述,Hashtable是一个线程安全的、不允许空键值的集合类,适用于需要同步机制的多线程环境,但需注意其性能开销‌。

  1. HashMap

    HashMap 是一种快速的查找并且插入、删除性能都良好的一种 K/V键值对的数据结构,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。

    HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。

  • HashMap的底层数据结构

    • JDK1.8 之前,底层采用数组+链表,用(n - 1) & hash找到数组索引位置,若冲突则用拉链法解决冲突。

    • 拉链法 简单来说就是将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可 2.png

    • JDK1.8 之后 底层初始数据结构仍采用数组+链表,当某个桶链表的长度大于8时,会先调用treeifyBin()方法,这个方法会判断数组长度是否小于64,如果大于或等于则执行转换红黑树操作,以减少搜索时间;反之则调用resize()进行扩容。

    • JDK1.8的数据结构示意图如下:

3.png

其中,桶数组是用来存储数据元素,链表是用来解决冲突,红黑树是为了提高查询的效率。

- 数据元素通过映射关系,也就是散列函数,映射到桶数组对应索引的位置。
- 如果发生冲突,从冲突的位置拉一个链表,插入冲突的元素
- 如果链表长度>8&数组大小>=64,链表转为红黑树
- 如果红黑树节点个数<6 ,转为链表
  • 解决哈希冲突有哪些方法呢

    • 链式地址法:把存在Hash冲突的key,以单向链表来进行存储。
    • 开放定址法:开放定址法也称线性探测法,就是从冲突的位置再接着往下找,给冲突元素找个空位。
    • 再哈希法:换种哈希函数对key进行Hash,一直运算,直到不再产生冲突为止。
    • 建立公共溢出区:再建一个数组,把冲突的元素放进去。
  • HashMap是如何解决哈希冲突的

    • 在JDK8之 前HashMap采用的是链式寻址法解决哈希冲突的,而JDK8之后则是通过链式寻址法以及红黑树来解决Hash冲突。
  1. LinkedHashMap

4.png

5.png

  • 定义与继承‌:LinkedHashMap是HashMap的子类,结合了HashMap和双向链表的特点,实现了Map接口。
  • 特性‌
    • 有序性‌:通过维护一个额外的双向链表,保证了迭代顺序。可以是插入顺序,也可以是访问顺序(需设置accessOrder为true)。
    • 允许空值‌:与HashMap一样,LinkedHashMap允许键和值为null,但键最多只能有一个为null。
    • 非同步‌:LinkedHashMap是非线程安全的,适用于单线程环境。 ‌
  • 应用场景‌:由于LinkedHashMap支持按照访问顺序排序,因此常用于实现LRU(最近最少使用)缓存策略。
  • 内部实现‌:LinkedHashMap的Entry继承了HashMap的Node,并增加了before和after变量,用于维护双向链表的前后节点关系。在put和get操作时,会维护这个双向链表,从而确保遍历时的有序性‌ 键值对的能力。
  1. TreeMap

TreeMap的底层数据结构是红黑树‌。以下是关于TreeMap底层数据结构的详细解释:

  • 红黑树特性‌:红黑树是一种自平衡的二叉搜索树,具有严格的规则来保持树的平衡。每个节点都有颜色(红色或黑色),且遵循特定的排列规则,如根节点是黑色的,所有叶子节点都是黑色的,红色节点的两个子节点都是黑色的等。
  • 排序原理‌:TreeMap利用红黑树的性质,根据key进行排序。在TreeMap中,每个元素都能插入到红黑树的适当位置,从而维护了key的大小关系。这使得TreeMap特别适用于需要排序的场景。 ‌自定义排序‌:如果需要实现自定义的排序方式(如降序),可以通过实现Comparator接口来自定义比较器。

综上所述,TreeMap的底层数据结构是红黑树,这种结构使得TreeMap能够高效地维护有序的键值对集合。‌

  1. ConcurrentHashMap ConcurrentHashMap是Java集合框架中一个非常重要的线程安全哈希表实现,专为多线程环境设计。以下是对ConcurrentHashMap的详细总结:
  • 特性

    • 线程安全:ConcurrentHashMap通过一系列复杂的机制,如分段锁(在早期版本中)和CAS(Compare and Swap)操作,以及改进的锁策略(在Java 8及更高版本中),确保在多线程环境下进行安全的读写操作。
    • 高效并发:通过减小锁的粒度(例如,在Java 8中,锁只锁定哈希表的一个部分,而不是整个表),ConcurrentHashMap允许更高的并发度,从而提高了性能。
    • 存储结构:底层采用数组+链表+红黑树的组合。当链表长度超过一定阈值时,链表会转换为红黑树,以提高查询效率。这种结构使得ConcurrentHashMap在处理大量数据时仍然能够保持高效。
  • 原理

    • 分段锁(早期版本):在Java 7及之前的版本中,ConcurrentHashMap使用分段锁来减少锁的竞争。哈希表被分成多个段(Segment),每个段都是一个独立的哈希表,有自己的锁。这样,当一个线程在操作某个段时,不会影响其他段的操作。
    • CAS操作与锁策略(Java 8及更高版本):在Java 8中,ConcurrentHashMap的锁策略进行了改进,不再使用分段锁,而是采用了更细粒度的锁和CAS操作。这进一步减少了锁的竞争,提高了并发性能。
    • 节点类型:在Java 8中,引入了节点(Node)的概念,用于存储键值对。节点可以是普通的链表节点,也可以是树节点(当链表长度超过阈值时转换为红黑树)。
  • 注意事项

    • 复合操作非原子性:尽管ConcurrentHashMap提供了线程安全的读写操作,但它不保证复合操作(如先检查键是否存在然后再插入)的原子性。对于此类操作,应使用ConcurrentHashMap提供的原子性复合操作方法,如putIfAbsent
    • 迭代弱一致性:ConcurrentHashMap的迭代器是弱一致性的,即在迭代过程中,允许其他线程对哈希表进行并发修改。这可能导致迭代器在遍历过程中遇到并发修改的数据。
  • 使用场景

    • ConcurrentHashMap适用于需要高效并发访问的场景,如缓存、计数器、多线程环境下的数据共享等。它提供了比传统哈希表更高的并发性能,同时保证了线程安全。

综上所述,ConcurrentHashMap是Java中一个非常强大且高效的线程安全哈希表实现,适用于多线程环境下的数据存取操作。通过不断改进的锁策略和存储结构,它能够在高并发环境下提供出色的性能。