欢迎光临
我们一直在努力

JAVA数据结构 DAY4-ArrayList

本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。

点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励! 

系列文章目录

JAVA初阶———已更完

JAVA数据结构 DAY1-集合和时空复杂度

JAVA数据结构 DAY2-包装类和泛型

JAVA数据结构 DAY3-List接口

JAVA数据结构 DAY4-ArrayList


目录

目录

系列文章目录

目录

前言

一、基础铺垫:线性表与顺序表

1. 线性表

2. 顺序表

二、ArrayList 核心解析

1. ArrayList 类结构与特性

2. 构造方法

3. 常用操作

4. 遍历方式

(1)for 循环 + 下标(最常用)

(2)foreach 循环(简洁)

(3)迭代器(Iterator)

三、深度剖析:ArrayList 扩容机制

1. 核心源码解析

2. 扩容流程总结

3. 扩容相关问题

四、实战案例:ArrayList 的实际应用

1. 扑克牌游戏(洗牌与发牌)

实现步骤:

代码实现:

核心知识点:

2. 杨辉三角(基于 ArrayList 构建)

代码实现:

输出结果:

五、ArrayList 的局限性与优化思考

1. 核心局限性

2. 优化思路

六、总结

总结


前言

小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!

在 Java 开发中,ArrayList 是使用频率极高的集合类。它基于动态数组实现,兼顾了随机访问的高效性与灵活扩容的特性,成为存储和操作数据的首选工具之一。本文将从线性表基础概念出发,全面剖析 ArrayList 的底层原理、核心用法、扩容机制,并通过实战案例加深理解,最后探讨其局限性与优化思路。

一、基础铺垫:线性表与顺序表

要理解 ArrayList,首先需要明确两个核心概念:线性表和顺序表。

1. 线性表

线性表(linear list)是 n 个具有相同特性的数据元素的有限序列,是一种逻辑结构上呈线性(连续一条直线)的数据结构。它在实际开发中应用广泛,常见的线性表包括顺序表、链表、栈、队列等。

关键特性:

  • 逻辑结构连续:元素之间存在 “一对一” 的相邻关系
  • 物理结构灵活:存储时不一定连续,通常以数组(顺序存储)或链式结构(离散存储)实现

2. 顺序表

顺序表是线性表的一种具体实现,它使用一段物理地址连续的存储单元依次存储数据元素,本质上是基于数组的线性结构。我们可以通过自定义顺序表理解其核心设计:

public class SeqList {
private int[] array; // 底层存储数组
private int size; // 有效元素个数

// 构造方法:默认构造、指定初始容量构造
SeqList() {}
SeqList(int initcapacity) {}

// 核心操作:增删查改
public void add(int data) {} // 尾插元素
public void add(int pos, int data) {} // 指定位置插入
public boolean contains(int toFind) { return true; } // 包含判断
public int indexOf(int toFind) { return -1; } // 查找元素位置
public int get(int pos) { return -1; } // 获取指定位置元素
public void set(int pos, int value) {} // 修改指定位置元素
public void remove(int toRemove) {} // 删除元素
public int size() { return 0; } // 获取长度
public void clear() {} // 清空表
public void display() {} // 打印(测试用)
}

顺序表的核心优势是支持随机访问(通过下标直接定位元素),但短板也很明显:插入删除需要移动后续元素,效率较低。

二、ArrayList 核心解析

ArrayList 是 Java 集合框架中顺序表的实现类,在日常开发中几乎无处不在。下面从类结构、构造方法、常用操作、遍历方式四个维度全面解析。

1. ArrayList 类结构与特性

ArrayList 的类继承关系如下:Iterable <- Collection <- AbstractCollection <- List <- AbstractList <- ArrayList

同时实现了三个关键接口,赋予其特殊能力:

  • RandomAccess:支持随机访问,可通过下标快速获取元素
  • Cloneable:支持克隆,可通过 clone () 方法创建副本
  • Serializable:支持序列化,可在网络传输或文件存储中使用

核心特性总结:

  • 泛型实现:使用时需指定元素类型(如List<Integer>),避免类型转换错误
  • 非线程安全:单线程环境使用,多线程需选择 Vector 或 CopyOnWriteArrayList
  • 动态扩容:底层是连续空间,容量不足时自动扩容
  • 基于数组:本质是动态数组,继承了数组随机访问的优势

2. 构造方法

ArrayList 提供三种构造方式,满足不同使用场景:

构造方法说明示例
ArrayList() 无参构造,默认初始容量 10 List<Integer> list = new ArrayList<>();
ArrayList(int initialCapacity) 指定初始容量 List<Integer> list = new ArrayList<>(20);
ArrayList(Collection<? extends E> c) 基于其他 Collection 构建 ArrayList<Integer> list = new ArrayList<>(otherList);

注意事项:

  • 推荐使用泛型指定元素类型,避免使用无泛型的原始类型(如List list = new ArrayList()),否则可能存储任意类型元素,引发类型转换异常
  • 指定初始容量适用于已知数据量的场景,可减少扩容次数,提升性能

3. 常用操作

ArrayList 提供了丰富的方法用于数据操作,核心常用方法如下:

方法功能描述注意事项
boolean add(E e) 尾插元素 e 成功返回 true,触发扩容机制
void add(int index, E element) 在 index 位置插入元素 index 需在 [0, size] 之间,后续元素后移
boolean addAll(Collection<? extends E> c) 尾插集合 c 中所有元素 批量添加,减少扩容次数
E remove(int index) 删除 index 位置元素 返回被删除元素,后续元素前移
boolean remove(Object o) 删除第一个遇到的元素 o 需重写元素的 equals 方法才能正确匹配
E get(int index) 获取 index 位置元素 index 需在 [0, size) 之间,否则抛下标越界异常
E set(int index, E element) 修改 index 位置元素为 element 返回原元素,index 需合法
void clear() 清空所有元素 仅置空元素引用,不释放数组空间
boolean contains(Object o) 判断是否包含元素 o 依赖 equals 方法判断
int indexOf(Object o) 返回第一个 o 的下标 未找到返回 – 1
int lastIndexOf(Object o) 返回最后一个 o 的下标 未找到返回 – 1
List<E> subList(int fromIndex, int toIndex) 截取 [fromIndex, toIndex) 区间元素 与原集合共用底层数组,修改会相互影响

示例代码:

public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 尾插元素
list.add("JavaSE");
list.add("JavaWeb");
list.add("JavaEE");
System.out.println(list); // [JavaSE, JavaWeb, JavaEE]

// 获取有效元素个数
System.out.println(list.size()); // 3

// 获取和修改元素
System.out.println(list.get(1)); // JavaWeb
list.set(1, "JavaWEB");
System.out.println(list.get(1)); // JavaWEB

// 指定位置插入
list.add(1, "Java数据结构");
System.out.println(list); // [JavaSE, Java数据结构, JavaWEB, JavaEE]

// 删除元素
list.remove("JavaEE");
System.out.println(list); // [JavaSE, Java数据结构, JavaWEB]
list.remove(list.size() – 1);
System.out.println(list); // [JavaSE, Java数据结构]

// 包含判断与查找
System.out.println(list.contains("JavaSE")); // true
list.add("JavaSE");
System.out.println(list.indexOf("JavaSE")); // 0
System.out.println(list.lastIndexOf("JavaSE")); // 2

// 截取子列表
List<String> subList = list.subList(0, 2);
System.out.println(subList); // [JavaSE, Java数据结构]

// 清空
list.clear();
System.out.println(list.size()); // 0
}

4. 遍历方式

ArrayList 支持三种遍历方式,各有适用场景:

(1)for 循环 + 下标(最常用)

利用数组随机访问特性,效率最高,适用于需要下标操作的场景:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}

(2)foreach 循环(简洁)

语法简洁,无需关注下标,适用于仅遍历元素的场景:

for (Integer num : list) {
System.out.print(num + " ");
}

(3)迭代器(Iterator)

集合通用遍历方式,支持遍历过程中删除元素(需使用迭代器的 remove 方法):

Iterator<Integer> it = list.listIterator();
while (it.hasNext()) {
System.out.print(it.next() + " ");
}

注意:遍历过程中避免使用集合的 remove 方法,可能引发 ConcurrentModificationException 异常,如需删除应使用迭代器的 remove 方法。

三、深度剖析:ArrayList 扩容机制

ArrayList 的 “动态” 特性核心在于扩容机制 —— 当底层数组容量不足时,自动申请更大的空间,拷贝原有数据,实现容量动态增长。

1. 核心源码解析

先看 ArrayList 扩容相关的核心源码(JDK8):

// 底层存储元素的数组
transient Object[] elementData;
// 默认空数组(无参构造初始化用)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 最大数组容量(避免OOM)
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE – 8;

// 尾插元素时触发扩容检查
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 检查是否需要扩容
elementData[size++] = e;
return true;
}

// 计算最小所需容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 无参构造创建的空数组,首次扩容时使用默认容量10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

// 确认是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 结构修改计数器(用于快速失败机制)
// 所需容量超过当前数组长度,触发扩容
if (minCapacity – elementData.length > 0) {
grow(minCapacity);
}
}

// 扩容核心方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length; // 旧容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容(右移1位等价于除以2)

// 若1.5倍扩容后仍不足,直接使用所需容量
if (newCapacity – minCapacity < 0) {
newCapacity = minCapacity;
}

// 若容量超过最大值,使用Integer.MAX_VALUE或MAX_ARRAY_SIZE
if (newCapacity – MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}

// 拷贝原有数据到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}

// 处理超大容量需求
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) {
throw new OutOfMemoryError(); // 内存溢出
}
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

2. 扩容流程总结

  • 扩容触发条件:当添加元素后,有效元素个数(size)超过当前数组容量(elementData.length)时触发扩容
  • 容量计算规则:
    • 无参构造的 ArrayList,首次添加元素时,容量从 0 扩容到默认值 10
    • 后续扩容默认按 1.5 倍(oldCapacity + oldCapacity/2)增长
    • 若 1.5 倍扩容后仍无法满足需求(如批量添加大量元素),则直接扩容到所需最小容量
    • 最大容量限制为 Integer.MAX_VALUE(特殊情况),避免数组容量过大导致 OOM
  • 扩容核心操作:通过Arrays.copyOf方法创建新数组,将原有数据拷贝到新数组,释放旧数组空间
  • 3. 扩容相关问题

    问题:以下代码是否有缺陷?

    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
    list.add(i);
    }

    分析:无参构造的 ArrayList 初始容量为 10,添加第 11 个元素时触发第一次扩容(10→15),添加第 16 个元素时触发第二次扩容(15→22)…… 直到容量满足 100 个元素需求。多次扩容会产生多次数组拷贝,存在性能开销。

    优化方案:已知数据量时,使用指定初始容量的构造方法(new ArrayList<>(100)),避免多次扩容。

    四、实战案例:ArrayList 的实际应用

    理论结合实践,通过两个经典案例掌握 ArrayList 的使用场景。

    1. 扑克牌游戏(洗牌与发牌)

    需求:创建一副扑克牌,实现洗牌功能,然后分给 3 个人,每人 5 张牌。

    实现步骤:
  • 定义扑克牌类(花色 + 牌面值)
  • 创建一副完整的扑克牌(52 张,不含大小王)
  • 实现洗牌算法(随机交换元素)
  • 发牌并展示结果
  • 代码实现:

    // 扑克牌类
    class Card {
    public int rank; // 牌面值(1-13)
    public String suit; // 花色

    @Override
    public String toString() {
    return String.format("[%s %d]", suit, rank);
    }
    }

    public class CardDemo {
    // 四种花色
    public static final String[] SUITS = {"♠", "♥", "♣", "♦"};

    // 购买一副牌(52张)
    private static List<Card> buyDeck() {
    List<Card> deck = new ArrayList<>(52);
    for (int i = 0; i < 4; i++) { // 遍历花色
    for (int j = 1; j <= 13; j++) { // 遍历牌面值
    Card card = new Card();
    card.suit = SUITS[i];
    card.rank = j;
    deck.add(card);
    }
    }
    return deck;
    }

    // 交换元素(洗牌核心)
    private static void swap(List<Card> deck, int i, int j) {
    Card temp = deck.get(i);
    deck.set(i, deck.get(j));
    deck.set(j, temp);
    }

    // 洗牌算法(从后往前随机交换)
    private static void shuffle(List<Card> deck) {
    Random random = new Random();
    for (int i = deck.size() – 1; i > 0; i–) {
    int randomIndex = random.nextInt(i); // 生成[0, i)之间的随机数
    swap(deck, i, randomIndex);
    }
    }

    public static void main(String[] args) {
    // 1. 买牌
    List<Card> deck = buyDeck();
    System.out.println("刚买回来的牌:");
    System.out.println(deck);

    // 2. 洗牌
    shuffle(deck);
    System.out.println("\\n洗过的牌:");
    System.out.println(deck);

    // 3. 发牌(3人,每人5张)
    List<List<Card>> hands = new ArrayList<>();
    hands.add(new ArrayList<>()); // A玩家
    hands.add(new ArrayList<>()); // B玩家
    hands.add(new ArrayList<>()); // C玩家

    for (int i = 0; i < 5; i++) { // 发5轮
    for (int j = 0; j < 3; j++) { // 分给3个玩家
    hands.get(j).add(deck.remove(0)); // 移除第一张牌分给玩家
    }
    }

    // 4. 展示结果
    System.out.println("\\n剩余的牌:");
    System.out.println(deck);
    System.out.println("\\nA手中的牌:");
    System.out.println(hands.get(0));
    System.out.println("B手中的牌:");
    System.out.println(hands.get(1));
    System.out.println("C手中的牌:");
    System.out.println(hands.get(2));
    }
    }

    核心知识点:
    • ArrayList 的批量添加、删除、修改操作
    • 洗牌算法的实现(随机交换元素)
    • 嵌套 List 的使用(存储多个玩家的牌)

    2. 杨辉三角(基于 ArrayList 构建)

    杨辉三角的特点:

    • 第 n 行有 n 个元素
    • 每行的首尾元素都是 1
    • 中间元素 = 上一行相邻两个元素之和
    代码实现:

    public class YangHuiTriangle {
    public static List<List<Integer>> generate(int numRows) {
    List<List<Integer>> triangle = new ArrayList<>();

    // 第一行(只有1)
    triangle.add(new ArrayList<>());
    triangle.get(0).add(1);

    // 生成第2行到第numRows行
    for (int i = 1; i < numRows; i++) {
    List<Integer> currentRow = new ArrayList<>();
    List<Integer> prevRow = triangle.get(i – 1);

    // 每行第一个元素为1
    currentRow.add(1);

    // 中间元素:上一行相邻两个元素之和
    for (int j = 1; j < i; j++) {
    currentRow.add(prevRow.get(j – 1) + prevRow.get(j));
    }

    // 每行最后一个元素为1
    currentRow.add(1);

    triangle.add(currentRow);
    }

    return triangle;
    }

    public static void main(String[] args) {
    List<List<Integer>> result = generate(5);
    // 打印杨辉三角
    for (List<Integer> row : result) {
    System.out.println(row);
    }
    }
    }

    输出结果:

    [1]
    [1, 1]
    [1, 2, 1]
    [1, 3, 3, 1]
    [1, 4, 6, 4, 1]

    五、ArrayList 的局限性与优化思考

    ArrayList 虽好用,但并非万能,其底层实现决定了它存在以下局限性:

    1. 核心局限性

    • 插入删除效率低:任意位置插入 / 删除元素时,需要移动后续元素,时间复杂度为 O (N)
    • 扩容开销大:扩容时需要申请新空间、拷贝数据、释放旧空间,频繁扩容会影响性能
    • 空间浪费:扩容通常按 1.5 倍增长,可能导致部分空间闲置(如容量 200 但仅存储 105 个元素,浪费 95 个空间)
    • 线程不安全:多线程环境下并发修改可能导致数据不一致或 ConcurrentModificationException 异常

    2. 优化思路

    针对以上问题,可通过以下方式优化:

    • 已知数据量时,初始化指定初始容量,减少扩容次数
    • 批量添加元素时,使用addAll方法,避免多次触发扩容
    • 频繁插入删除操作时,改用 LinkedList(基于链表实现,插入删除效率 O (1))
    • 多线程环境下,使用 Vector(线程安全,效率较低)或 CopyOnWriteArrayList(读写分离,适合读多写少场景)
    • 若空间紧张,可手动调用trimToSize()方法,将数组容量缩减为当前有效元素个数(注意:后续添加元素会再次触发扩容)

    六、总结

    ArrayList 是 Java 中最基础、最常用的集合类,其本质是动态扩容的顺序表,核心优势是随机访问效率高(O (1)),适用于读多写少、需要频繁访问元素的场景。

    本文从基础概念出发,依次讲解了线性表与顺序表的定义、ArrayList 的类结构与核心用法、扩容机制的底层实现,通过扑克牌和杨辉三角两个实战案例巩固应用,最后分析了其局限性与优化方向。

    掌握 ArrayList 的核心原理,不仅能在开发中灵活运用,更能理解数据结构中 “顺序存储” 的设计思想,为后续学习链表、栈、队列等数据结构打下基础。在实际开发中,应根据业务场景选择合适的集合类 —— 没有最好的集合,只有最适合的集合!


    总结

    以上就是今天要讲的内容,本文简单记录了java数据结构,仅作为一份简单的笔记使用,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!

    赞(0)
    未经允许不得转载:171主机测评 » JAVA数据结构 DAY4-ArrayList
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址