深入源码解析ArrayList:探秘Java动态数组的机制与性能
一、 简介ArrayList
1.1 介绍ArrayList的基本概念和作用
在Java中,ArrayList是一个实现了List接口的动态数组。它可以根据需要自动增加大小,因此可以存储任意数量的元素。
- 基本概念:
- ArrayList是Java中常用的集合类之一,它可以存储对象,并且可以根据索引访问和操作这些对象。
- ArrayList是基于数组实现的,但是它具有动态扩展的能力,因此可以动态地增加和减少元素的数量。
- 作用:
- 存储数据:ArrayList可以用来存储各种类型的数据,包括基本类型和对象类型。
- 动态扩展:由于ArrayList的大小是动态的,因此它非常适合需要动态增加和减少元素的场景。
- 方便操作:ArrayList提供了丰富的方法来操作元素,比如添加、删除、查找等,使得对集合的操作变得非常便利。
总之,ArrayList在Java中是非常常用的数据结构,它提供了动态存储数据的能力,以及丰富的操作方法,非常适合在开发中使用。
1.2 与数组的区别和优势
ArrayList是一种动态数组,它是基于数组实现的,但具有动态扩展和收缩的能力。与普通数组相比,ArrayList具有以下区别和优势:
- 大小动态性:ArrayList的大小是动态的,可以根据需要动态扩展和收缩。而普通数组的大小是固定的,一旦创建就无法改变。
- 自动扩展:当ArrayList中的元素数量超过当前容量时,ArrayList会自动进行扩展,而普通数组需要手动重新分配内存并复制数据。
- 插入和删除元素效率高:ArrayList支持在任意位置插入和删除元素,而普通数组在插入和删除元素时需要移动其他元素。
- 内置方法和功能:ArrayList提供了许多便捷的方法和功能,如添加、删除、查找等操作,使其更易于使用和操作。
总的来说,ArrayList相对于普通数组来说更加灵活、便捷,并且具有更高的操作效率。因此,在大多数情况下,使用ArrayList比使用普通数组更加方便和实用。
二、 内部实现
2.1 数据结构:动态数组
在Java中,ArrayList是一个动态数组实现的类,它是基于数组实现的动态数组,可以自动扩容。下面是ArrayList的动态数组原理:
- 内部数组:ArrayList内部使用一个数组来存储元素。当创建一个ArrayList时,会初始化一个初始容量的数组。
- 自动扩容:当向ArrayList中添加元素时,如果当前数组已满,ArrayList会创建一个新的更大容量的数组,并将原数组中的元素复制到新数组中,然后将新元素添加到新数组中。
- 扩容策略:ArrayList的扩容策略是在每次扩容时将当前容量扩大为原来的
1.5倍
,这种策略既能够保证空间利用率,又能够减少因频繁扩容而带来的性能开销。 - 随机访问:由于ArrayList内部基于数组实现,因此支持随机访问,可以通过索引直接访问数组中的元素,时间复杂度为
O(1)
。
总的来说,ArrayList通过动态扩容的方式,利用数组实现了一个动态数组,提供了高效的随机访问和动态增删元素的功能。
2.2 添加元素:add()方法的实现原理
在Java中,ArrayList的add()
方法用于向ArrayList中添加元素。
其实现原理如下:
- 在调用add()方法时,先检查当前ArrayList的大小和容量(即存储空间是否足够)。
- 如果当前容量不够,就进行扩容操作。一般情况下,会创建一个新的数组,将原数组中的元素复制到新数组中,并且为新数组分配更大的存储空间。
- 然后将要添加的元素放入ArrayList的内部数组中,并更新ArrayList的大小。
- 如果添加成功,则返回
true
,如果添加失败,则返回false
。
总的来说,ArrayList的add()方法实现原理就是对内部数组的扩容和元素的添加操作。
2.3 扩容机制:ensureCapacity()方法的实现原理
在使用ArrayList时,如果我们预先知道将要插入的元素数量,可以使用ensureCapacity()
方法来预先分配内部数组大小。调用了一个名为ensureCapacityInternal()
的私有方法。这个方法首先会判断当前内部数组是否已经足够大来容纳新增的元素,如果不够大,则会进行扩容:
- 如果当前内部数组为空,则直接扩容到指定容量大小。
- 否则,计算出新数组的容量大小,这个容量大小取决于原始数组的大小和扩容因子(默认为1.5倍)。
- 如果新数组容量大小小于所需容量,则按照所需容量分配新的数组;否则,按照新数组容量大小分配新的数组。
- 将原始数组中的元素复制到新数组中,并将新数组赋值给ArrayList对象的
elementData
变量。
import java.util.ArrayList;
public class EnsureCapacityExample {
public static void main(String[] args) {
// 创建一个空的ArrayList
ArrayList<String> list = new ArrayList<>();
// 预先设定ArrayList内部数组的容量为20
list.ensureCapacity(20);
// 现在,ArrayList的内部数组至少可以容纳20个元素
// 添加元素到ArrayList
list.add("Element 1");
list.add("Element 2");
list.add("Element 3");
// ...
}
}
通过使用ensureCapacity()方法,我们可以避免由于频繁扩容带来的性能损失,提高程序效率。
三、 常见操作分析
3.1 获取元素:get()方法的实现原理
在Java中,ArrayList的get()
方法实际上是通过调用数组的索引来获取指定位置的元素。ArrayList内部维护了一个Object类型
的数组来存储元素。
- 当调用get()方法时,ArrayList会将传入的索引作为数组的下标,直接访问数组中对应位置的元素,并返回该元素。
- 因为数组的访问是基于内存地址的,所以获取元素的时间复杂度为
O(1)
,即常数时间复杂度。 - ArrayList的
get()
方法并不会对数组进行拷贝或重新分配空间,因此在获取元素时是非常高效的。 - 频繁进行插入、删除等涉及数组扩容的操作,可能会导致性能下降。
ArrayList的get()
方法通过直接访问底层数组的方式快速获取指定位置的元素。
3.2 删除元素:remove()方法的实现原理
Java中的ArrayList类是基于数组实现的动态数组,当我们使用remove()
方法从ArrayList中删除元素时,这个方法会将指定位置的元素从内部数组中移除,并将后续元素向前移动一位。
这个操作可以通过以下几个步骤来实现:
- 检查待删除的元素下标是否越界,如果越界则抛出
IndexOutOfBoundsException
异常。 - 将要删除的元素从内部数组中移除,这个过程可以通过
System.arraycopy()
方法来实现,该方法可以将数组的某一范围内的元素复制到另一个位置上。 - 将后续元素向前移动一位,以填补被删除的空位。这个过程同样可以通过
System.arraycopy()
方法来实现。
ps:ArrayList的remove()
方法只能移除第一个与指定元素相等的元素。如果我们想要移除所有等于指定元素的元素,可以通过循环遍历ArrayList并使用remove()
方法来实现。
3.3 修改元素:set()方法的实现原理
Java中的ArrayList是一种基于数组的动态数组实现,它继承了AbstractList
类并实现了List
接口。set()
方法是ArrayList中的一个方法,用于将指定索引位置的元素替换为新的元素。
其实现原理如下:
- 首先,
set()
方法会检查传递的索引是否在ArrayList范围之内。如果索引小于0或大于等于ArrayList的大小(size()
方法返回的值),则会抛出IndexOutOfBoundsException
异常。 - 如果索引有效,则会使用数组的索引定位到指定的元素,并将其替换为新的元素。
set()
方法返回被替换掉的元素。- 在替换元素时,ArrayList可能需要调整内部数组的大小。如果新元素的大小与当前数组的容量不匹配,ArrayList会创建一个新数组,并将所有元素从旧数组复制到新数组中。
总之,ArrayList的set()
方法的实现原理是通过数组索引定位和替换元素来完成的,而且可能需要动态调整内部数组的大小。
四、 性能分析
4.1 时间复杂度分析
在Java中,ArrayList是一个动态数组实现的集合类,它提供了随机访问和快速插入/删除元素的功能。
下面是ArrayList的常见操作及其时间复杂度分析:
- 访问元素(get):通过索引访问特定位置的元素,时间复杂度为
O(1)
。 - 插入元素(add):在指定位置插入元素,平均时间复杂度为
O(n)
,最坏情况下需要将插入位置之后的元素都向后移动,时间复杂度为O(n)
。 - 删除元素(remove):删除指定位置的元素,平均时间复杂度为
O(n)
,最坏情况下需要将删除位置之后的元素都向前移动,时间复杂度为O(n)
。 - 查找元素(contains):判断集合中是否包含某个元素,平均时间复杂度为
O(n)
,需要遍历整个集合来查找。 - 获取集合大小(size):获取集合中元素的数量,时间复杂度为
O(1)
。
需要注意的是,ArrayList的插入和删除操作涉及到元素的移动,当集合的大小较大时,这些操作可能会导致性能下降。如果需要频繁进行插入和删除操作,可以考虑使用LinkedList
来代替ArrayList
,因为LinkedList
对于插入和删除操作的时间复杂度是O(1)
。
4.2 空间复杂度分析
ArrayList的空间复杂度主要取决于两个因素:集合中的元素数量和内部数组的容量。
- 元素数量:ArrayList存储的元素数量,即集合的大小,会占用一定的空间。假设元素数量为n,则空间复杂度为
O(n)
。 - 内部数组容量:ArrayList内部使用一个动态数组来存储元素,数组的容量可能会比集合的大小大一些,以容纳未来添加的元素。假设数组的容量为m,则空间复杂度为
O(m)
。
需要注意的是,ArrayList的实际空间占用可能会比集合中的元素数量多一些,因为它预留了一些额外的容量供后续添加元素使用。当集合的元素数量接近或超过内部数组的容量时,ArrayList会自动进行扩容操作,重新分配更大的数组并将原有元素复制到新数组中,这可能会导致空间复杂度的增加。
在实际使用中,可以通过调整ArrayList的初始容量或使用构造函数指定初始容量来控制空间复杂度。通常情况下,如果能够预估集合的大小,设置一个适当的初始容量可以减少扩容操作的频率,提高性能。
4.3 与LinkedList的比较
ArrayList和LinkedList是Java中两种常见的集合类,它们都实现了List接口,但在内部实现和性能特点上有所不同。
下面是ArrayList和LinkedList的比较:
- 内部实现:
- ArrayList:使用动态数组实现,内部维护一个可变长度的数组来存储元素。
- LinkedList:使用双向链表实现,内部由一系列节点组成,每个节点包含元素值和前后指针。
- 访问效率:
- ArrayList:由于使用数组实现,可以通过索引直接访问元素,因此随机访问的效率很高,时间复杂度为
O(1)
。但在插入和删除元素时,需要移动数组中的元素,效率较低,时间复杂度为O(n)
。 - LinkedList:插入和删除元素的效率较高,因为只需要调整节点的指针,时间复杂度为
O(1)
。但在随机访问元素时,需要从头节点开始按序遍历查找,效率较低,时间复杂度为O(n)
。
- ArrayList:由于使用数组实现,可以通过索引直接访问元素,因此随机访问的效率很高,时间复杂度为
- 空间占用:
- ArrayList:使用动态数组,会预留一定容量的空间,当元素数量超过容量时,需要进行扩容操作。因此,可能会有额外的空间浪费。
- LinkedList:使用链表结构,每个节点除了存储元素还需要存储前后节点的指针,因此会略微增加一些额外空间。
- 适用场景:
- ArrayList:适合于随机访问和遍历操作较多的场景,例如根据索引访问元素、遍历集合等。但在频繁插入和删除元素的情况下,性能相对较差。
- LinkedList:适合于频繁插入和删除元素的场景,例如实现队列或栈等数据结构。但在随机访问元素时,性能相对较差。
五、 源码解读
5.1 成员变量
5.2 构造方法
5.3 trimToSize()方法
5.4 indexOf()方法
5.5 clone()方法
5.6 get()方法
5.7 set()方法
5.8 add()方法
5.9 remove()方法
5.10 addAll()方法
六、 案例分析与实例演示
6.1 案例分析
假设有一个学生管理系统,需要存储学生的信息,包括姓名、年龄、性别等。
为了方便管理,我们可以使用ArrayList来存储学生对象。
首先定义一个学生类,包含姓名、年龄、性别三个属性:
public class Student {
private String name;
private int age;
private String gender;
public Student(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// getter 和 setter 方法省略
}
然后在主类中创建ArrayList对象,并添加学生信息:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
// 创建ArrayList对象
ArrayList<Student> list = new ArrayList<>();
// 添加学生信息
list.add(new Student("张三", 18, "男"));
list.add(new Student("李四", 20, "女"));
list.add(new Student("王五", 19, "男"));
// 遍历学生信息
for (Student student : list) {
System.out.println("姓名:" + student.getName() + " 年龄:" + student.getAge() + " 性别:" + student.getGender());
}
}
}
输出结果如下:
姓名:张三 年龄:18 性别:男
姓名:李四 年龄:20 性别:女
姓名:王五 年龄:19 性别:男
6.2 实例演示
演示一下如何使用ArrayList实现一个简单的购物车程序。
首先定义一个商品类,包含名称和价格两个属性:
public class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
// getter 和 setter 方法省略
}
然后在主类中创建ArrayList对象,并添加商品信息:
import java.util.ArrayList;
import java.util.Scanner;
public class ShoppingCart {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 创建ArrayList对象
ArrayList<Product> cart = new ArrayList<>();
// 添加商品信息
cart.add(new Product("可乐", 3.5));
cart.add(new Product("薯片", 5.0));
cart.add(new Product("巧克力", 8.0));
// 输出商品信息
System.out.println("欢迎来到购物车!");
for (Product product : cart) {
System.out.println(product.getName() + " 价格:" + product.getPrice());
}
// 计算总价
double totalPrice = 0;
while (true) {
System.out.print("请输入要购买的商品编号(输入-1结束):");
int index = scanner.nextInt();
if (index == -1) {
break;
}
Product product = cart.get(index);
System.out.println("已选择 " + product.getName() + " 价格:" + product.getPrice());
totalPrice += product.getPrice();
}
System.out.println("总价:" + totalPrice);
}
}
运行程序,输出结果如下:
欢迎来到购物车!
可乐 价格:3.5
薯片 价格:5.0
巧克力 价格:8.0
请输入要购买的商品编号(输入-1结束):0
已选择 可乐 价格:3.5
请输入要购买的商品编号(输入-1结束):1
已选择 薯片 价格:5.0
请输入要购买的商品编号(输入-1结束):2
已选择 巧克力 价格:8.0
请输入要购买的商品编号(输入-1结束):-1
总价:16.5
盈若安好,便是晴天
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!