深入浅出ArrayList:从线性表到洗牌算法,掌握Java集合核心

在Java编程中,数据结构是构建高效程序的基石,而ArrayList作为最常用的集合类之一,理解其背后的原理和用法至关重要。本篇博客将带你全面了解ArrayList,从基本概念到实际应用,一步步掌握这个强大的动态数组实现。

一、数据结构基础:从线性表到顺序表

1.1 什么是线性表?

在开始学习ArrayList之前,我们需要理解其理论基础------线性表。线性表(Linear List)是n个具有相同特性的数据元素的有限序列,是实际编程中最常用的数据结构之一。常见的线性表包括:

  • 顺序表(如数组)

  • 链表

  • 队列

线性表在逻辑上 是连续的线性结构,但在物理存储上不一定连续。它可以通过数组(顺序存储)或链式结构(链式存储)来实现。

1.2 顺序表:连续存储的实现

顺序表是线性表的一种实现方式,它使用一段物理地址连续的存储单元 依次存储数据元素。简单来说,顺序表就是用数组实现的线性表,在数组上完成数据的增删查改操作。

顺序表的基本结构通常包含:

复制代码
public class SeqList {
    private int[] array;  // 存储数据的数组
    private int size;     // 当前有效元素个数
    
    // 构造方法
    SeqList(int initCapacity) {
        array = new int[initCapacity];
        size = 0;
    }
}

二、ArrayList:Java中的动态顺序表

2.1 ArrayList简介

ArrayList是Java集合框架中一个普通的类,实现了List接口。文档中明确指出了它的几个重要特性:

  1. 泛型实现ArrayList以泛型方式实现,使用时必须先实例化

  2. 接口实现

    • 实现RandomAccess接口,支持随机访问

    • 实现Cloneable接口,可以克隆

    • 实现Serializable接口,支持序列化

  3. 线程安全ArrayList不是 线程安全的,在单线程下使用,多线程中可选择VectorCopyOnWriteArrayList

  4. 底层结构ArrayList底层是一段连续的空间,可以动态扩容,是一个动态类型的顺序表

2.2 ArrayList的构造方法

文档中介绍了三种主要的构造方法:

方法 说明
ArrayList() 无参构造,创建空列表
ArrayList(Collection<? extends E> c) 利用其他集合构建ArrayList
ArrayList(int initialCapacity) 指定顺序表初始容量

使用示例

复制代码
// 推荐写法:使用List接口引用
List<Integer> list1 = new ArrayList<>();       // 空列表
List<Integer> list2 = new ArrayList<>(10);     // 初始容量10
list2.add(1);
list2.add(2);
list2.add(3);

// 利用已有列表构造新列表
ArrayList<Integer> list3 = new ArrayList<>(list2);

// 避免使用原始类型(会有类型安全问题)
List list4 = new ArrayList();  // 不推荐:可以存放任意类型
list4.add("111");
list4.add(100);  // 混合类型,使用时需要强制转换,容易出错

2.3 ArrayList的常用操作

文档中列出了ArrayList的核心操作方法:

方法 功能说明
boolean add(E e) 尾部插入元素e
void add(int index, E element) 在index位置插入元素
E remove(int index) 删除index位置元素
boolean remove(Object o) 删除第一个匹配的元素o
E get(int index) 获取下标index位置的元素
E set(int index, E element) 设置下标index位置的元素
boolean contains(Object o) 判断元素o是否在线性表中
int indexOf(Object o) 返回第一个o的下标
List<E> subList(int from, int to) 截取子列表

实战示例

复制代码
List<String> list = new ArrayList<>();
list.add("JavaSE");
list.add("JavaWeb");
list.add("JavaEE");
list.add("JVM");
list.add("测试课程");

System.out.println(list);                    // 输出整个列表
System.out.println(list.size());             // 获取有效元素个数:5
System.out.println(list.get(1));             // 获取索引1的元素:"JavaWeb"

list.set(1, "JavaWEB");                      // 设置索引1的元素
list.add(1, "Java数据结构");                  // 在索引1处插入元素
list.remove("JVM");                          // 删除指定元素
list.remove(list.size() - 1);                // 删除最后一个元素

if (list.contains("测试课程")) {               // 检查是否包含
    list.add("测试课程");
}

// 查找元素位置
list.add("JavaSE");
System.out.println(list.indexOf("JavaSE"));   // 0
System.out.println(list.lastIndexOf("JavaSE")); // 5

// 截取子列表
List<String> subList = list.subList(0, 4);

2.4 ArrayList的遍历方式

文档展示了三种遍历ArrayList的方法:

复制代码
List<Integer> list = new ArrayList<>();
list.add(1); list.add(2); list.add(3); list.add(4); list.add(5);

// 1. for循环+下标(最常用)
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<Integer> it = list.iterator();
while (it.hasNext()) {
    System.out.print(it.next() + " ");
}

注意ArrayList最常用的遍历方式是for循环+下标以及foreach循环。

三、ArrayList的扩容机制

3.1 为什么需要扩容?

考虑以下代码是否有问题:

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

这段代码完全正确!因为ArrayList是一个动态类型顺序表,在插入元素时会自动扩容。

3.2 扩容机制详解

从文档中的源码分析可以看出ArrayList的扩容流程:

  1. 容量检查 :每次添加元素时,调用ensureCapacityInternal(size + 1)检查是否需要扩容

  2. 容量计算

    • 如果当前数组是默认空数组,返回Math.max(DEFAULT_CAPACITY(10), minCapacity)

    • 否则返回所需最小容量

  3. 显式扩容检查 :如果需要的最小容量大于当前数组长度,则调用grow()方法扩容

  4. 扩容计算

    • 默认按1.5倍 扩容:newCapacity = oldCapacity + (oldCapacity >> 1)

    • 如果用户所需大小超过1.5倍,按用户所需大小扩容

    • 检查是否超过最大数组大小限制

  5. 执行扩容 :使用Arrays.copyOf()复制到新数组

扩容源码关键逻辑

复制代码
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);  // 1.5倍扩容
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;  // 如果1.5倍还不够,使用所需容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);  // 处理大容量情况
    elementData = Arrays.copyOf(elementData, newCapacity);
}

四、ArrayList实战应用

4.1 扑克牌洗牌算法

文档提供了一个完整的扑克牌示例,展示了ArrayList在实际问题中的应用:

复制代码
// 1. 定义扑克牌类
class Card {
    public int rank;     // 牌面值
    public String suit;  // 花色
    
    @Override
    public String toString() {
        return String.format("[%s%d]", suit, rank);
    }
}

// 2. 买一副牌
public class CardDemo {
    public static final String[] SUITS = {"♠", "♥", "♣", "♦"};
    
    private static List<Card> buyDeck() {
        List<Card> deck = new ArrayList<>(52);
        for (int i = 0; i < 4; i++) {          // 4种花色
            for (int j = 1; j <= 13; j++) {    // 13个点数
                Card card = new Card();
                card.suit = SUITS[i];
                card.rank = j;
                deck.add(card);
            }
        }
        return deck;
    }
    
    // 3. 洗牌算法
    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(20190905);  // 固定种子,便于测试
        for (int i = deck.size() - 1; i > 0; i--) {
            int r = random.nextInt(i);
            swap(deck, i, r);
        }
    }
    
    // 4. 发牌
    public static void main(String[] args) {
        List<Card> deck = buyDeck();
        System.out.println("刚买回来的牌:" + deck);
        
        shuffle(deck);
        System.out.println("洗过的牌:" + deck);
        
        // 三人轮流抓5张牌
        List<List<Card>> hands = new ArrayList<>();
        hands.add(new ArrayList<>());
        hands.add(new ArrayList<>());
        hands.add(new ArrayList<>());
        
        for (int i = 0; i < 5; i++) {
            for (int j = 0; j < 3; j++) {
                hands.get(j).add(deck.remove(0));  // 从牌堆顶部取牌
            }
        }
        
        System.out.println("A手中的牌:" + hands.get(0));
        System.out.println("B手中的牌:" + hands.get(1));
        System.out.println("C手中的牌:" + hands.get(2));
        System.out.println("剩余的牌:" + deck);
    }
}

4.2 杨辉三角

这是一个经典的二维结构问题,可以使用ArrayList的列表嵌套来实现:

复制代码
List<List<Integer>> triangle = new ArrayList<>();
// 每一行都是一个ArrayList<Integer>

五、ArrayList的优缺点与思考

5.1 ArrayList的优势

  1. 随机访问高效 :支持O(1)时间复杂度的随机访问

  2. 尾部操作高效 :在尾部添加元素平均时间复杂度为O(1)

  3. 缓存友好:连续内存存储,缓存命中率高

  4. 接口丰富:提供了完整的List接口实现

5.2 ArrayList的缺点

文档中明确指出了ArrayList的几个问题:

  1. 插入删除效率低 :在任意位置插入或删除元素时,需要将后续元素整体移动,时间复杂度为O(N)

  2. 扩容有开销:增容需要申请新空间、拷贝数据、释放旧空间,消耗较大

  3. 空间浪费:增容通常按1.5倍增长,可能造成空间浪费

例如:当前容量为100,满后增容到200,再插入5个数据后,就浪费了95个空间。

5.3 解决方案思考

文档最后提出了一个思考题:如何解决这些问题?

可能的解决方案

  1. 频繁插入删除 :考虑使用LinkedList

  2. 空间浪费 :可以使用trimToSize()方法裁剪容量

  3. 预分配空间:如果能预估数据量,创建时指定合适初始容量

  4. 替代方案:根据场景选择合适数据结构

    • 需要快速查找:ArrayList

    • 需要频繁插入删除:LinkedList

    • 需要线程安全:CopyOnWriteArrayListVector

    • 需要去重:HashSet

六、总结

ArrayList作为Java集合框架的核心组件,理解其内部机制对于编写高效Java程序至关重要。通过本文的学习,你应该掌握:

  1. 理论基础:线性表和顺序表的概念

  2. 基本使用:构造、增删改查、遍历

  3. 核心原理:扩容机制和实现细节

  4. 实战应用:洗牌算法等实际场景

  5. 选型思考:根据需求选择合适的数据结构

记住,没有最好的数据结构,只有最适合场景的数据结构。ArrayList在随机访问和遍历场景下表现出色,但在频繁插入删除时可能不是最佳选择。理解每种数据结构的特性,才能在实际编程中做出明智的选择。

最佳实践建议

  • 如果能预估数据量,创建时指定初始容量避免频繁扩容

  • 使用List<E>接口类型引用,提高代码灵活性

  • 避免在列表中间频繁插入删除大量数据

  • 多线程环境使用合适的线程安全替代方案

希望这篇博客能帮助你深入理解ArrayList,在未来的Java编程中更加得心应手!

相关推荐
sunfdf3 小时前
使用免费工具在 Windows 11/10/8/7 中扩展 C 盘的 3 种方法
windows
月走乂山4 小时前
Trae CLI 全局配置 - Windows PATH 配置
windows
l1t4 小时前
DeepSeek辅助编写的dmp转schema和csv文件c语言程序
c语言·开发语言·windows
qq_429499574 小时前
更新SD卡文件列表到tablew中的list
windows
雨浓YN4 小时前
OPC DA 通讯开发笔记
windows·笔记
劳埃德福杰5 小时前
Windows系统卸载Edge浏览器
前端·windows·edge
wdfk_prog5 小时前
解决 Linux 使用符号链接的 Git 仓库在 Windows 下无法创建符号链接的问题
linux·windows·git
kcuwu.19 小时前
Python进阶:生成器与协程,高效并发编程的核心实践
windows·python·php
2401_8357925420 小时前
FastAPI 速通
windows·python·fastapi