1. 介绍
- ArrayList是一个实现了List接口的变长数组。它实现了List定义的操作,并可以存储任意类型的元素(包括null)。
- 内部数据使用数组存储,因此从ArrayList获取数据是
常量级
的。- ArrayList是
不同步
的,多个线程同时操作一个ArrayList实例的时候,可能会出现不可预估的问题。
这是ArrayList类的定义,继承自AbstractList,实现了List接口、RandomAccess(随机访问接口)、Cloneable(可复制)、Serializable(可序列化接口)。
1.1. 实现RandomAccess接口
List实现使用的标记接口,表明它支持快速(通常是常量时间)随机访问。该接口的目的主要是允许常用算法改变其行为,以便在应用于随机或者顺序访问列表时提供良好的性能。
1.2. 实现Cloneable接口
标记接口,调用Object.clone()方法的时候必须实现这个接口,否则会抛出异常:CloneNotSupportedException。
1.3. 实现Serializable接口
标记接口,表示能被序列化。
1.4. 实现List接口,继承AbstractList
我们看AbstractList源码我们会发现它继承了List接口,所以这里其实可以不用实现List接口,这个应该是Java开发者遗留的小问题。
- List接口中规定了实现List接口的子类需要实现的方法,比如常用的get、add、remove、set、indexOf等等。
- AbstractList给出了一些默认的实现,子类可以重写或者直接复用。
2. 字段
- 内部数组
- 元素数量
- 静态常量
重点看下面定义的两个Object数组都是空的数组实例。
这两个的作用是用于了解ArrayList在添加元素的时候是如何扩容的。会在后面详细解释。
3. 构造器
- 无参构造
- 参数为初始容量的构造器
注意:无参构造器和初始容量为0时,内部维护的数组被设定了两个不一样的空数组。
这里在后面添加元素时扩容方式不同
- 参数为集合的构造器
注意:传入集合为空的时候和初始容量为0的构造器一样,都是空数组1;区别于无参构造器。
4. 添加数据+扩容机制(重点)
ArrayList内部是通过维护了一个Object数组实现的,向ArrayList中添加数据其实就是向数组添加数据。
但是我们知道数组的大小是在创建的时候就确定了的,但是我们在向ArrayList中添加数据的时候,好像没有这个问题,这就是因为它的扩容机制。
- 添加元素
- 计算并确定内部容量
- 扩容(如果需要的话)
- 总结一下
(1)通过无参构造器创建一个新对象,初始容量是0。
(2)第1次添加元素时,创建一个长度为10的数组,并将该元素赋值到数组的第1个位置。
(3)第2次添加元素时,size+1<数组长度10,直接添加到数组的第2个位置。
(4)第10次添加元素时,size+1=10=数组长度10,直接添加到数组的第10个位置。
(5)第11次添加元素时,size+1=11>数组长度11,需要扩容,计算得到新的数组长度应该为10+10/2=15,创建一个长度为15的数组将之前数组中的数据复制进去。将元素添加到新数组的第11个位置。
(6)一直添加元素,直到由于int溢出导致计算出来的新长度<size+1时,扩容的长度设置为size+1。创建新数组复制数据并添加新数组。
(7)添加元素到达临界值时,进入大数判断,将新长度设置为int的最大值,创建新数组复制数据并添加新数组。
(8)第int的最大值+1次添加元素时,抛出越界异常。
5. 删除元素
5.1. 根据索引删除元素
由于删除指定索引位置的元素,在数组中的实现是将当前元素删除后,将索引后面所有的数据往前移动一位。即先计算当前元素后是否还有数据,由于索引从0开始,所以size-index-1 就是index后面元素的总数;如果后面有数据的话,内部的Object自复制(将index+1开始的所有的数据重新复制到Object数组:从index开始)。
此时由于最后一个位置没有被覆盖,所以需要将最后一个位置删除,设置为null,主要是将真实数据的引用移除以便于垃圾回收。
来举个栗子:
- 先判断索引4位置后还有几个数据,size(8)-index(4)-1=3
- 有数据,需要从index+1=5索引开始共计3个数据复制到数组从index=4开始的位置。
用图片表示为:
5.2. 删除指定元素(只删除第一次出现的)
小贴士:开发中如果碰到可能会有null的地方,记得做null判断,防止空指针异常。
我们发现,再找到第一个相同的元素之后,直接return了,所以这个方法只能删除第一个匹配到的元素。
6. 修改元素
7. 查询元素
7.1. 根据索引查询元素
7.2. 根据元素查询索引(碰到的第一个)
8. 遍历方式(重点)
8.1. 普通for循环
ArrayList list = new ArrayList();
for (int i = 0; i < 10; ++i) {
list.add(i);
}
for (int i = 0; i < list.size(); ++i) {
System.out.print(list.get(i) + " ");
}
8.2. 迭代器(重点)
ArrayList list = new ArrayList();
for (int i = 0; i < 10; ++i) {
list.add(i);
}
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
来,我们看下具体的实现即注意事项:
我们在文章就给出了ArrayList的继承关系,ArrayList实现了List接口,List继承自Collection接口,Collection接口继承自Iterable接口,在这个接口中有一个方法是:
我们可以通过该方法获取一个迭代器来辅助进行集合遍历。
我们来看下ArrayList是如何实现该方法的。
在ArrayList的内部实现中,是返回了一个Itr对象。
8.2.1. 基础属性
8.2.2. 获取元素、删除元素
8.2.3. 注意事项
我们可以看到在next()和remove()中都进行checkForComodification()判断,表示在进行迭代的过程中不可以直接调用Arraylist的add()、remove()对列表进行修改。
如果我们需要在迭代过程中需要进行元素删除的话,可以使用迭代器中的remove()方法。
8.3. 增强for循环(迭代器变种)
ArrayList list = new ArrayList();
for (int i = 0; i < 10; ++i) {
list.add(i);
}
for (Object i : list) {
System.out.print(i + " ");
}
这种方法是Java的一种语法糖,通过编译后的class文件反编译后我们可以看到会变为使用迭代器
ArrayList list = new ArrayList();
int i;
for(i = 0; i < 10; ++i) {
list.add(i);
}
Iterator var4 = list.iterator();
while(var4.hasNext()) {
Object i = var4.next();
System.out.print(i + " ");
}