在网上找了些ArrayList的讲解,包括源码分析,找来找去,感觉内容讲的不是那么精髓,今天呢,我就斗胆给大家分享下我对ArrayList的理解哈,斗胆哈哈。
首先呢,我只讲java集合下ArrayList,其余LinkList、Map和Set等内容我在这篇文章中不进行分享,它们将依次在我的以后文章中出现和大家分享。
1、概述
在开发过程中,要数java集合中那个数据结构最常用,那么大家都会毫不犹豫的认为是Arraylist。它继承AbstractList类,底层是基于数组来实现容量大小的动态变化,并且允许null值存在,同时它还实现了List,RandomAccess,Cloneable,Serializable接口。因此支持快速访问,复制和序列化。
ArrayList是有序的,这里所说有序指的是元素放入ArrayList集合的先后顺序,不是里面的元素比较大小之类顺序。
2、ArrayList创建
ArrayList有三种方式创建,其中无参构造函数和带初始容量参数有参构造最为常用,另外一个参数是Collection的构造函数不常用。下面我就来依次和大家分析
2.1、无参构造创建 ——>new ArrayList<>()
List<Object> objects = new ArrayList<>();
这种创建方式,大家最为常用,我们来进这种方式创建源码来看看。
注意: 我们可以将泛型Object可以改成基本数据类型,也可以该用引用数据类型,一旦改变,那么我们在添加元素时只能添加我们指定的数据类型。比如:List<String> objects = new ArrayList<>();
那么我们添加的元素只能是String类型的数据,其余不能添加。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
//默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;
//空数组elenmentData 注意:这里是有参构造函数中使用,且参数为 0 时使用
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认空数组elenmentData 注意:这里是无参构造函数时使用
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//注意:上面两个空数组,其目的是为了区分是无参构造函数还是有参构造函数
//ArrayList底层数据元素
transient Object[] elementData;
//集合数据实际元素大小 注意:这里不是集合容量
private int size;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
}
在源码中,无参构造函数是直接将一个空Objects数组赋值给elementData(这就是ArrayList集合底层数据),因此我们知道,在无参构造创建ArrayList中,ArrarList初始大小为0,千万不要一昧听从某些人说是10。 为什么有些人说是10呢?因为我这里给讲的源码版本是11,ArrayList在1.8版本之前和之后发生了某些改变,这里的初始容量就是其一,在1.8之前默认容量是10,之后就改成了0。
这里你还会发出疑问,为什么上面有个默认容量大小DEFAULT_CAPACITY = 10
,没错这是默认容量大小,但是它在无参构造创建是初始容量是0,当它往里面添加第一个元素时,那么它的容量就变为了10, 在下面的ArrayList扩容时我将详细讲解。
从无参构造函数创建这段源码中我们可以清楚的看出两点:
一: ArrayList初始容量为0,而非10
二: ArrayList底层是Object数组。
注意:我在代码的注释中写的也有内容去解释,大家有那些不懂的一定记得看上面代码中注释,希望大家能养成一种习惯。
2.2、带有容量大小的有参构造创建——>new ArrayList<>(length);
List<Object> list = new ArrayList<>(20);
这里传参数20代表集合容量大小为20
源码如下所示:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
//默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;
//空数组elenmentData 注意:这里是有参构造函数中使用,且参数为 0 时使用
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认空数组elenmentData 注意:这里是无参构造函数时使用
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//注意:上面两个空数组,其目的是为了区分是无参构造函数还是有参构造函数
//ArrayList底层数据元素
transient Object[] elementData;
//集合数据实际元素大小 注意:这里不是集合容量
private int size;
//参数initialCapacity代表集合初始容量大小
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);
}
}
}
源码中initialCapacity参数代表是集合初始容量大小,进入构造方法后,将先判断传值是否大于0,当大于0时,直接new出一个Object数组,且数组大小为initialCapacity;当等于0时直接将EMPTY_ELEMENTDATA空数组赋值给elementData,注意下这个空数组叫EMPTY_ELEMENTDATA和上面无参构造赋值空数组不是同一个,它是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,两者虽然全是空数组,名字之所以不一样,是因为两者为了区分是无参构造还是传值为0的有参构造。最后一种是如果传值小于0,那么直接抛出非法容量大小异常。
通过有参构造再次看出ArrayList底层为Object类型数组。
2.3、使用指定 Collection 来构造 ArrayList 的构造函数(这个不常用)
ArrayList<Object> objects1 = new ArrayList<>(collection);
上面代码是在代码中创建方式,下面给大家列出源码,如下所示:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
//默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;
//空数组elenmentData 注意:这里是有参构造函数中使用,且参数为 0 时使用
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认空数组elenmentData 注意:这里是无参构造函数时使用
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//注意:上面两个空数组,其目的是为了区分是无参构造函数还是有参构造函数
//ArrayList底层数据元素
transient Object[] elementData;
//集合数据实际元素大小 注意:这里不是集合容量
private int size;
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
}
当进入构造函数时,先将collection转换为数组,然后将数组长度赋值给size,判断size是否不等于0,不等于0时,然后判断类型是否为Object类型,如果不是Object类型,那么将它转换为Object类型;如果长度等于0,那么直接将空Object数组赋值给elementData。
3、主要操作方法解析
在分享操作方法时,我不单单要讲方法使用,而且还要着重讲源码分析,分析源码嘛,过程肯定无聊,希望大家仔细琢磨透。在下面操作方法中,我将会着重讲解add()方法,这个是大家最常用并且关联的问题比较多,因此这里我将多下点功夫。
3.1、add()
add()方法是往集合中添加元素,其有两种情况,一种是直接添加元素,也就是add(“a”),这种将元素直接集合最后一个后面进行添加;另外一种方式是指定添加位置来添加元素,add(5,“a”),意思是在集合第五个元素中添加a这个元素。
直接添加元素add(“a”)方法分析:
//代码使用add()方法
list.add("a");
add()源码:
//向集合中添加元素,调用add()返回是boolean类型,成功则返回true,否则返回false
public boolean add(E e) {
modCount++;
//参数e代表我们要添加元素,elementData代表目前集合,size代表当前集合中元素个数
add(e, elementData, size);
return true;
}
//add()增加方法
private void add(E e, Object[] elementData, int s) {
//判断集合容量大小是否等于集合元素大小 你可以直接理解为 集合容量大小————>数组长度 集合元素大小————>数组中实际元素个数
if (s == elementData.length)
//容量大小等于集合元素大小说明集合容量已经满了,无法添加元素;必须要扩充集合容量大小
elementData = grow();
//将元素e赋值给集合最后一个元素
elementData[s] = e;
size = s + 1;
}
//增加集合容量大小方法
private Object[] grow() {
//将size+1
return grow(size + 1);
}
//增加集合容量大小
private Object[] grow(int minCapacity) {
//将原集合复制到容量大小扩充后的新集合中
return elementData = Arrays.copyOf(elementData,
//newCapacity()方法判断将集合容量增加长度1后是否满足能够将新元素添加进去
newCapacity(minCapacity));
}
//进行判断应该以何种方式去扩充集合容量,如果容量为minCapacity能够增加新元素,那么扩充容量为minCapacity
//否则按照扩充至oldCapacity + (oldCapacity >> 1),也就是原来元素的1.5倍
private int newCapacity(int minCapacity) {
// overflow-conscious code
//旧集合长度
int oldCapacity = elementData.length;
//扩充后集合元素,扩充为旧集合元素1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//新集合长度小于需要扩充的最小集合长度大小
//可以直接理解为原来集合为空,集合中没有任何一个元素
if (newCapacity - minCapacity <= 0) {
//判断集合是否为空
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//这里就是取出默认集合大小和最小集合长度的值
//也就是开始给空集合确定大小,默认集合长度为10
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
//扩充新集合元素小于int类型的最大值,则直接返回新集合元素大小,否则再调用hugeCapacity()方法
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
大家是不是看的贼恶心了?你们恶心不恶心我不知道,我就知道我在源码中分析在注解中解释是快要把我恶心死了,好了,给大家直接扔出结论吧,别再让大家恶心了。
结论: 直接调用add()方法添加元素,它是直接将该元素添加在集合的末尾;这里将会出现个问题,就是当添加这个元素时,这个集合长度恰好不够了怎么办?上面分析了那么多的源码基本上全是围绕这个问题在解决。也就是如何扩充这个元素集合长度大小,其实就是当向集合中添加元素时,会先判断其集合长度大小,当集合长度大小不够时,会再判断该集合是否为空集合,那这时也就是第一次向集合中添加元素,那么这个时候直接将集合长度大小扩容为10;如果不是空集合,那么需要注意还有三点:一是我们需要的集合长度大小 大于扩充后的1.5倍容量,那么我们就直接扩充为我们需要的集合长度;二是我们扩充的1.5倍之后集合容量大小比较大,那么我们直接扩充1.5倍;三是扩充1.5倍之后实在太大了,那么这个时候我们去判断我们需要的集合容量大小是否大于int的最大值,如果大于,那么就直接扩充为int最大值否则扩充为int最大值-8。
注意: 当别人问ArrayList集合如何扩充时,大家不要再傻傻直接说按照1.5倍扩容了,它是分好几种情况进行扩充的哦。
3.2、addAll()
废话少说,我直接上源码解析,希望大家耐着性子看完哈:
public boolean addAll(Collection<? extends E> c) {
//将集合转换成数组
Object[] a = c.toArray();
modCount++;
//传来集合长度
int numNew = a.length;
if (numNew == 0)
return false;
Object[] elementData;
final int s;
//判断传来集合长度是否大于原来集合剩余容量大小
if (numNew > (elementData = this.elementData).length - (s = size))
//剩余原来容量集合大小不够开始进行集合扩容,和add()方法扩容机制一样,不再详述
elementData = grow(s + numNew);
//将传来新集合添加在原来集合后面
System.arraycopy(a, 0, elementData, s, numNew);
//改变原来集合中元素大小
size = s + numNew;
return true;
}
addAll()方法目的是将一个集合中所有元素复制到另一个集合中。
流程:先将传递来的集合转换成数组,然后判断数组长度是否大于原来集合剩余的元素大小,如果大于,说明原来集合不能够完全将新元素复制过来,那么需要将集合容量进行扩容,扩容方法和add()方法扩容是一样的,这里就不再说明;最后直接将新集合复制到旧集合最后一个元素后面即可,记得size也要改变下。
3.3、set()
set()方法目的是将集合指定位置元素进行修改。
源码如下所示:
//index修改元素位置
public E set(int index, E element) {
//检查索引位置是否超过集合元素大小
Objects.checkIndex(index, size);
E oldValue = elementData(index);
//直接将指定元素修改为新元素
elementData[index] = element;
return oldValue;
}
set()方法流程:先判断传递过来的索引是否超过集合元素大小,如果超过则报异常;不超过说明索引在元素大小范围内。最后直接将指定元素修改为新元素即可。因为底层是数组,因此set修改非常快。
3.4、get()
get()方法目的为了获取指定位置元素。
源码如下所示:
public E get(int index) {
//判断索引是否越界
Objects.checkIndex(index, size);
//直接返回数组位置
return elementData(index);
}
//返回数组位置
E elementData(int index) {
return (E) elementData[index];
}
get()方法流程:根据传值过来索引,判断是否越界,越界直接报异常;不越界则直接返回数组位置。因为底层是数组,因此get查询非常快。
3.5、remove()
remove()方法有两种,一种是传值为索引,那么目的是将该索引位置的元素进行删除;另外一种传值是元素,目的是将该元素从集合中删除。
传值为索引的remove()方法源码如下所示:
public E remove(int index) {
//检查索引是否越界
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
//删除索引方法
fastRemove(es, index);
return oldValue;
}
//删除索引方法
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
//判断要删除的是否是集合的最后一个元素
if ((newSize = size - 1) > i)
//利用System.arraycopy()复制方法将该索引的元素给跳过不复制,其余全部复制
System.arraycopy(es, i + 1, es, i, newSize - i);
//将集合最后一个元素赋值为null
es[size = newSize] = null;
}
源码中根据索引来删除元素也非常简单:首先就是判断下索引是否越界,不越界的话开始删除该索引位置的元素,根据System.arraycopy()方法将该索引元素成功跳过复制(也就是不复制该索引的元素,其余都复制),最后将集合最后一个位置赋值为null,那么就巧妙的删除了该索引元素。
传值为元素的remove()方法源码如下所示:
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
found: {
//判断要删除的元素是否为null
if (o == null) {
for (; i < size; i++)
//循环判断集合中元素是否为null
if (es[i] == null)
break found;
} else {
//循环判断集合中元素是否与要删除元素相等
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
return false;
}
//删除索引方法
fastRemove(es, i);
return true;
}
//删除索引方法
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
源码中根据元素来删除集合中元素是非常巧妙,分析:先判断下删除元素是否为null值,如果是的话,那么开始循环遍历判断集合中元素是否为null值,如果是的话,那么记录下元素索引位置,然后根据System.arraycopy()方法将该索引元素成功跳过复制(也就是不复制该索引的元素,其余都复制),最后将集合最后一个位置赋值为null。这种处理方法真的非常巧妙。实名爱了爱了。
3.6、removeAll()
removeAll()传值为collection类型,目的是将原来集合与传来的集合所相等的元素全部删除。代码如下所示:
//removeAll方法
public boolean removeAll(Collection<?> c) {
//调用批量删除方法
return batchRemove(c, false, 0, size);
}
//批量删除方法
boolean batchRemove(Collection<?> c, boolean complement,
final int from, final int end) {
//判断传来集合是否为null
Objects.requireNonNull(c);
final Object[] es = elementData;
int r;
// Optimize for initial run of survivors
//遍历查询原集合中是否存在和新集合中相等元素,当有相等的,则直接跳出循环
//如果没有,则说明两个集合之间没有交集,直接报false
for (r = from;; r++) {
if (r == end)
return false;
if (c.contains(es[r]) != complement)
break;
}
int w = r++;
try {
//从两个集合交集的第一个元素开始往后遍历原来集合剩余所有元素
for (Object e; r < end; r++)
//如果新集合中不包含原来集合的这个元素
if (c.contains(e = es[r]) == complement)
//将这个元素赋值给原来集合,注意是原来集合的依次位置哈
es[w++] = e;
} catch (Throwable ex) {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
System.arraycopy(es, r, es, w, end - r);
w += end - r;
throw ex;
} finally {
modCount += end - w;
//将最后的脏数据设置为null方法
shiftTailOverGap(es, w, end);
}
return true;
}
//将最后的脏数据设置为null方法
private void shiftTailOverGap(Object[] es, int lo, int hi) {
System.arraycopy(es, hi, es, lo, size - hi);
//将最后脏数据赋值为null数据
for (int to = size, i = (size -= hi - lo); i < to; i++)
es[i] = null;
}
这个方法的源码我就不带大家一点点分析了,因为这个方法大家用的不太多,源码大家大概知道下就行了,其实也是我太菜,我分析的也是个大概、、、希望大家凑合看看吧。
3.7、retainAll()
retainAll()是只保留两个集合中相等的元素,也就类似于两个集合的交集。和上面所分享的removeAll()方法正好相反,它是删除两个集合中相等的元素,而retainAll()中是只保留两个相等的元素。源码分析如下所示:
//retainAll方法
public boolean retainAll(Collection<?> c) {
//调用批量删除方法
return batchRemove(c, true, 0, size);
}
//批量删除方法
boolean batchRemove(Collection<?> c, boolean complement,
final int from, final int end) {
//判断传来集合是否为null
Objects.requireNonNull(c);
final Object[] es = elementData;
int r;
// Optimize for initial run of survivors
//遍历查询原集合中是否存在和新集合中相等元素,当有相等的,则直接跳出循环
//如果没有,则说明两个集合之间没有交集,直接报false
for (r = from;; r++) {
if (r == end)
return false;
if (c.contains(es[r]) != complement)
break;
}
int w = r++;
try {
//从两个集合交集的第一个元素开始往后遍历原来集合剩余所有元素
for (Object e; r < end; r++)
//如果新集合中不包含原来集合的这个元素
if (c.contains(e = es[r]) == complement)
//将这个元素赋值给原来集合,注意是原来集合的依次位置哈
es[w++] = e;
} catch (Throwable ex) {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
System.arraycopy(es, r, es, w, end - r);
w += end - r;
throw ex;
} finally {
modCount += end - w;
//将最后的脏数据设置为null方法
shiftTailOverGap(es, w, end);
}
return true;
}
//将最后的脏数据设置为null方法
private void shiftTailOverGap(Object[] es, int lo, int hi) {
System.arraycopy(es, hi, es, lo, size - hi);
//将最后脏数据赋值为null数据
for (int to = size, i = (size -= hi - lo); i < to; i++)
es[i] = null;
}
这个方法的源码和上面的removeAll方法源码基本上是一样的,唯一一点不一样的是在调用批量删除的方法中的第二个传值为true,正好这里和remiveAll是相反的,得到的结果是相反的。
3.8、indexOf()
indexOf()方法是查找传值元素的索引位置,源码也比较简单。源码如下所示:
//index()方法
public int indexOf(Object o) {
//遍历查找元素位置
return indexOfRange(o, 0, size);
}
//遍历查找元素位置方法
int indexOfRange(Object o, int start, int end) {
Object[] es = elementData;
//判断查找元素是否为null
if (o == null) {
//逐个遍历进行判断是否为null,采用==进行判断
for (int i = start; i < end; i++) {
if (es[i] == null) {
//直接返回索引位置
return i;
}
}
} else {
//逐个判断元素是否与传值元素相等,底层采用的是equals方法进行判断
for (int i = start; i < end; i++) {
if (o.equals(es[i])) {
//直接返回索引位置
return i;
}
}
}
//如果查找内容不存在,则直接返回-1
return -1;
}
从上层源码中可以发现,查找元素索引位置底层采用的是遍历集合比较的方法进行查找。
3.9、contains()
contains()方法是判断集合中是否包含这个元素,返回类型为boolean类型。源码如下所示:
//contains()方法
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
//查找元素位置索引
public int indexOf(Object o) {
//查找元素位置索引方法
return indexOfRange(o, 0, size);
}
//查找元素位置索引方法
int indexOfRange(Object o, int start, int end) {
Object[] es = elementData;
if (o == null) {
for (int i = start; i < end; i++) {
if (es[i] == null) {
return i;
}
}
} else {
for (int i = start; i < end; i++) {
if (o.equals(es[i])) {
return i;
}
}
}
return -1;
}
从上面源码中可以发现它和上面查找索引位置indexOf()方法是基本一样的,只不过它去判断下索引位置是否大于0,如果大于0则说明集合中存在该元素,返回为true;如果<0,则说明集合中不存在该元素,返回为false。
3.10、clear()
clear()方法目的是清空集合,其源码如下所示:
//clear()方法
public void clear() {
modCount++;
final Object[] es = elementData;
//循环遍历元素,将每个元素设置为null
for (int to = size, i = size = 0; i < to; i++)
es[i] = null;
}
4、集合三种遍历方式
for (int i = 0; i < list.size(); i++) {
int elem = (int) list.get(i);
System.out.println(i + "------->" + elem)
}
这种for循环比较常用,也不过多描述
for (Object elem : list) {
System.out.println(elem);
}
for-each同样也是比较常用,也不过多描述。
Iterator iterator = list.iterator();
while (iterator.hasNext()) {//是否还有元素
//取出下一个元素
Object next = iterator.next();
//删除这个迭代元素
iterator.remove();
System.out.println(next);
}
这个稍微比较重要,它采用迭代器循环,先从list中获取迭代对象,然后判断迭代对象中下一个元素是否存在,如果存在则可以利用next()方法获取该迭代元素,remove()方法删除该迭代元素。
注意: 集合循环删除元素必须使用迭代器循环删除,如果使用其他循环则会出现异常。具体原因分析,请看以前专门所写的foreach遍历集合删除元素抛异常文章。
总结: ArrayList底层采用数组类型,查询、修改效率高,但是添加和删除效率比较低。
好了,这篇吐血源码分析就写到这吧,一下子写了六千多字。拜拜啦,有哪些不正确的内容希望大家在评论区中给指出哦,还希望大家动动手指来个一键三连啦。