在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接口。文档中明确指出了它的几个重要特性:
-
泛型实现 :
ArrayList以泛型方式实现,使用时必须先实例化 -
接口实现:
-
实现
RandomAccess接口,支持随机访问 -
实现
Cloneable接口,可以克隆 -
实现
Serializable接口,支持序列化
-
-
线程安全 :
ArrayList不是 线程安全的,在单线程下使用,多线程中可选择Vector或CopyOnWriteArrayList -
底层结构 :
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的扩容流程:
-
容量检查 :每次添加元素时,调用
ensureCapacityInternal(size + 1)检查是否需要扩容 -
容量计算:
-
如果当前数组是默认空数组,返回
Math.max(DEFAULT_CAPACITY(10), minCapacity) -
否则返回所需最小容量
-
-
显式扩容检查 :如果需要的最小容量大于当前数组长度,则调用
grow()方法扩容 -
扩容计算:
-
默认按1.5倍 扩容:
newCapacity = oldCapacity + (oldCapacity >> 1) -
如果用户所需大小超过1.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的优势
-
随机访问高效 :支持
O(1)时间复杂度的随机访问 -
尾部操作高效 :在尾部添加元素平均时间复杂度为
O(1) -
缓存友好:连续内存存储,缓存命中率高
-
接口丰富:提供了完整的List接口实现
5.2 ArrayList的缺点
文档中明确指出了ArrayList的几个问题:
-
插入删除效率低 :在任意位置插入或删除元素时,需要将后续元素整体移动,时间复杂度为
O(N) -
扩容有开销:增容需要申请新空间、拷贝数据、释放旧空间,消耗较大
-
空间浪费:增容通常按1.5倍增长,可能造成空间浪费
例如:当前容量为100,满后增容到200,再插入5个数据后,就浪费了95个空间。
5.3 解决方案思考
文档最后提出了一个思考题:如何解决这些问题?
可能的解决方案:
-
频繁插入删除 :考虑使用
LinkedList -
空间浪费 :可以使用
trimToSize()方法裁剪容量 -
预分配空间:如果能预估数据量,创建时指定合适初始容量
-
替代方案:根据场景选择合适数据结构
-
需要快速查找:
ArrayList -
需要频繁插入删除:
LinkedList -
需要线程安全:
CopyOnWriteArrayList或Vector -
需要去重:
HashSet
-
六、总结
ArrayList作为Java集合框架的核心组件,理解其内部机制对于编写高效Java程序至关重要。通过本文的学习,你应该掌握:
-
理论基础:线性表和顺序表的概念
-
基本使用:构造、增删改查、遍历
-
核心原理:扩容机制和实现细节
-
实战应用:洗牌算法等实际场景
-
选型思考:根据需求选择合适的数据结构
记住,没有最好的数据结构,只有最适合场景的数据结构。ArrayList在随机访问和遍历场景下表现出色,但在频繁插入删除时可能不是最佳选择。理解每种数据结构的特性,才能在实际编程中做出明智的选择。
最佳实践建议:
-
如果能预估数据量,创建时指定初始容量避免频繁扩容
-
使用
List<E>接口类型引用,提高代码灵活性 -
避免在列表中间频繁插入删除大量数据
-
多线程环境使用合适的线程安全替代方案
希望这篇博客能帮助你深入理解ArrayList,在未来的Java编程中更加得心应手!