每日面试题05:ArrayList和LinkedList的底层原理

ArrayList与LinkedList深度解析:从底层原理到实战选择

在Java的List接口实现中,ArrayListLinkedList是最常用的两种选择。面试中“它们的区别”几乎是必问题,但仅仅停留在“数组vs链表”的层面显然不够。本文将从​​底层数据结构、内存布局、核心操作性能、线程安全、实际场景选择​​等维度展开深度解析,并结合性能测试数据,帮你彻底掌握两者的差异与适用场景。


一、底层数据结构:连续内存 vs 离散节点

1. ArrayList:动态扩容的数组

ArrayList的底层是​​动态数组​​(本质是Object[] elementData),其核心特点是​​内存连续​​。这意味着:

  • ​随机访问高效​​:数组的索引与内存地址直接对应(通过索引*元素大小+起始地址计算),因此通过索引访问元素的时间复杂度是O(1)
  • ​动态扩容​​:数组初始容量默认是10(若通过无参构造创建),当元素数量超过容量时,会触发扩容机制:新容量 = 原容量 + 原容量/2(即1.5倍扩容)。扩容时需要新建数组并复制原数据(Arrays.copyOf()),这一步的时间复杂度是O(n),但属于“均摊操作”——大多数情况下添加操作的时间复杂度仍是O(1)(只有扩容时才会额外消耗)。
2. LinkedList:双向链表的节点网络

LinkedList的底层是​​双向链表​​(本质是Node节点的链式结构),每个节点(private static class Node<E>)包含三个字段:

Node(E e, Node<E> prev, Node<E> next) {this.item = e;this.prev = prev;this.next = next;
}
  • ​内存离散​​:节点存储在堆内存的不同位置,通过prevnext指针连接。这种结构导致节点的内存地址不连续,无法通过索引直接计算内存位置。
  • ​双向遍历​​:双向链表支持从头部或尾部高效遍历(通过firstlast指针直接访问头尾节点),但中间节点的访问仍需从头部或尾部开始遍历。

二、内存占用:数组紧凑 vs 链表冗余

1. ArrayList的内存开销

数组的内存占用主要由​​元素本身​​和​​数组元数据​​组成:

  • 元素存储:连续的内存空间,无额外指针开销(仅存储对象引用)。
  • 数组元数据:包括数组长度、对象头(Mark Word、类型指针等),但这些是所有数组共有的开销,与元素数量无关。
2. LinkedList的内存开销

链表的内存占用包含​​节点数据​​和​​指针开销​​:

  • 每个节点需存储prevnext两个指针(64位JVM下每个指针8字节,共16字节),以及元素本身的引用(4或8字节)。
  • 对象头开销:每个Node对象还有自己的对象头(约12字节,压缩指针下),导致内存浪费更严重。

​示例对比​​(以存储Integer对象为例,64位JVM启用压缩指针):

  • 存储1个Integer
    • ArrayList:数组元数据(16字节) + 元素引用(4字节)= 20字节(未算对象对齐)。
    • LinkedListNode对象头(12字节) + prev(4字节) + next(4字节) + element(4字节)= 24字节(未算对象对齐)。
  • 存储100万元素:
    • ArrayList:约100万 * 4字节(元素引用) + 数组元数据 ≈ 4MB(忽略扩容损耗)。
    • LinkedList:约100万 * 24字节(节点) ≈ 24MB(是ArrayList的6倍)。

​结论​​:存储大量小对象时,LinkedList的内存占用远高于ArrayList


三、核心操作性能:随机访问 vs 头尾插入

1. 查找操作:随机访问 vs 顺序遍历
  • ​ArrayList​​:通过索引访问元素时,直接计算内存地址,时间复杂度O(1)
    但如果通过contains(Object o)indexOf(Object o)查找元素(需遍历比较),时间复杂度是O(n)(与链表相同)。
  • ​LinkedList​​:无论查找哪个元素,都需要从firstlast开始遍历(最坏情况遍历整个链表),时间复杂度O(n)
2. 插入/删除操作:数组移动 vs 节点链接

插入/删除的核心差异在于是否需要移动元素(数组)或定位节点(链表)。

操作场景ArrayListLinkedList
​尾部插入(add(E e))​均摊O(1)(仅需判断是否扩容,无需移动元素)O(1)(直接操作last指针)
​头部插入(add(0, e))​O(n)(需将所有元素后移一位)O(1)(修改first指针和新节点的next
​中间插入(add(index, e))​O(n)(需将index到末尾的元素后移一位)O(n)(需先遍历找到index位置的节点,再修改前后指针)
​尾部删除(remove(size()-1))​O(1)(直接置空末尾元素,数组长度减一)O(1)(修改last指针)
​头部删除(remove(0))​O(n)(需将所有元素前移一位)O(1)(修改first指针)
​中间删除(remove(index))​O(n)(需移动index到末尾的元素)O(n)(需遍历找到节点)

​关键误区​​:
很多人认为LinkedList的任意位置插入都是O(1),这是错误的。虽然链表节点的链接操作是O(1),但​​定位插入位置需要遍历​​(除非已知前驱节点)。例如,add(index, e)的时间复杂度由node(index)方法的遍历时间决定——若index接近头部或尾部,遍历次数少;若index在中间,仍需O(n)时间。


四、线程安全与扩展实现

两者均​​非线程安全​​,多线程环境下可能出现数据不一致(如迭代时修改列表)。若需线程安全:

  • ​ArrayList​​:可通过Collections.synchronizedList(new ArrayList<>())包装,或使用CopyOnWriteArrayList(写时复制,适合读多写少场景)。
  • ​LinkedList​​:同样可通过Collections.synchronizedList包装,但更推荐使用ConcurrentLinkedQueue(无界非阻塞队列,适合高并发场景)。

五、实战选择:根据场景匹配特性

1. 优先选ArrayList的场景
  • ​随机访问频繁​​:如遍历、按索引获取元素(如缓存系统、需要快速响应的查询场景)。
  • ​数据量已知或可预估​​:初始化时指定容量,避免动态扩容的性能损耗。
  • ​内存敏感场景​​:存储大量小对象时,数组的紧凑内存更节省资源。
2. 优先选LinkedList的场景
  • ​头尾插入/删除频繁​​:如实现队列(addLast+removeFirst)或栈(addFirst+removeFirst)。
    (注:Java 6后ArrayDeque在头尾操作上的性能已优于LinkedList,且内存占用更低,更推荐作为队列/双端队列的选择。)
  • ​数据量小且动态变化大​​:小数据量时,链表的指针开销可忽略,而数组的扩容损耗可能更明显。
3. 避免踩坑
  • ​避免用LinkedList做随机访问​​:即使get(index)方法时间复杂度是O(n),实际性能远低于ArrayList
  • ​避免用ArrayList做高频中间插入​​:频繁移动元素会导致大量内存复制,性能下降明显。

六、性能测试:用数据说话

为了验证理论分析,我们编写一个简单的性能测试(JDK 17,64位系统):

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;public class ListPerformanceTest {private static final int SIZE = 100000;private static final int INSERT_TIMES = 1000;public static void main(String[] args) {// 测试随机访问性能List<Integer> arrayList = new ArrayList<>(SIZE);for (int i = 0; i < SIZE; i++) arrayList.add(i);long start = System.nanoTime();for (int i = 0; i < SIZE; i += 1000) arrayList.get(i);System.out.println("ArrayList随机访问耗时:" + (System.nanoTime() - start)/1e6 + "ms");List<Integer> linkedList = new LinkedList<>();for (int i = 0; i < SIZE; i++) linkedList.add(i);start = System.nanoTime();for (int i = 0; i < SIZE; i += 1000) linkedList.get(i);System.out.println("LinkedList随机访问耗时:" + (System.nanoTime() - start)/1e6 + "ms");// 测试中间插入性能start = System.nanoTime();for (int i = 0; i < INSERT_TIMES; i++) {arrayList.add(SIZE/2, i);}System.out.println("ArrayList中间插入耗时:" + (System.nanoTime() - start)/1e6 + "ms");start = System.nanoTime();for (int i = 0; i < INSERT_TIMES; i++) {linkedList.add(SIZE/2, i);}System.out.println("LinkedList中间插入耗时:" + (System.nanoTime() - start)/1e6 + "ms");}
}

​测试结果(示例)​​:

ArrayList随机访问耗时:0.2ms       // 几乎瞬间完成
LinkedList随机访问耗时:125.3ms   // 遍历耗时显著
ArrayList中间插入耗时:12.1ms     // 移动元素的开销
LinkedList中间插入耗时:156.7ms   // 遍历+链接的双重开销

​结论​​:随机访问和中间插入场景下,ArrayList的性能优势非常明显;而头尾插入场景中,两者性能接近(LinkedList略优,但实际开发中更推荐ArrayDeque)。


总结

ArrayListLinkedList的选择没有绝对答案,关键在于​​场景匹配​​:

  • 若需​​高频随机访问​​(如遍历、按索引操作),选ArrayList
  • 若需​​高频头尾插入/删除​​(且数据量不大),选LinkedList(或更优的ArrayDeque);
  • 内存敏感场景,优先ArrayList(紧凑存储更省内存);
  • 多线程环境,根据需求选择线程安全的扩展实现(如CopyOnWriteArrayListConcurrentLinkedQueue)。

理解底层原理后,结合具体业务场景的性能瓶颈(如访问频率、插入位置、数据量),才能做出最优选择。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914680.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914680.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python的慈善捐赠平台管理信息系统

前端开发框架:vue.js 数据库 mysql 版本不限 后端语言框架支持&#xff1a; 1 java(SSM/springboot)-idea/eclipse 2.NodejsVue.js -vscode 3.python(flask/django)–pycharm/vscode 4.php(thinkphp/laravel)-hbuilderx 数据库工具&#xff1a;Navicat/SQLyog等都可以 摘要 本文…

三十二、【核心功能改造】数据驱动:重构仪表盘与关键指标可视化

三十二、【核心功能改造】数据驱动:重构仪表盘与关键指标可视化 前言准备工作第一部分:后端实现 - 统计 API1. 创建 `DashboardStatsView`2. 注册统计 API 路由3. 后端初步测试第二部分:前端实现 - 重构仪表盘页面1. 创建 `api/dashboard.ts` API 服务2. 重构 `HomeView.vue…

神经网络与深度学习Python入门

一、神经网络基础 1. 神经元模型 在神经网络中&#xff0c;最基本的单元是神经元&#xff08;Neuron&#xff09;&#xff0c;也称为节点或单元。它模拟了生物神经系统中的神经元行为。一个典型的神经元模型包含多个输入&#xff08;x1,x2,…,xnx_1, x_2, \ldots, x_nx1​,x2​…

Android System WebView:Android生态的核心组件

在Android生态系统中&#xff0c;Android System WebView&#xff08;简称WebView&#xff09;扮演着至关重要的角色。它是Chrome浏览器的内核&#xff0c;为Android设备提供了强大的网页浏览和Web内容展示功能。无论是日常浏览网页、使用基于Web的应用程序&#xff0c;还是进行…

Element Plus和Ant Design Vue深度对比分析与选型指南

在 Vue3生态中&#xff0c;Element Plus和Ant Design Vue&#xff08;以下简称 AntD Vue&#xff09;是两款最主流的 UI 组件库。它们分别脱胎于 Element UI&#xff08;Vue 2 版本&#xff09;和 Ant Design&#xff08;React 生态&#xff09;&#xff0c;经过多年迭代已成为…

AJAX 开发中的注意点

关键词&#xff1a;AJAX、异步请求、前端开发、跨域、错误处理、安全、性能优化 ✅ 引言 在现代 Web 应用中&#xff0c;AJAX 是实现前后端数据交互的重要手段。然而&#xff0c;在实际开发过程中&#xff0c;如果不注意一些常见问题&#xff0c;可能会导致应用出现安全性漏洞…

类之间的纵向关系——继承

继承定义&#xff1a;被继承的类叫做基类&#xff08;父类&#xff09;&#xff0c;继承的类叫派生类&#xff08;子类&#xff09;&#xff0c;在派生类类名后面加&#xff1a; 继承方式 基类class CFather{}; class CSon:public CFather{};父类(基类)与子类(派生类)之间的关系…

bytetrack漏检补齐

bytetrack漏检补齐1.人流慢速运动&#xff0c;跟踪效果比较好&#xff0c;偶尔有漏检&#xff0c;跟踪可以自动补齐。2.快速运动&#xff0c;频繁遮挡&#xff0c;效果可能不好*如果漏检&#xff0c;倒着跟踪&#xff0c;把丢失的检测框拷贝出来&#xff0c;保留进行跟踪。有时…

安装Keycloak并启动服务(macOS)

前提&#xff1a;电脑已经安装Java 17 1、下载Keycloak 2、下载完后解压缩&#xff0c;使用文本编辑器修改配置文件&#xff08;keycloak/conf/keycloak.conf&#xff09; # Basic settings for running in production. Change accordingly before deploying the server. # …

汽车动力转向器落锤冲击试验台

落锤冲击试验台主要用于扣件减振量的测试&#xff0c;采用电动锚链提锤结构&#xff0c;控制精度高&#xff0c;定位准确。采用伺服电机减速机驱动&#xff0c;避免提锤加速和到位减速时的冲击&#xff0c;具有多重安全保护功能&#xff0c;防止二次冲击装置。主机框架采用上下…

Linux系统集群部署模块之Keepalived双机热备

目录 概述 一、keepalived安装 二、配置文件 三、 其他配置项说明 四、名词解释 五、高阶使用 1、介绍 2、keepalived主要作用 3、工作在三层、四层和七层原理 4、健康状态检测方式 4.1 HTTP服务状态检测 4.2 TCP端口状态检测&#xff08;使用TCP端口服务基本上都可…

TDengine 使用最佳实践(1)

目录 数据建模 单列模型 多列模型 分库分表 边界限制 资源规划 CPU 主频 CPU 核数 内存分类 内存计算 CPU 内存比例 磁盘 网络 下一篇 TDengine 使用最佳实践&#xff08;1&#xff09; 关于 TDengine TDengine 是一款专为物联网、工业互联网等场景设计并优化的大数据平台&am…

Java行为型模式---责任链模式

责任链模式基础概念责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为型设计模式&#xff0c;其核心思想是将请求的发送者和接收者解耦&#xff0c;使多个对象都有机会处理请求。这些对象连接成一条链&#xff0c;请求沿着链传递&#xff0c;直到有…

嵌入式学习笔记- 结构体名字被赋值时是整体内容赋值

结构体变量名被赋值时&#xff0c;‌不是赋值的地址&#xff0c;而是执行对整个结构体内容的复制&#xff08;值拷贝&#xff09;‌直接赋值是成员级复制‌ 当使用 s2 s1; 形式的赋值时&#xff08;其中 s1 和 s2 是同类型结构体变量&#xff09;&#xff0c;系统会‌逐成员复…

基于UDP/IP网络游戏加速高级拥塞控制算法(示意:一)

/* ███████╗ 基于UDP/IP网络游戏加速高级拥塞控制算法&#xff08;示意&#xff1a;一&#xff09; ███████╗ */#pragma once#include <iostream> #include <vector> #include <deque> #include <cmath> #include <algorithm> …

【YOLOv11-目标检测】06-模型部署(C++)

上一节课,我们学习了模型的预测。那么,如何用C++部署呢? 克隆项目 进入cmd,进入自己的项目文件夹,然后git clone项目: git clone https://github.com/Geekgineer/YOLOs-CPP 进入到YOLOs-CPP文件夹: 配置环境 ONNX Runtime 后续构建项目的时候,会自动下载,因此,我…

【第零章编辑器开发与拓展】

前言&#xff1a;对编辑器拓展与开发可以节省很多时间&#xff0c;提高开发效率&#xff0c;比如技能编辑器&#xff0c;关卡编辑器这种。当然这只是编辑器开发的一些典型应用&#xff0c;它能做不止这些。学习完这个之后&#xff0c;我们可以开发项目需要的工具。我本意在编辑…

使用 mongoimport 导入本地 JSON 文件到 MongoDB 及数据查看指南

在项目中&#xff0c;我们经常需要将本地 JSON 文件批量导入 MongoDB 数据库。本文以 Ubuntu 22.04 环境为例&#xff0c;详细记录了如何安装 mongoimport 工具、正确导入多个 JSON 文件&#xff0c;以及查看导入后的数据。一、环境介绍操作系统&#xff1a;Ubuntu 22.04.5 LTS…

新手向:Python数据处理Excel报表自动化生成与分析

Python实现Excel报表自动化系统全流程指南本文将详细介绍如何使用Python实现一个完整的Excel报表自动化系统&#xff0c;涵盖从数据清洗、分析到可视化报表生成的全流程。本教程面向Python初学者&#xff0c;通过实际案例讲解pandas和openpyxl库的核心用法。系统概述Excel报表自…

【第六节】docker可视化工具portainer安装

该文章参考了这篇文章https://zhuanlan.zhihu.com/p/27740131259portainer是一个基于网页的docker可视化管理工具&#xff0c;试想一下我们怎么登录路由器管理界面的&#xff0c;异曲同工。那么就需要在服务器的docker内安装portainer&#xff0c;然后在我们的开发机或者说工作…