java集合

List

核心特点

  • 存取有序
  • 可以重复
  • 有索引

常见实现类

ArrayList和LinkedList是Java集合框架中List接口的两个常见实现类,它们在底层实现和性能特点上有以下几点区别:

  • 底层数据结构: ArrayList使用数组 来存储元素,而LinkedList使用双向链表来存储元素。
  • 随机访问性能: ArrayList支持高效的随机访问(根据索引获取元素),因为它可以通过下标计算元素在数组中的位置。而LinkedList在随机访问方面性能较差,获取元素需要从头或尾部开始遍历链表找到对应位置。
  • 插入和删除性能: ArrayList在尾部添加或删除元素的性能较好,因为它不涉及数组的移动。而在中间插入或删除元素时,ArrayList涉及到元素的移动,性能相对较低。LinkedList在任意位置进行插入和删除操作的性能较好,因为只需要调整链表中的指针即可。

综上所述,如果需要频繁进行随机访问操作或在尾部进行插入和删除操作,可以选择ArrayList。如果需要频繁进行中间位置的插入和删除操作,可以选择LinkedList。


Set

核心特点

  • 不可以存储重复元素
  • 没有索引,不能使用普通for循环遍历

常见实现类

  • HashSet
    • 底层数据结构是哈希表 (在 JDK 1.8 中,由数组 + 链表 + 红黑树组成)。它利用 HashMap 的 key 机制来保证元素的唯一性,但不保证顺序。
    • 存取无序
    • 不可以存储重复元素
    • 没有索引,不能使用普通for循环遍历
  • TreeSet
    • 底层数据结构是 红黑树(一种自平衡的二叉搜索树)。它保证元素的唯一性,并且根据元素的自然排序或指定的比较器进行有序存储
    • 不可以存储重复元素
    • 没有索引
    • 可以将元素按照规则进行排序
      • TreeSet():根据其元素的自然排序进行排序
      • TreeSet(Comparator comparator) :根据指定的比较器进行排序

Queue

核心特点

Queue以及Deque都是继承于Collection,Deque是Queue的子接口。

Queue是FIFO的单向队列,Deque是双向队列。

Queue有一个直接子类PriorityQueue,而Deque中直接子类有两个:LinkedList以及ArrayDeque。

常见实现类

  • LinkedList

    • 底层结构:基于双向链表实现

    • 功能特性:

      • 既可作为列表,也可作为队列或双端队列(Deque)使用
      • 非线程安全,性能较高
      • 允许存储null元素
    • 使用示例:

      java 复制代码
      Queue<String> queue = new LinkedList<>();
      queue.offer("A");
      queue.offer("B");
      System.out.println(queue.poll()); // 输出 A
  • ArrayDeque

    • 底层结构:基于动态数组实现的双端队列

    • 功能特性:

      • LinkedList更高效(尤其是随机访问和内存占用)
      • 不支持 null元素
      • 非线程安全
    • 使用示例:

      java 复制代码
      Queue<String> queue = new ArrayDeque<>();
      queue.offer("X");
      System.out.println(queue.poll()); // 输出 X
  • PriorityQueue

    • 底层结构:基于优先堆(二叉堆)实现的无界优先级队列

    • 功能特性:

      • 元素按自然顺序 或自定义Comparator排序
      • 出队顺序为优先级最高者(最小值优先,自然排序)
      • 不支持 null元素
      • 非线程安全
    • 使用示例:

      java 复制代码
      Queue<Integer> pq = new PriorityQueue<>();
      pq.offer(3);
      pq.offer(1);
      pq.offer(2);
      System.out.println(pq.poll()); // 输出 1(最小值)

Map

核心特点

  • 双列集合,一个键对应一个值
  • 键不可以重复,值可以重复

常见实现类

  • HashMap
    • HashMap底层是哈希表结构的
    • 依赖hashCode方法和equals方法保证键的唯一
    • 如果键要存储的是自定义对象,需要重写hashCode和equals方法
  • TreeMap
    • reeMap底层是红黑树结构
    • 依赖自然排序或者比较器排序,对键进行排序
    • 如果键存储的是自定义对象,需要实现Comparable接口或者在创建TreeMap对象时候给出比较器排序规则

HashMap集合

简介

HashMap 是 非线程安全 的键值对存储结构,JDK 1.8 通过 链表→红黑树 优化哈希冲突场景,仅在链表>8 且数组>64 时 转换,确保查询效率最大化。

数据结构

在JDK1.8 之前 HashMap 由 数组+链表 数据结构组成的。

在JDK1.8 之后 HashMap 由 数组+链表 +红黑树数据结构组成的。

java 复制代码
Map<String, Integer> map = new HashMap<>();

map.put("Apple", 10);
map.get("Apple")

HashMap成员变量

  • 集合的初始化容量( 必须是二的n次幂 )

    java 复制代码
    //默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   

    问题: 为什么必须是2的n次幂?如果输入值不是2的幂比如10会怎么样?

    HashMap构造方法还可以指定集合的初始化容量大小。

    • 当我们根据key的hash确定其在数组的位置时,如果n为2的幂次方,可以保证数据的均匀插入,如果n不是2的幂次方,可能数组的一些位置永远不会插入数据,浪费数组的空间,加大hash冲突。
    • 另一方面,一般我们可能会想通过 % 求余来确定位置,这样也可以,只不过性能不如 & 运算。而且当n是2的幂次方时:hash & (length - 1) == hash % length
    • 因此,HashMap 容量为2次幂的原因,就是为了数据的的均匀分布,减少hash冲突,毕竟hash冲突越大,代表数组中一个链的长度越大,这样的话会降低hashmap的性能
    • 如果创建HashMap对象时,输入的数组长度是10,不是2的幂,HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字。
  • 默认的负载因子,默认值是0.75

    java 复制代码
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • 当链表的值超过8则会转红黑树(1.8新增)

    java 复制代码
     //当桶(bucket)上的结点数大于这个值时会转成红黑树
     static final int TREEIFY_THRESHOLD = 8;

    问题:为什么Map桶中节点个数超过8才转为红黑树?

    HashMap选择8作为链表转红黑树的阈值,是基于泊松分布的科学概率计算(链表长度≥8的概率仅6e-8),在空间(红黑树节点占用2倍内存)和时间(链表查询O(n) vs 红黑树O(log n))之间取得最佳平衡。同时要求数组长度≥64,是因为小数组扩容比转红黑树更高效,确保整体性能最优。

  • 当链表的值小于6则会从红黑树转回链表

    java 复制代码
    //当桶(bucket)上的结点数小于这个值时树转链表
     static final int UNTREEIFY_THRESHOLD = 6;
  • 当Map里面的数量超过这个值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,而不是树形化

    java 复制代码
    //桶中结构转化为红黑树对应的数组长度最小的值 
    static final int MIN_TREEIFY_CAPACITY = 64;
  • table用来初始化(必须容量是二的n次幂)(重点)

    java 复制代码
    //存储元素的数组 
    transient Node<K,V>[] table;
  • HashMap中存放元素的个数(重点)

    java 复制代码
    //存放元素的个数,注意这个不等于数组的长度。
     transient int size;
  • 用来记录HashMap的修改次数

    JAVA 复制代码
    // 每次扩容和更改map结构的计数器
     transient int modCount;  
  • 用来调整大小下一个容量的值计算方式为(容量*负载因子)

    JAVA 复制代码
    // 临界值 当实际大小(容量*负载因子)超过临界值时,会进行扩容
    int threshold;
  • 哈希表的加载因子(重点)

    java 复制代码
    // 加载因子
    final float loadFactor;
    • loadFactor 加载因子,是用来衡量 HashMap 满的程度,表示HashMap的疏密程度,影响hash操作到同一个数组位置的概率

      loadFactor的默认值为0.75f是官方给出的一个比较好的临界值

      当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能。,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。

    • 为什么加载因子设置为0.75,初始化临界值是12?

      loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。

      复制代码
      例如:
      加载因子是0.4。 那么16*0.4--->6 如果数组中满6个空间就扩容会造成数组利用率太低了。
      加载因子是0.9。 那么16*0.9---->14 那么这样就会导致链表有点多了。导致查找元素效率低。

      所以既兼顾数组利用率又考虑链表不要太多,经过大量测试0.75是最佳方案。

      threshold计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。这个值是当前已占用数组长度的最大值。当Size>=threshold的时候,那么就要考虑对数组的resize(扩容),也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准 。 扩容后的 HashMap 容量是之前容量的两倍

new HashMap()

new HashMap() 的时候发生了什么?

值初始化了一个负载因子 this.loadFactor = DEFAULT_LOAD+FACTOR (0.75)

负载因子:数组大小不会改变,当到达某个阈值的时候,就需要进行扩容。(阈值 = 当前数组大小 * 负载因子)

HashMap采用的是一种"懒加载"模式,当向HashMap种put第一个元素的时候,HashMap的数组才会建立。

put

源码中put方法的过程:

  • 调用hash()(扰动函数)函数,将拿到key的hash值,table就是当前HashMap的结构,table是Node的数组对象。

  • 判断table是否为空(第一次put值肯定为空),如果为空则调用resize()新建一个table对象,默认的新建数组的长度是16

    resize()方法十分复杂,但是做的事很简单(新建、扩容)

    • 新建:如果table为空,则新建一个数组长度为16的table
    • 扩容:如果是扩容操作,则新建一个数组长度为原数组长度两倍的新数组。把原数组的值放到新数组里,放的过程要对所有元素的Key的hash值与当前数组的长度-1取与(&)
  • 判断( n-1 ) & hash(数组索引计算,这种方式确保了索引在数组的有效范围内)这个位置上是否有值

    • 如果没有值:将 k , v 封装到Node对象当中(节点),赋值到 ( n-1 ) & hash 这个位置上

    • 如果有值:

      • 如果hash值相同 && (key是同一个对象 || key值相同)则取代当前节点的值

      • 节点p是不是红黑树的节点,如果是,调用红黑树的存储方式

      • 链表头开始查找比较key的值,如果next为null则新建一个Node放在next上。如果找到"一样的"就覆盖,如果都不一样,则添加到最后。添加的时候比较目前的链表长度是否 ≥ 8,如果是,则(扩容数组或改用红黑树的存储方式)

        注意:如果某个位置的链表长度 >= 8,但是整个HashMap的长度小于64,是扩充数组,而不是转为红黑树。

问答

  1. HashMap中hash函数(扰动函数)是怎么实现的?

    对于key的hashCode做hash操作,无符号右移16位然后做异或运算。这种处理方式的目的是将高位和低位的信息结合起来,从而减少哈希冲突,提高哈希表的性能。

    java 复制代码
     (h = key.hashCode()) ^ (h >>> 16)
  2. 当两个对象的hashCode相等时会怎么样?

    会产生哈希碰撞,若key值内容相同则替换旧的value.不然连接到链表后面,链表长度超过阈值8就转换为红黑树存储。

  3. 何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞?

    只要两个元素的key计算的哈希码值相同就会发生哈希碰撞。jdk8前使用链表解决哈希碰撞。jdk8之后使用链表+红黑树解决哈希碰撞。

  4. 如果两个键的hashcode相同,如何存储键值对?

    hashcode相同,通过equals比较内容是否相同。

    相同:则新的value覆盖之前的value

    不相同:则将新的键值对添加到哈希表中

  5. 在不断的添加数据的过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。threshold( 临界值) = capacity(容量) * loadFactor( 加载因子 )

  6. JDK1.8为什么引入红黑树?这样结构的话不是更麻烦了吗

    JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。 当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。

get

源码中get方法的过程:

  • 调用hash()方法将拿到key的hash值
  • 拿到( n-1 ) & hash 这个位置的值
    • 如果如果hash值相同 && (key是同一个对象 || key值相同),直接返回
    • 否则,链表中有下一个值。如果下一个值是红黑树的值,则调用红黑树获取节点的方式。反之则从链表头开始查找比较key的值,如果找到"一样的"就返回,如果都不一样返回null。

扩容机制

什么时候才需要扩容

当HashMap中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中的元素个数超过16×0.75=12(这个值就是阈值或者边界值threshold值)的时候,就把数组的大小扩展为2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预知元素的个数能够有效的提高HashMap的性能。
HashMap的扩容是什么

进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。

HashMap在进行扩容时,使用的rehash方式非常巧妙,因为每次扩容都是翻倍,与原来计算的 (n-1)&hash的结果相比,只是多了一个bit位,所以我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就可以了,是0的话索引没变,是1的话索引变成"原索引+oldCap(**原位置+旧容量**)"。

例如我们从16扩展为32时,具体的变化如下所示:

复制代码
n:16 			0000 0000 0000 0000 0000 0000 0001 0000
n-1:15			0000 0000 0000 0000 0000 0000 0000 1111
假设hashCode生成的值:
(n-1)&hash
hash1(key1):	1111 1111 1111 1111 0000 1111 0000 0101
hash2(key2):	1111 1111 1111 1111 0000 1111 0001 0101
--------------------------------------------------------------
				0000 0000 0000 0000 0000 0000 0000 0101	计算出的索引是5
扩容 16*2 => 32
n:32 			0000 0000 0000 0000 0000 0000 0010 0000
n-1:31			0000 0000 0000 0000 0000 0000 0001 1111
假设hashCode生成的值:
(n-1)&hash
hash1(key1):	1111 1111 1111 1111 0000 1111 0000 0101
hash2(key2):	1111 1111 1111 1111 0000 1111 0001 0101
--------------------------------------------------------------
				0000 0000 0000 0000 0000 0000 0000 0101	计算出的索引是5
				0000 0000 0000 0000 0000 0000 0001 0101	计算出的索引是5+16=21

说明:5是假设计算出来的原来的索引。这样就验证了上述所描述的:扩容之后所以节点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置。

相关推荐
凯子坚持 c2 小时前
Qt常用控件指南(5)
开发语言·数据库·qt
weixin_BYSJ19872 小时前
django农作物批发交易系统--附源码24008
java·javascript·spring boot·python·django·flask·php
foundbug9992 小时前
MATLAB实现轴承刚度计算
开发语言·matlab
Knight_AL2 小时前
Java + FFmpeg 实现视频分片合并(生成 list.txt 自动合并)
java·ffmpeg·音视频
C++ 老炮儿的技术栈2 小时前
CMFCEditBrowseCtrl用法一例
c语言·开发语言·c++·windows·qt·visual studio code
Three~stone2 小时前
Matlab R2024b 保姆级安装教程(附:解决win10问题)
开发语言·算法·matlab
ytttr8732 小时前
基于MATLAB的一维对流扩散方程数值求解
开发语言·算法·matlab
空空kkk2 小时前
SpringBoot整合Thymeleaf
java·spring boot·spring
程序员spped2 小时前
分享一套非常不错的基于Python的Django图书馆(自习室)座位预约管理系统
开发语言·python·座位预约