一次性讲清楚Java中的Array数组、List列表、Set集合、Map的底层逻辑
2026/9/13 23:31:41 网站建设 项目流程

一、开篇:为什么要懂底层物理状态?

先抛一个痛点:很多人学集合只知道addgetremove,但一被问到“ArrayList 和 LinkedList 有什么区别”、“HashSet 为什么查询快”,就答不上来。根本原因是不理解它们在内存中的物理形态。

二、一切的基石:数组(Array)

1. 核心特点:
  • 内存连续,像一排紧挨着的火车座位。

  • 有索引,可以通过arr[2]瞬间定位,查找数据时间复杂度O(1)。

  • 但是内置没有增删改查方法,例如删除元素代码需要自己来写循环。

  • 长度固定,一旦创建长度定死,不能扩容。

  • 存储类型L:Array 能存基本类型和对象

2. Java代码:
// 1. Array 可以存 int、double、char 等基本类型 int[] arr = new int[5]; // ✅ 存的是真正的 int 值(4 字节) double[] arr2 = new double[5]; // ✅ 存的是真正的 double 值 // 2. 创建时必须定死长度 int[] arr = new int[5]; // 2. 按下标赋值 arr[0] = 10; arr[1] = 20; arr[2] = 30; // arr[3] = 40; // ❌ 报错!ArrayIndexOutOfBoundsException(数组越界) // arr.add(40); // ❌ 报错!没有add()方法 // 3. 获取长度用属性 System.out.println(arr.length); // 输出 3
3. 内存图

三、List 体系(有序、有索引、可重复)

1. List 接口(规范)

合同内容:必须按插入顺序排列,必须给每个元素发索引号,允许重复元素。

2. ArrayList(动态数组)
  • 底层Object[] elementData(连续数组)。

  • 存储类型:ArrayList 只能存对象不能存基本类型

  • 特点:查询快(O(1)),增删慢(需要移动元素),但是有内置方法remove()。不仅如此,它还可以动态扩容。

  • 内存连续

3. Java代码
// 1. 用法 ArrayList 只能存对象(泛型必须是引用类型) ArrayList<Integer> list = new ArrayList<>(); // ✅ 只能写 Integer,不能写 int // ArrayList<int> list = new ArrayList<>(); // ❌ 编译报错! // 2. 插入、删除数据有对应的方法 list.add(1); list.add(2); // 插入元素 list.remove(0); // 删除元素 移除索引为 0 的元素
4. 内存图

四、Set 体系(无索引、不可重复)

1. Set 接口(规范)

合同内容:不提供索引,不允许重复元素。Java严格按照数学中的集合思想。

2. HashSet(底层是 HashMap)
  • 底层HashMap<E, Object>,只用了 Key,Value 固定为PRESENT

  • 特点:查询极快(O(1)),无序。

  • 内存数组连续 + 节点分散(哈希表结构)。

3. Java极简版代码
public class SimpleHashSetDemo { // ========== 1. 节点:数组里挂的东西 ========== static class Node { String key; // 元素(比如 "A") Node next; // 指向下一个节点(链表) Node(String key) { this.key = key; } } // ========== 2. 模拟 HashSet ========== static class HashSet { Node[] table = new Node[16]; // 底层数组,长度16 // 存元素 public void add(String key) { // ① 算哈希值,取模得到数组索引 int index = key.hashCode() % 16; // ② 如果这个索引是空的,直接放进去 if (table[index] == null) { table[index] = new Node(key); return; } // ③ 如果这个索引已经有东西了(哈希冲突),挂到链表后面 Node current = table[index]; while (current.next != null) { current = current.next; } current.next = new Node(key); } } }
4. 内存图

5. 解析说明

HashSet 的底层其实与 HashMap 用的是同一套代码逻辑,这也是 Java 设计时的一种“懒惰”体现——说白了,HashSet 本质上就是 HashMap 的 Key 集合,Value 统一使用一个默认的静态常量对象PRESENTprivate static final Object PRESENT = new Object();)。因此,HashSet 自身的泛型是<E>,而它内部持有的 HashMap 则使用了<E, Object>泛型,其中 E 代表任意元素类型。

HashSet 在存入元素时,会先根据元素的hashCode()计算出哈希值,再与数组长度进行取模运算(实际源码中使用位运算,效果等价),从而定位到数组的某个索引。假设字符'A'的哈希值为 66,数组长度为 16,那么66 % 16 = 2,因此它会被放到数组索引 2 的位置。如果此时再存入字符'Q',其哈希值若为 82,则82 % 16 = 2,同样得到索引 2,但该位置已经被'A'占据,这就发生了哈希冲突。Java 采用拉链法来解决冲突:将新节点挂在已有节点后面,形成链表(当链表长度超过 8 时转为红黑树)。所以索引 2 的位置会挂着一串节点,'A'在前,'Q'在后,通过next指针相连。

五、Map 体系(键值对)

1. HashMap(最常用)
  • 底层Node<K,V>[] table(数组 + 链表/红黑树)。

  • 特点:Key 唯一,查询快。

  • 内存:数组连续,节点分散。

2. 解析说明

HashMap 的底层内存结构与 HashSet 所依赖的底层结构完全一致——因为 HashSet 本身就是基于 HashMap 实现的。它们都采用“数组 + 链表/红黑树”的哈希表结构,通过哈希值定位数组索引,用链表或红黑树解决哈希冲突。

唯一的区别在于:HashSet 只使用了 Key,Value 被固定为一个默认的占位对象PRESENT;而 HashMap 的 Value 是由开发者指定的,因此它拥有自己的一套泛型HashMap<K, V>。其中K代表键(Key),可以是任意类型;V代表值(Value),也可以是任意类型。

文章最后,我会带大家复习一下泛型中常见字符的含义,帮助大家彻底记住TEKV这些符号的用法。

六、总结与复习

1. Array和ArrayList的区别
对比项ArrayArrayList<E>
长度固定动态扩容
存储类型基本类型 + 对象只能存对象
性能更快稍慢(装箱拆箱 + 方法调用)
类型检查运行时编译时(泛型)
长度获取arr.length(属性)list.size()(方法)
方法丰富度几乎没有一大堆现成方法
协变支持不支持
多维原生支持需要嵌套
适用场景性能敏感、长度固定日常开发、频繁增删
2. 内存连续性大对比(核心表格)
集合类底层结构内存是否连续有索引?允许重复?
Array原生数组✅ 连续✅ 有✅ 允许
ArrayListObject[]✅ 连续✅ 有✅ 允许
LinkedList双向链表❌ 分散✅ 有(但慢)✅ 允许
HashSetHashMap⚠️ 数组连续 + 节点分散❌ 无❌ 不允许
HashMap数组 + 链表/红黑树⚠️ 数组连续 + 节点分散❌ 无(按 Key 查)❌ Key 不可重复
3. 常见的泛型符号及含义(约定俗成)
符号全称含义典型使用场景
TType通用类型(最常用)class Box<T>,表示“某个类型”
EElement元素类型List<E>Set<E>,表示“集合里的元素”
KKeyMap<K, V>的键
VValueMap<K, V>的值
NNumber数字类型class Calculator<N extends Number>
RReturn返回值类型interface Function<T, R>
?通配符未知类型List<?>,表示“任意类型的 List”
SU(无全称)第二、第三类型参数class Triple<T, S, U>

泛型符号说明:

其实TEKV这些占位符本质上都代表同一个意思——任意类型。那为什么ArrayList<E>的源码用E,而不用TKV呢?

其实不是不行,而是会大大降低代码可读性EElement(元素)的首字母,因为集合里存的就是元素,所以用E让人一看就懂。

那又有人要问了:为什么HashMap<K, V>不写成HashMap<T, T>呢?

对,T确实代表任意类型,但如果你写成HashMap<T, T>,就等于强制规定键和值的类型必须完全一致。比如HashMap<String, String>两个都必须是String,这就大大限制了 Map 存储数据的灵活性

举个例子,如果你想存HashMap<String, Object>(键是 String,值是任意对象),用HashMap<T, T>就做不到了。所以源码中必须使用不同的占位符来区分键和值。

但为了代码可读性,又不能随便乱起名,于是采用了约定俗成的KV

  • K= Key(键)

  • V= Value(值)

这样一来,既保证了键和值可以是不同类型,又让人一眼就能看懂谁是谁。

4. Java中常见的数组、集合接口和实现类的关系
名称是接口还是类继承/实现关系
Array(数组)语言内置类型无(不是类也不是接口)
List接口继承Collection
ArrayList实现List
LinkedList实现ListDeque
Set接口继承Collection
HashSet实现Set
LinkedHashSet继承HashSet
TreeSet实现NavigableSet
Map接口不继承Collection
HashMap实现Map
LinkedHashMap继承HashMap
TreeMap实现NavigableMap

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询