一、定义
ArrayList是Java中的一种常见的数据结构,它实现了List接口,是线程不安全的动态数组,也是我们常用的集合,它允许任何元素的插入,甚至包括null元素。
二、属性
//默认初始容量
private static final int DEFAULT_CAPACITY = 10;
//空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认容量的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//集合真正存储数组元素的数组 transient:被修饰的变量不参与序列化和反序列化。
transient Object[] elementData;
//集合的大小
private int size;
三、构造器分析
1、无参构造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
将一个默认容量的空数组赋值给elementData数组。当数组长度发生变化(add添加)时,该数组默认变成长度为10的数组。
2、指定数组长度的有参构造器
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
创建一个指定长度的ArrayList集合
当 initialCapacity > 0 时,创建一个指定长度的Object数组并且将它赋值给elementData 。
当 initialCapacity = 0 时,将空数组赋值给elementData 。
当 initialCapacity < 0 时,抛出IllegalArgumentException(非法参数)异常。
3、指定集合元素的有参构造器
//首先看toArray方法:将集合转数组的方法
public Object[] toArray() {
//调用数组工具类方法进行拷贝
return Arrays.copyOf(elementData, size);
}
public ArrayList(Collection<? extends E> c) {
// 将集合构造中的集合对象转成数组,且将数组的地址赋值给elementData
elementData = c.toArray();
// 将elementData的长度赋值给 集合长度size,且判断是否不等于 0
if ((size = elementData.length) != 0) {
// 判断elementData 和 Object[] 是否为不一样的类型
if (elementData.getClass() != Object[].class)
//如果不一样,使用Arrays的copyOf方法进行元素的拷贝
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 用空数组代替
this.elementData = EMPTY_ELEMENTDATA;
}
}
将集合作为参数传入,首先对集合进行数组转化,转化成数组,然后判断是否长度大于0。
如果 (size = elementData.length) == 0 ,则用空数组代替。
如果 (size = elementData.length) != 0 ,进入到下一步判断。
如果elementData与Object的类型不同,使用copyOf进行元素拷贝。
关于copyOf 与深拷贝、浅拷贝的问题,详见另一篇博客:
四、常用方法分析
1、添加方法
1.1、add(E e) 方法
//将添加的数据传入给 e
public boolean add(E e) {
//调用方法对内部容量进行校验 【扩容机制】
ensureCapacityInternal(size + 1);
//数组长度加1,并且把e赋值上去
elementData[size++] = e;
//返回添加成功
return true;
}
ArrayList的add方法其实很简单,只是在内部容量校验通过后对数组长度加1,并且将数据赋值到数组上就可以,但是难点在于容量校验与扩容机制上。
1.2、ArrayList的扩容机制剖析
详见另一篇博客:ArrayList 的扩容机制_再吃一口就不吃了~的博客-CSDN博客
1.3、add(int index,E element) 方法
//将元素添加到指定位置
public void add(int index, E element) {
//对参数进行判断,如果输入错误报错:数组下标越界异常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//内部容量校验,判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//将元素向后移动1位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//将元素添加到指定下标位置上
elementData[index] = element;
//最后对数组长度加1
size++;
}
该方法在于指定位置添加元素,该位置之后的元素向后移动一位的操作。主要在于System.arraycopy() 方法的理解上。
对于System.arraycopy 方法与Arrays.copyOf 方法单独出一个博客:
1.4、addAll(Collection<? extends E> c) 方法
public boolean addAll(Collection<? extends E> c) {
//把集合的元素转存到Object类型的数组中
Object[] a = c.toArray();
//记录数组的长度
int numNew = a.length;
//调用方法检验是否要扩容,且让增量++
ensureCapacityInternal(size + numNew);
//调用方法将a数组的元素拷贝到elementData数组中
System.arraycopy(a, 0, elementData, size, numNew);
//集合的长度+=a数组的长度
size += numNew;
//只要a数组的长度不等于0,即说明添加成功
return numNew != 0;
}
addAll()方法与上面两个有些区别在于:传入的参数不再是单个元素,而是一个包含0至多个元素的集合,首先要将集合转换成Object类型的数组,通过length获取数组的长度,再进行扩容校验,最关键的一步:将数组中的元素拷贝到elementData数组的后面,最后扩展elementData数组长度。返回值为boolean类型,【只要传入的集合不是null就返回true】。
1.5、addAll(int index, Collection<? extends E> c) 方法
public boolean addAll(int index, Collection<? extends E> c) {
//校验索引
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//将数据源转成数组
Object[] a = c.toArray();
//记录数据源的长度 3
int numNew = a.length;
//目的就是为了给集合存储数据的数组进行扩容
ensureCapacityInternal(size + numNew);
//numMoved:代表要移动元素的个数 --> 1个
//numMoved: 数据目的(集合list1)的长度-调用addAll的第一个参数 (索引1)
int numMoved = size - index;
//判断需要移动的个数是否大于0
if (numMoved > 0)
//使用System中的方法arraycopy进行移动
System.arraycopy(elementData, index, elementData, index + numNew,numMoved);
//才是真正将数据源(list)中的所有数据添加到数据目的(lsit1)
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
与1.4的区别在是否是在指定位置添加集合元素。需要注意的是:当 size == index 的时候,两个 addAll() 方法没有任何区别。只有当 size - index > 0 的时候,才会移动原数组的元素。
2、删除方法
2.1、remove(int index) 方法
public E remove(int index) {
//范围校验
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//增量++
modCount++;
//将index对应的元素赋值给 oldValue
E oldValue = (E)elementData(index);
//计算集合需要移动元素个数
int numMoved = size - index - 1;
//如果需要移动元素个数大于0,就使用arrayCopy方法进行拷贝
//注意:数据源和数据目的就是elementData
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将源集合最后一个元素置为null,尽早让垃圾回收机制对其进行回收
elementData[--size] = null;
//返回被删除的元素
return oldValue;
}
remove(int index)方法:删除指定索引下标的元素。
调用通过数组下标删除元素的方法首先对下标进行判定,数组下标越界会报错,下标符合后对数组修改次数加1,开辟一个变量接收指定下标元素的值,随后计算要移动的元素的个数,当 numMoved > 0 时,说明后面还有元素要移动,接下来通过System.arraycopy() 方法将数组拷贝,变成新的数组,最后将原集合最后一个元素设置为null,目的是为了尽早可以被垃圾回收机制回收。最后返回 oldValue【被删除的元素】。
2.2、remove(Object o) 方法
public boolean remove(Object o) {
//判断要删除的元素是否为null
if (o == null) {
//遍历集合
for (int index = 0; index < size; index++)
//判断集合的元素是否为null
if (elementData[index] == null) {
//如果相等,调用fastRemove方法快速删除
fastRemove(index);
return true;
}
} else {
//遍历集合
for (int index = 0; index < size; index++)
//用o对象的equals方法和集合每一个元素进行比较
if (o.equals(elementData[index])) {
//如果相等,调用fastRemove方法快速删除
fastRemove(index);
return true;
}
}
//如果集合没有o该元素,那么就会返回false
return false;
}
remove(Object o) 方法 :删除指定内容的第一个元素。
当 o == null 时,遍历集合,当某一集合元素 == null 时,调用 fastRemove() 方法,返回 true 。
当 o 为其他元素时,遍历集合,通过 equals 方法判断每个元素的值是否与 o 相等,如果相等就会调用 fastRemove() 方法并返回true 。
当集合不存在这个元素的时候,便返回 false 。
2.3、fastRemove(int index) 方法
private void fastRemove(int index) {
//增量++
modCount++;
//计算集合需要移动元素的个数
int numMoved = size - index - 1;
//如果需要移动的个数大于0,调用arrayCopy方法进行拷贝
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将集合最后一个元素置为null,尽早被释放
elementData[--size] = null;
}
fastRemove(int index) 方法:删除元素方法,只是在2.2中会调用,它的作用在于:获取将要删除的指定的元素下标,通过下标再进行删除。是 remove(Object o)方法的核心功能 。
2.4、removeRange(int fromIndex, int toIndex) 方法
//删除指定起始位置元素的方法
protected void removeRange(int fromIndex, int toIndex) {
//校验开始位置是否小于结束位置 有问题报错
if (toIndex < fromIndex) {
throw new IndexOutOfBoundsException("toIndex < fromIndex");
}
//对集合的修改操作次数加1
modCount++;
//将elementData从toIndex位置开始的元素向前移动到fromIndex
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
//将toIndex位置之后的元素全部置空,等待GC回收
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
//修改size
size = newSize;
}
removeRange(int fromIndex, int toIndex) 方法:在 [ fromIndex - toIndex )范围内,所有元素都将被删除。
它的执行过程是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。
注意:这个方法的权限是 protected ,受保护的方法,具体原因我还没搞懂。创建的对象没办法调用 removeRange 方法,但是我找到了它的平替
对象.subList(fromIndex,toIndex).clear();
如果我学会了出一篇博客,链接在这:
3、修改方法
set(int index, E element) 方法
public E set(int index, E element) {
//范围校验
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//先取出index对应的元素,且赋值给oldValue
E oldValue = elementData(index);
//将element直接覆盖index对应的元素
elementData[index] = element;
//返回被覆盖的元素
return oldValue;
}
set(int index, E element) 方法:修改指定元素的元素值。
首先对 index 进行校验,通过后将该位置上原来的值赋值给 oldValue ,然后 把新值赋给elementData[index] 上,最后返回 oldValue 。
4、获取方法
get(int index) 方法
public E get(int index){
//首先对index进行校验
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
// 直接返回指定下标的数组的值
return elementData[index];
}
get(int index) 方法:源码很简单,直接 return elementData数组指定下标的值即可。
5、其他方法
5.1、trimToSize() 方法
public void trimToSize() {
//修改数组次数加1
modCount++;
//判断数组元素个数是否小于数组长度
if (size < elementData.length) {
//三元运算符:如果没有元素,则设置成空数组,
//反之,拷贝elementData成size大小的新数组,数组内容保持不变
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
trimToSize() 方法:剔除未被利用的容量,使数组长度与元素个数相同。
由于扩容机制,elementData的长度会被拓展,size标记的是其中包含的元素的个数。在这种情况下,内部数组中或许会有一些未分配的空间。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length很size相同,节省空间。
5.2、size() 方法
public int size() {
// 直接返回size
return size;
}
size() 方法:返回elementData数组的实际长度。
5.3、isEmpty() 方法
public boolean isEmpty() {
// 直接返回 size == 0 的结果,没有元素返回true
return size == 0;
}
isEmpty() 方法:判断elementData数组是否为空。
5.4、indexOf(Object o) 方法
public int indexOf(Object o) {
//判断传入的元素是否为null
if (o == null) {
//遍历集合
for (int i = 0; i < size; i++)
//判断下标为 i 的元素是否为null
if (elementData[i]==null)
//如果是,返回该元素的下标
return i;
//传入的数据不为null
} else {
// 遍历集合
for (int i = 0; i < size; i++)
// 通过 equals 方法判断元素的值是否与传入的值相等
if (o.equals(elementData[i]))
// 相等返回该元素的下标
return i;
}
// 如果集合中不存在该数据则返回 -1
return -1;
}
indexOf(Object o) 方法:获取指定元素在此集合中第一次出现的下标。
注意:在这个方法中比较 null 的时候,采用的是 == 比较,比较非 null 值的时候,采用的是 o1.equals(o2) 的方法进行比较,但是,在比较未重写 equals 方法的对象的时候,仍然比较的是引用值是否相等,并不是具体的值,之所以比较 String ,Array 等引用数据类型的时候会比较内容,是因为它们已经重写了 equals 方法,所以在比较自定义对象的时候,记得重写 equals 方法。
5.5、lastIndexOf(Object o) 方法
public int lastIndexOf(Object o) {
if (o == null) {
// 在这需要注意,从后往前数,起始值应当是size-1
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
lastIndexOf(Object o) 方法:获取指定元素在此集合中最后一次出现的下标。需要注意,从后往前数,起始值应当是size-1。
5.6、contains(Object o) 方法
public boolean contains(Object o) {
// 返回indexOf的结果,存在该元素返回true,不存在返回false
return indexOf(o) >= 0;
}
contains(Object o) 方法:查询集合中是否存在该元素。返回 indexOf 的结果,存在该元素返回 true ,不存在返回 false 。
5.7、clear() 方法
public void clear() {
//修改集合次数加1
modCount++;
// 遍历集合,将每个元素赋值为null,等待GC机制回收
for (int i = 0; i < size; i++)
elementData[i] = null;
//最后设置集合长度为0
size = 0;
}
clear() 方法 :清除集合中的所有元素。
5.8、subList(int fromIndex, int toIndex) 方法
public List<E> subList(int fromIndex, int toIndex) {
//进行参数校验
subListRangeCheck(fromIndex, toIndex, size);
// 返回截取内容
return new SubList(this, 0, fromIndex, toIndex);
}
//参数校验方法
static void subListRangeCheck(int fromIndex, int toIndex, int size) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > size)
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
}
subList(int fromIndex, int toIndex) 方法:截取并返回 [ fromIndex , toIndex )的元素 。
5.9、clone() 方法
ArrayList类中的 clone 方法 主要继承于 Object 类
public Object clone() {
try {
//调用父类的clone方法返回一个对象的副本
ArrayList<?> v = (ArrayList<?>) super.clone();
//将返回对象的elementData数组的内容赋值为原对象elementData数组的内容
v.elementData = Arrays.copyOf(elementData, size);
//将副本的modCount设置为0
v.modCount = 0;
//返回副本
return v;
} catch (CloneNotSupportedException e) {
// 报错
throw new InternalError(e);
}
}
private native Object internalClone();
Object类中的 clone 方法
protected Object clone() throws CloneNotSupportedException {
//判断该对象是否是支持克隆的,是否实现了克隆的接口
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class " + getClass().getName() +
" doesn't implement Cloneable");
}
//返回内部克隆的参数
return internalClone();
}
clone()方法:用来生成一个当前ArrayList的克隆对象,它与原始对象具有相同的元素和容量,并且是独立的对象。
五、需要注意的细节和常见问题汇总
1、ArrayList 每次扩容的容量?
第一次扩容:10,以后的每次扩容:原容量的1.5倍。
2、ArrayList 是线程安全的么?
ArrayList 本身不是线程安全的,比如 add 方法执行时首先会执行容量校验,必要时还需要扩容,
在多个线程进行 add 操作时可能会导致 elementData 数组越界。
解决线程不安全的方法有三种:
1、
使用 Vector写法:ArrayList<String> list = newVector<>();
Vector大部分方法和ArrayList都是相同的,只是加上了synchronized关键字,这种方式严重影响效率,因此,不再推荐使用Vector了 。
2、使用 Collections 里面的 synchronizedList
写法:ArrayList<String> list =Collections.synchronizedList(new ArrayList<>());
这个方法可以实现线程安全,但是迭代器未加锁,需要手动实现同步。
3、使用 CopyOnWriteArrayList
写法:ArrayList<String> list = new CopyOnWriteArrayList();
3、如何复制某个 ArrayList 到另一个 ArrayList 中?
1、使用 clone() 方法
2、使用 ArrayList 构造方法
3、使用 addAll 方法
4、ArrayList 和 LinkList 区别?
ArrayList
基于动态数组的数据结构对于随机访问的get和set,ArrayList要优于LinkedList 。对于随机操作的add和remove,ArrayList不一定比LinkedList慢 (ArrayList底层由于是动态数组,因此,并不是每次add和remove的时候都需要创建新数组)LinkedList
基于链表的数据结构
对于顺序操作,LinkedList不一定比ArrayList慢
对于随机操作,LinkedList效率明显较低
5、为什么ArrayList中的clone方法不是继承AbstractList类,而是继承Object类的clone呢?
ArrayList继承于AbstractList类,但是AbstractList类中不存在clone方法,所有类的总类是Object类,所以ArrayList继承的clone方法是Object类中的clone方法。
6、Arrays.copyOf,这个代码有什么作用?
Arrays.copyof是用于数组进行复制时常使用的方法,将原数组的内容复制到一个新数组中,新数组后期的改动对原数组不会产生任何影响。但是从源码上看其实质是调用了System.arraycopy方法。
7、浅拷贝的局限性是什么?
浅拷贝对于基本数据类型拷贝是深拷贝,也就是两个数组之间是毫无影响的。但当新数组拷贝原数组引用类型的元素时,引用数据类型只是复制了引用值,当原数组或者新数组更改的时候,另一个相应的引用元素也会随之更改。
8、为什么 LinkedList 随机访问比顺序访问要慢这么多?
因为 ArrayList 实现了 RandomAccess 接口,而 LinkedList 没有实现 RandomAccess 接口。
通过源码分析:
由于随机访问的时候底层每次都需要进行折半的动作,再判断是从头还是从尾部一个个寻找,比较耗时。
而顺序访问只会在获取迭代器的时候进行一次折半的动作,以后每次都是在上一次的基础上获取下一个元素。
因此顺序访问要比随机访问快得多。
9、在遍历集合取出结果集之前面临一个问题,使用普通for遍历好 还是使用迭代器(增强for)?
数据量特别大的时候一定要考虑
对返回的集合进行判断,如果返回的集合实现了 RandomAccess 就使用 普通for
否则使用迭代器(增强for)
//判断是否实现了 RandomAccess 接口
if(list instanceof RandomAccess){
//如果实现了就用普通 for 循环
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}else {
// 如果没实现就使用 增强 for 循环
for (Stutb stutb : list) {
System.out.println(stutb);
}
}