Java集合的底层原理

embedded/2025/3/19 9:03:44/

目录

Collection

Arraylist

HashSet

介绍

哈希值

哈希表的基本概念

HashSet 的内部实现

HashMap

哈希碰撞的处理

总结

TreeSet

特点

红黑树的特性

红黑规则

TreeSet 的内部实现

1. 存储结构

2. 添加元素(重点)

3. 查找元素

4. 删除元素

总结

几个问题思考


Collection

  1. List
    • Arraylist: Object数组
    • Vector: Object数组
    • LinkedList: 双向链表
  1. Set
    • HashSet(无序,唯一):基于 HashMap 实现的底层采用 HashMap 来保存元素
    • LinkedHashSet: LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
    • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)
  • Map
  • HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
  • LinkedHashMap(存取有序):LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。

  • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap(可排序): 红黑树(自平衡的排序二叉树)

存取有序:存和取的元素顺序相同。

Arraylist

注:扩容时会创建一个新数组,会先将原数组的数据拷贝到新数组当中。。。

HashSet

介绍

HashSet 的底层实现主要依赖于 HashMap。在 Java 中,HashSet 的实现是通过内部维护一个 HashMap 实例来完成的,而 HashMap 则是基于哈希表的数据结构。哈希表是一种使用哈希函数来快速访问数据的数据结构,它通过把键映射到表中一个位置来访问记录,以加快查找的速度。

哈希值

重写自定义类的 equals 方法以及 hashCode 方法:

举例(Student 类):

哈希表的基本概念

  1. 哈希函数:哈希表首先会定义一个哈希函数,用于将键转换为数组索引。在 Java 的 HashSet 中,这个哈希函数会使用hashCode() 方法来生成一个哈希码。
  2. 数组 + 链表/红黑树:哈希表通常由一个数组组成,数组的每个槽位(slot)或称为桶(bucket),可以存储一个或多个元素。如果两个不同的键通过哈希函数产生了相同的索引(哈希碰撞),那么这些键将以链表的形式存储在同一个桶中。在 Java 8 之后,如果链表长度超过一定阈值(默认为8),链表会转换为红黑树,以减少查找时间。

HashSet 的内部实现

注:

当数组存储的数据达到 (0.75*数组长度) 时,会对数组进行2倍扩容。

链表长度大于8而且数组长度大于等于64 时:链表自动转成红黑树

如果集合中存储的是自定义对象,必须要重写hashcodeequals方法!!!

  1. 存储结构HashSet 内部维护了一个 HashMap 实例,这个 HashMap 的每个键值对中的值都是一个固定的对象(在 HashSet 的实现中,这个对象并不重要,通常是一个名为 PRESENT 的静态 final 对象)。
  2. 添加元素:当向 HashSet 添加一个元素时,首先会调用这个元素的 hashCode() 方法来计算它的哈希码(哈希值),然后根据哈希码找到对应的桶。如果这个桶中没有元素,或者元素与要添加的元素相等(通过 equals() 方法比较),则将元素添加到桶中。如果发生哈希碰撞,新元素将被添加到链表的末尾,或者添加到红黑树中。
  3. 查找元素:当要查找一个元素时,HashSet 会使用同样的哈希函数来计算元素的哈希码,然后到对应的桶中查找。如果找到了相等的元素,则返回 true。如果桶中有多个元素(因为哈希碰撞),那么会使用 equals() 方法来逐个比较链表或红黑树中的元素。
  4. 删除元素:删除元素的过程与查找类似,首先找到元素所在的桶,然后从链表或红黑树中删除元素。

HashMap

HashSet 内部本质是维护了一个 HashMap 实例,所以它们的底层基本相同,

唯一不同的地方:

当用 equals 方法比较属性值之后,如果一样的话,HashSet 底层是不存新元素,而 HashMap 的底层是覆盖(新加的元素覆盖老的元素)。

其实 HashSet 的底层也是覆盖,只是因为它不存和覆盖的效果一样,而 HashMap 键一样但是值可能不一样,就会出现覆盖的效果。

哈希碰撞的处理

哈希碰撞是哈希表中的一个重要概念,当两个不同的键产生了相同的哈希码时,就会发生哈希碰撞。Java 的 HashSet 通过链表或红黑树来解决哈希碰撞:

  • 链表:在发生碰撞的情况下,新元素将被添加到链表的末尾。如果链表变得过长,那么它的性能会下降,因为查找时间会变长。
  • 红黑树:在 Java 8 中,如果一个桶中的链表长度超过阈值(默认为8),链表会被转换成红黑树红黑树是一种自平衡的二叉搜索树,它能够保持树的平衡,从而保证查找、插入和删除的最坏情况时间复杂度为 O(log n)。

总结

HashSet 的底层哈希表实现提供了高效的添加、删除和查找操作。它的性能取决于哈希函数的质量、哈希碰撞的处理以及负载因子的设置。负载因子决定了哈希表什么时候进行扩容,默认值为 0.75,这意味着当哈希表中的元素数量达到数组大小的 75% 时,哈希表会进行扩容操作,以减少哈希碰撞的几率。

TreeSet

TreeSet 是 Java 中 SortedSet 接口的一个实现,它基于红黑树 数据结构来存储元素。红黑树是一种自平衡的二叉搜索树,它能够保持树的平衡,从而保证最坏情况下的查找、插入和删除操作的时间复杂度为 O(log n)。下面是 TreeSet 底层实现的详细介绍:

特点

可排序,不重复,无索引

红黑树的特性

红黑树具有以下五个主要特性:

  1. 每个节点包含一个颜色属性:节点可以是红色或黑色。
  2. 根节点是黑色红黑树的根节点必须是黑色。
  3. 红色节点不能连续红黑树中不能有两个连续的红色节点,即红色节点的子节点和父节点不能是红色。
  4. 从任一节点到其每个叶子节点的所有路径都包含相同数量的黑色节点:这意味着从根节点到叶子节点的所有路径上的黑色节点数目是相同的,这个属性称为“黑色完美性”。
  5. 新插入的节点是红色:新插入的节点默认为红色,以保持其他红黑树性质的稳定性。

红黑规则

TreeSet 的内部实现

1. 存储结构

TreeSet 内部维护了一个 NavigableMap 实例,默认情况下这个实例是 TreeMap。TreeMap 的底层实现就是红黑树,它存储了排序后的键值对,而 TreeSet 实际上只是使用了 TreeMap 的键来存储元素,值则是一个固定的对象(通常是一个静态的 final 对象)

2. 添加元素(重点)

当向 TreeSet 添加一个元素时,TreeMap 会根据元素的比较结果(自然排序或者比较器排序)来确定元素的存储位置。如果元素(自定义的类)实现了 Comparable 接口,那么会使用这个接口的方法来比较元素。如果没有实现 Comparable,那么在创建 TreeSet 集合时必须自定义Comparator比较器对象来定义元素的排序规则。添加元素时,红黑树会进行颜色变更和旋转操作,以保持树的平衡。

也可以分为以下两种情况:

Java写好的类
//1.自然排序/默认排序
//Integer Double  默认情况下按照升序排列(Java的内部源码)
//String  按照字母在ASCll码表中对应的数字升序排列
//2.比较器排序
//创建集合时传递Comparator比较器对象,指定比较规则
自定义的类
//1.默认排序/自然排序
//自定义类实现Comparable接口,再重写compareTo方法(用于指定排序规则)
//2.比较器排序
//创建集合时传递Comparator比较器对象,指定比较规则

代码举例:

如:(比较器排序)

import java.util.Comparator;
import java.util.TreeSet;public class TreeSetExample {public static void main(String[] args) {//使用匿名内部类创建 TreeSet,并传入自定义的比较器TreeSet<Integer> ts1 = new TreeSet<>(new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {//o1:当前要添加的元素//o2:表示已经在红黑树中存在的元素return o2 - o1;//实现降序功能//o2 - o1 可能导致整数溢出,//return Integer.compare(o2, o1); //这个是更安全的降序比较}});//使用Lambda表达式创建 TreeSet,并传入自定义的比较器TreeSet<Integer> ts2 = new TreeSet<>((o1, o2) -> o2 - o1);// 向 TreeSet 中添加元素ts1.add("Apple");ts1.add("Banana");ts1.add("Cherry");ts1.add("Date");// 输出 TreeSet 中的元素System.out.println(ts1);}
}

如:(自然排序、默认排序)

public class Student2 implements Comparable<Student2> {private String name;private int age;public Student2() {}public Student2(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String toString() {return "Student2{name = " + name + ", age = " + age + "}";}//自定义类实现Comparable接口,再重写compareTo方法(用于指定排序规则)@Overridepublic int compareTo(Student2 o) {//this:当前要添加的元素//o:表示已经在红黑树中存在的元素//返回值://负数:表示当前要添加的元素是小的,存左边//正数:表示当前要添加的元素是大的,存右边//0:覆盖int i = this.getAge() - o.getAge();//按照年龄升序排序//年龄一样按照姓名的字母排序i = i == 0 ? this.getName().compareTo(o.getName()) : i;return i;}
}

自然排序和比较器排序的合理使用:

  • 使用自然排序:当集合中的元素类型已经实现了 Comparable 接口,并且你希望使用这个排序规则时,可以选择自然排序。这是最简单的情况,因为你不需要提供额外的比较器。
  • 使用比较器排序:当集合中的元素类型没有实现 Comparable 接口,或者你希望使用不同于自然顺序的排序规则时,应该提供一个自定义的 Comparator。这允许你根据特定的业务逻辑或者需求来定义排序规则。
  • 注意事项:在使用 TreeSet 时,确保添加到集合中的所有元素都是可比较的,无论是通过自然排序还是通过比较器排序。如果元素不可比较,或者在比较时抛出 ClassCastException,那么在添加元素时会抛出异常。
  • 性能考虑比较器排序可能会稍微降低性能,因为它需要使用额外的 Comparator 对象来进行比较。如果性能是关键考虑因素,并且可以接受自然排序,那么应该优先使用自然排序。

3. 查找元素

查找元素时,TreeSet 会使用红黑树的搜索性质来定位元素。由于树是平衡的,所以查找操作的时间复杂度为 O(log n)。

4. 删除元素

删除元素时,TreeSet 会先找到要删除的节点,然后根据不同情况执行删除操作,并可能需要进行颜色变更和旋转操作,以保持红黑树的性质。

总结

TreeSet 的底层红黑树实现提供了有序的集合存储,并且保证了高效的添加、删除和查找操作。由于红黑树的自平衡特性,TreeSet 能够在元素动态变化的情况下保持良好的性能。与基于哈希表的 HashSet 相比,TreeSet 在元素顺序和排序功能上提供了更多的支持,但可能在某些操作上稍微慢一些,尤其是在最坏情况下的性能。

几个问题思考


http://www.ppmy.cn/embedded/173813.html

相关文章

DexClassLoader 动态加载机制

DexClassLoader 动态加载机制 DexClassLoader 是 Android 提供的 动态加载 DEX&#xff08;Dalvik Executable&#xff09;文件 的工具&#xff0c;允许应用在 运行时 加载 .dex 或 .apk 文件中的类&#xff0c;而不需要在编译时静态引入。 1. DexClassLoader 介绍 DexClassL…

使用PyMongo操作MongoDB(一)

使用PyMongo操作MongoDB MongoDB作为一款流行的NoSQL数据库&#xff0c;以其灵活的数据模型和强大的查询能力受到开发者青睐。通过PyMongo库&#xff0c;我们可以在Python中轻松实现与MongoDB的交互。本文将系统介绍PyMongo的安装、连接及数据库操作全流程。 一、环境准备 安…

Redis客户端Jedis、Lettuce 和 Redisson优缺点总结

https://developer.huawei.com/consumer/cn/blog/topic/03825550899620047 Redis 官方推荐的 Java 客户端有Jedis、Lettuce 和 Redisson。本文总结这些客服端的优缺点 1. Jedis Jedis 是老牌的 Redis 的 Java 实现客户端&#xff0c;提供了比较全面的 Redis 命令的支持&#…

总结 kotlin中的关键字和常用方法

Kotlin 的关键字分为 硬关键字&#xff08;Hard Keywords&#xff09;、软关键字&#xff08;Soft Keywords&#xff09; 和 修饰符关键字&#xff08;Modifier Keywords&#xff09;。以下是分类整理的关键字列表及其核心用途&#xff1a; 1. 硬关键字&#xff08;Hard Keywor…

【数据库】如何用索引优化查询性能

引言 在数据库查询中&#xff0c;索引是提升性能的关键工具。合理使用索引可以显著减少数据扫描量&#xff0c;加快查询速度。然而&#xff0c;索引的使用也需要谨慎&#xff0c;错误的索引策略可能导致性能下降甚至系统崩溃。本文将深入探讨如何通过索引优化查询性能&#xf…

函数(函数的概念、库函数、自定义函数、形参和实参、return语句、数组做函数参数、嵌套调用和链式访问、函数的声明和定义、static和extern)

一、函数的概念 •C语⾔中的函数&#xff1a;⼀个完成某项特定的任务的⼀⼩段代码 •函数又被翻译为子函数&#xff08;更准确&#xff09; •在C语⾔中我们⼀般会⻅到两类函数&#xff1a;库函数 ⾃定义函数 二、库函数 1 .标准库和头文件 •C语⾔的国际标准ANSIC规定了⼀…

硬件设计抽象级别详解:门级、RTL级、行为级与HLS

硬件设计抽象级别详解&#xff1a;门级、RTL级、行为级与HLS 引言 在数字系统设计领域&#xff0c;硬件描述语言(HDL)提供了多种抽象级别来描述电路功能和结构。从最底层的门级描述到高层的行为级描述&#xff0c;每一种抽象级别都有其特定的用途和优势。理解这些不同级别以及…

Trae AI 能力:开启跨系统开发新时代,让远程协作与定制化开发触手可及

目录 前言 Trae 国内版&#xff1a;AI 原生 IDE 的新突破 1. Builder 模式&#xff1a;从自然语言到代码的“零摩擦”开发 2. 上下文深度感知&#xff1a;更懂代码的智能伙伴 3. 实时代码智能补全&#xff1a;让编码速度“质变” 4. AI 协作&#xff1a;跨模块开发与实时…