Java集合框架(知识整理)

集合框架

Java 集合框架可以分为两条大的支线:

1、Collection,主要由 List、Set、Queue 组成:

  • List 代表有序、可重复的集合,典型代表就是封装了动态数组的 ArrayList 和封装了链表的 LinkedList;
  • Set 代表无序、不可重复的集合,典型代表就是 HashSet 和 TreeSet;
  • Queue 代表队列,典型代表就是双端队列 ArrayDeque,以及优先级队列 PriorityQueue。

2、Map,代表键值对的集合,典型代表就是 HashMap。

一、List

  • 特点:存取有序,可以存放重复的元素,可以用下标对元素进行操作。

1)ArrayList

  • 特点:

    • ArrayList 是由动态数组实现的,支持随机存取,也就是可以通过下标直接存取元素;
    • ArrayList 在数组的基础上实现了自动扩容,并且提供了比数组更丰富的预定义方法(各种增删改查),非常灵活;
    • 从尾部插入和删除元素会比较快捷,从中间插入和删除元素会比较低效,因为涉及到数组元素的复制和移动;
    • 如果内部数组的容量不足时会自动扩容,因此当元素非常庞大的时候,效率会比较低
  • 创建:

    //下面两个创建的效果是差不多的
    ArrayList<String> alist = new ArrayList<String>();
    List<String> alist = new ArrayList<>();
    
    //非常确定 ArrayList 中元素的个数,在创建的时候还可以指定初始大小
    List<String> alist = new ArrayList<>(20);            
    
  • 功能:

    • 基本数组的增删改查,不过当添加删除中间的元素时,对应后续的元素位置得跟着向后向前移,考虑时间复杂度等性能
    • 扩容需要重新复制数组的

2)LinkedList

  • 特点:

    • LinkedList 是由双向链表实现的,不支持随机存取,只能从一端开始遍历,直到找到需要的元素后返回;
    • 任意位置插入和删除元素都很方便,因为只需要改变前一个节点和后一个节点的引用即可,不像 ArrayList 那样需要复制和移动数组元素;
    • 因为每个元素都存储了前一个和后一个节点的引用,所以相对来说,占用的内存空间会比 ArrayList 多一些。
    • 其node(index) 方法遍历是根据前后半段遍历的,优化其索引访问元素的性能。
  • 创建:

    LinkedList<String> list = new LinkedList();   
    
  • 作为List的功能:

    • 双向链表的增删改查(每一个节点都包含:双指针,一个元素)
    • 允许包含NULL元素;
    • 非线程安全,在多线程环境下需要进行外部同步;
    • 插入和删除效率高,但随即范围的效率低;

ArrayList 和 LinkedList 使用区别

 当需要频繁随机访问元素的时候,例如读取大量数据并进行处理或者需要对数据进行排序或查找的场景,可以使用 ArrayList。例如一个学生管理系统,需要对学生列表进行排序或查找操作,可以使用 ArrayList 存储学生信息,以便快速访问和处理。

 当需要频繁插入和删除元素的时候,例如实现队列或栈,或者需要在中间插入或删除元素的场景,可以使用 LinkedList。例如一个实时聊天系统,需要实现一个消息队列,可以使用 LinkedList 存储消息,以便快速插入和删除消息。

 在一些特殊场景下,可能需要同时支持随机访问和插入/删除操作。例如一个在线游戏系统,需要实现一个玩家列表,需要支持快速查找和遍历玩家,同时也需要支持玩家的加入和离开。在这种情况下,可以使用 LinkedList 和 ArrayList 的组合,例如使用 LinkedList 存储玩家,以便快速插入和删除玩家,同时使用 ArrayList 存储玩家列表,以便快速查找和遍历玩家

3)Vector

  • Vector 是一个动态数组,可以根据需要自动增长。

  • 它是线程安全的,这意味着多个线程可以同时访问和修改 Vector,而不会出现数据不一致的问题。这是通过在每个方法上使用 synchronized 关键字实现的。

  • 由于线程安全,Vector 的性能相对较低。

4)Stack

  • 特点:

    • Stack 继承自 Vector,因此它也具有 Vector 的线程安全特性。
    • Stack 实现了一个后进先出(LIFO)的堆栈数据结构。
    • 它提供了 push()、pop()、peek() 等方法来操作堆栈。
  • 应用:

    • 后进先出,可以用来反转一串字符、实现计算器(将表达式进行变换其为后缀式)、浏览器的后退按钮等等

二、Set

  • 特点:存取无序,不可以存放重复的元素,不可以用下标对元素进行操作,和 List 有很多不同
  1. HashSet

    • 确保元素都是唯一,不允许重复;
    • 内部使用哈希表进行存储,让基本的操作(CRUD非常高效);
    • 线程不安全,多线程并发访问一个HashSet,至少有一个线程修改它,需要外部同步;
    • 适用于集合中删除重复条目,检测集合中是否存在某个元素;
    • 底层是HashMap
  2. LinkedHashSet

    • 继承HashSet的唯一性,线程不安全等HashSet的基本功能;
    • 维护了元素的插入顺序,内部使用哈希表和双向链表的组合来实现。哈希表用于确保元素的唯一性,而链表用于维护元素的插入顺- 序;
    • 适用于当需要存储唯一元素的集合,并且需要维护元素的插入顺序时,LinkedHashSet 是理想的选择;
    • 底层是LinkedHashMap;

三、Queue

  • Queue,也就是队列,通常遵循先进先出(FIFO)的原则,新元素插入到队列的尾部,访问元素返回队列的头部。

1)ArrayDeque

  • 特点:

    • 一种双端队列,支持在队列的头部和尾部进行插入和删除操作;
    • 底层基于数组实现,使用连续的内存空间存储元素,便于缓存,调高CPU的缓存机制;
    • 由于数组的连续内存特性,ArrayDeque 在大多数情况下比 LinkedList 具有更好的性能;
    • 支持队列和栈操作;
    • 当元素数量超过数组容量时,会自动进行扩容,扩容策略是将数组容量扩大为原来的两倍;
    • 非线程安全;
  • 实现原理:

    • 循环数组:
      • ArrayDeque 使用循环数组来实现双端队列。
      • 维护两个指针,分别指向队列的头部和尾部。
      • 当指针到达数组的末尾时,会循环到数组的开头。
    • 扩容:
      • 当元素数量超过数组容量时,会创建一个新的数组,并将原有元素复制到新数组中。
      • 扩容时,会将数组容量扩大为原来的两倍。扩容操作的时间复杂度为 O(n)

2)LinkedList

  • 定义: LinkedList 一般应该归在 List 下,只不过,它也实现了 Deque 接口,可以作为队列来使用。等于说,LinkedList 同时实现了 Stack、Queue、PriorityQueue 的所有功能。
  • 作为Queue的特点:
    • 双端队列:LinkedList 实现了 Deque 接口,可以作为双端队列使用,支持在队列的头部和尾部进行插入和删除操作。
    • 先进先出(FIFO):作为队列使用时,LinkedList 遵循先进先出的原则。可以使用 add() 或 offer() 方法在队列尾部添加元素,使用 remove() 或 poll() 方法在队列头部移除元素。
    • 先进后出(LIFO):LinkedList 实现了 Deque 接口,可以作为栈使用。可以使用 push() 方法在栈顶添加元素,使用 pop() 方法在栈顶移除元素。
    • 高效的插入和删除:在队列头部或尾部进行插入和删除操作的时间复杂度为 O(1)。
    • 内存占用:每个节点都需要额外的指针空间,因此内存占用相对较高。

3)PriorityQueue

  • 特点:

    • 优先级排序(元素的自然排序,或比较器自定义排序)
    • 动态维护:当插入或删除元素时,优先队列会自动调整元素的顺序,以保持优先级排序;
    • 高效的操作:插入和删除操作的时间复杂度通常为 O(log n),其中 n 是队列中的元素数量
  • 应用情景:

    • 任务调度:在操作系统中,优先队列可以用于任务调度,确保优先级高的任务先执行。
    • 事件处理:在事件驱动的系统中,优先队列可以用于事件处理,确保优先级高的事件先处理。
    • 算法优化:在一些算法中(例如 Dijkstra 算法),优先队列可以用于优化算法的性能。
    • 数据压缩:在哈夫曼编码中,优先队列可以用于构建哈夫曼树,从而实现数据压缩。
  • 实现原理:

    • 优先队列采用数组来实现堆(完全二叉树+堆属性)
    • 堆分为最大堆和最小堆:
      • 最大堆:父节点的优先级总是高于或等于子节点。
      • 最小堆(默认):父节点的优先级总是低于或等于子节点。
    • 插入操作:
      • 将新元素插入到堆的末尾。
      • 从新元素开始,向上调整堆的结构,直到满足堆的性质。
    • 删除操作:
      • 将堆顶元素(优先级最高的元素)与堆的最后一个元素交换。
      • 删除最后一个元素。
      • 从堆顶元素开始,向下调整堆的结构,直到满足堆的性质

四、Map

  • Map 保存的是键值对,键要求保持唯一性,值可以重复。
特性TreeMapHashMapLinkedHashMap
排序支持不支持不支持
插入顺序不保证不保证保证
查找效率O(log n)O(1)O(1)
空间占用通常较大通常较小通常较大
适用场景需要排序的场景无需排序的场景需要保持插入顺序

1)HashMap

  • 特点:

    • 快速查找:通过哈希函数计算键的哈希值,平均时间复杂度为 O(1)来查找、插入和删除元素。
    • 无序:不保证元素的插入顺序。
    • 非线程安全:在多线程环境下可能会出现数据不一致的问题。
    • 采用数组+链表/红黑树的存储结构;
  • 创建:

    HashMap<String, Integer> map = new HashMap<>();
    map.put("沉默", 20);    //增加元素
    map.remove("沉默");   //删除元素
    map.put("沉默", 30);    //覆盖掉之前的
    int age = map.get("沉默");   //查找元素
    

  • hash方法原理:
    • 先获取 key 对象的 hashCode 值,然后将其高位与低位进行异或操作,到一个新的哈希值。为什么要进行异或操作呢?因为对于 hashCode 的高位低位,它们的分布是比较均匀的,如果只是简单地将它们加起来或者进行位运算,易出现哈希冲突,而异或操作可以避免这个问题。
    • 然后将新的哈希值取模(mod),得到一个实际的存储位置。这个取模操作的的是将哈希值映射到桶(Bucket)的索引上,桶是 HashMap 中的一个数组,每个中会存储着一个链表(或者红黑树),装载哈希值相同的键值对(没有相同哈希值话就只存储一个键值对)。

  • HashMap的扩容机制:为了解决哈希冲突,提高查找效率。当 HashMap 中的元素数量达到一定的阈值时,就需要对哈希表进行扩容,以减少哈希冲突的发生
  • 步骤:
    • 创建一个新的数组,其容量是原数组的两倍。
    • 遍历原数组,将所有元素重新计算哈希值,并放到新的数组中。
    • 如果某个桶(bucket)中的元素超过了阈值,会将链表转换为红黑树(JDK 8)。

  • JDK 7 和 JDK 8 的区别
    • 扩容时机:
      • JDK 7:先判断是否需要扩容,再插入。
      • JDK 8:先进行插入,插入完成再判断是否需要扩容。
    • 数据迁移:
      • JDK 7:在扩容时,会重新计算所有元素的哈希值,并放到新的数组中。
      • JDK 8:在扩容时,会根据元素的哈希值的高位来判断元素在新数组中的位置,从而避免了重新计算哈希值。
    • 链表转红黑树:
      • JDK 7:当某个桶中的元素数量过多时,会形成一个长链表,导致查找效率降低。
      • JDK 8:当某个桶中的元素数量超过 8 个时,会将链表转换为红黑树,从而提高查找效率。
    • 插入方法:
      • JDK 7:使用头插法不需要遍历链表,因此插入速度很快,但可能回造成在多线程情况下引发死循环
      • JDK 8:使用尾插法避免了链表反转;当链表长度过长时,查询效率会降低,JDK 8 引入了红黑树,提高了查询效率
  • 为什么选择0.75的加载因子:选择 0.75 这个值是为了在时间和空间成本之间达到一个较好的平衡点,既可以保证哈希表的性能表现,又能够充分利用空间.
    • 空间效率:
      • 如果加载因子过高(接近 1),则 HashMap 会等到非常满时才扩容。
      • 这会提高空间利用率,但会导致更多的哈希冲突,从而降低查找效率。
    • 查找效率:
      • 如果加载因子过低(接近 0),则 HashMap 会在很早的时候就扩容。
      • 这会减少哈希冲突,提高查找效率,但会浪费大量的空间。

  • 线程不安全:
    • 多线程下扩容会死循环
    • 多线程下 put 会导致元素丢失
    • put 和 get 并发时会导致 get 到 null

2)LinkedHashMap

  • 特点:

    • 默认插入顺序: 新元素总是添加到尾部,顺序不变;
    • 无论哪种模式,链表头部都代表“最老”的元素,可以用于实现 LRU 缓存;
    • LinkedHashMap 继承了 HashMap 的所有特性,包括快速的键值对查找和插入;
    • 底层内部维护一个双向链表,用于记录键值对的插入顺序或访问顺序。
    • 两种排序模式:
      • 插入顺序(默认): 键值对按照它们被插入的顺序排序。
      • 访问顺序: 每次访问一个键值对,它都会被移动到链表的末尾,最近访问的元素在最后面
  • 工作原理:

    • 当向 LinkedHashMap 中插入一个键值对时,它首先根据键的哈希值计算出数组中的位置(桶)。
    • 然后,它将键值对存储在对应的桶中。
    • 同时,它将该键值对添加到双向链表的末尾,以维护插入或访问顺序。
    • 当需要遍历 LinkedHashMap 时,它会遍历双向链表,从而按照特定的顺序访问键值对。

3)TreeMap

  • 定义:一种基于红黑树(Red-Black tree)实现的有序键值对集合,红黑树是一种自平衡的二叉搜索树,它保证了在最坏情况下,基本操作(如插入、删除和查找)的时间复杂度为 O(log n)。

  • 特点:

    • 有序性:TreeMap 中的键值对按照键的自然顺序(1,2,3,4 or a,b,c,d)或自定义顺序排序。
    • 高效性:由于基于红黑树实现,TreeMap 的基本操作时间复杂度为 O(log n)。
    • 空间占用:与 HashMap 相比,TreeMap 通常占用更多的内存空间。
    • 适用场景:需要有序集合的场景。

  • 红黑树结构:

    • 每个节点要么是红色,要么是黑色。
    • 根节点是黑色。
    • 所有叶子节点(NIL)都是黑色。
    • 如果一个节点是红色,则它的两个子节点都是黑色。
    • 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
  • 工作原理:

    • 排序:
      • TreeMap 默认按照键的自然顺序(natural ordering)进行排序。
      • 如果键没有实现 Comparable 接口,或者需要自定义排序规则,可以在创建 TreeMap 时传入一个 Comparator 对象。
    • 插入:
      • 当插入一个新的键值对时,TreeMap 首先根据键的比较结果,找到合适的插入位置。
      • 然后,将新的节点插入到红黑树中。
      • 为了保持红黑树的平衡,可能需要进行旋转和重新着色等操作。
    • 删除:
      • 当删除一个键值对时,TreeMap 首先找到要删除的节点。
      • 然后,将该节点从红黑树中移除。
      • 为了保持红黑树的平衡,可能需要进行旋转和重新着色等操作。
    • 查找:
      • 当查找一个键时,TreeMap 从根节点开始,根据键的比较结果,逐步向下查找。
      • 由于红黑树的有序性,查找效率很高。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值