ArrayList源码分析
发布日期:2021-05-06 23:31:25 浏览次数:21 分类:技术文章

本文共 9538 字,大约阅读时间需要 31 分钟。

简介

ArrayList是一个动态数组,与普通数组相比,他可以动态的增加或者减少里面的元素(当然不是无限制的)。它extends AbstractList,implements List,提供了增加、修改、删除、遍历这些方法。

它实现了Cloneable接口,即覆盖了函数clone(),表示能被克隆。
实现了java.io.Serializable接口,意味着ArrayList支持序列化,能通过序列化去传输。
实现了RandmoAccess接口,提供了快速访问功能,即通过序号来访问对象。
需要额外注意的是,与Vector不同,ArrayList不是线程安全的。在多线程的环境下,还是使用Vector吧。

先看看基本属性

public class ArrayList
extends AbstractList
implements List
, RandomAccess, Cloneable, Serializable {
// 序列化id private static final long serialVersionUID = 8683452581122892189L; // 默认初始的容量10 private static final int DEFAULT_CAPACITY = 10; // 实例化一个空对象,如果指定的ArrayList为0,就返回它 private static final Object[] EMPTY_ELEMENTDATA = new Object[0]; // 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0]; // 当前数据对象存放地方,当前对象不参与序列化。是个动态数组,ArrayList的容量就是这个数组的大小。该值为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,当第一次添加元素进入 ArrayList 中时,数组将扩容值 DEFAULT_CAPACITY(10) transient Object[] elementData; // 当前数组长度 private int size; // 数组最大长度 private static final int MAX_ARRAY_SIZE = 2147483639; // 省略方法。。}

我们可以着重关注一下elementData,它是存放List元素的数组,所以添加到ArrayList中的元素都保存于此。后面扩容也会说到它。

构造方法

无参数构造方法

如果不传入参数的话,默认使用该构造方法创建ArrayList对象。

//默认容量0,当加入第一个元素后,扩容成默认容量10	public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }

int类型的构造方法

传入int类型的参数,即代表我们就指定了要创建的ArrayList的初始长度。如果initialCapacity>=0就使用指定的长度来进行初始化;否则,就抛出异常。

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); } }

Collection对象构造函数

把collection对象转换成数组,把数组对象给elementData;判断size作出对应处理…

//参数为Collection集合    public ArrayList(Collection
c) {
//集合转数组,再把地址给elementData elementData = c.toArray(); if ((size = elementData.length) != 0) {
// 如果不是Object类型的数组,就转换成Object类型 if (elementData.getClass() != Object[].class) //把collection对象的内容(可以理解为深拷贝)copy到elementData中 elementData = Arrays.copyOf(elementData, size, Object[].class); } else {
//把空对象EMPTY_ELEMENTDATA的地址赋给elementData this.elementData = EMPTY_ELEMENTDATA; } }

从构造函数这些来看,就知道elementData的作用有多大了吧。据说,之所以elementData要被transient修饰,是因为elementData里面并不是所有的元素都有数据,要防止空的元素被”无辜“序列化。

常规操作

add方法

add方法有两个,分别是带一个参数和两个参数的。

add(E e) 方法

先以串联的形式来分析一下add(E e)

//不指定位置,添加到数组末尾    public boolean add(E e) {
//确保添加的元素有地方存 ensureCapacityInternal(size + 1); // Increments modCount!! //确保新增的元素有位置存后,把新元素放上去 elementData[size++] = e; return true; }
//第一次添加的时候会将当前elementData数组的长度变为10,看DEFAULT_CAPACITY    private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } //判断是否需要扩充数组长度 ensureExplicitCapacity(minCapacity); }
//将修改次数自增1,并判断是否需要扩充数组长度    private void ensureExplicitCapacity(int minCapacity) {
modCount++;//修改次数自增1 // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity);//增长数组长度 }
//该方法用于增长数组长度,这里的arg0就是size+1的	private void grow(int arg0) {
int arg1 = this.elementData.length; int arg2 = arg1 + (arg1 >> 1); if (arg2 - arg0 < 0) {
arg2 = arg0; } if (arg2 - 2147483639 > 0) {
arg2 = hugeCapacity(arg0);//判断大小 } //得到arg2,已使用size+1>数组长度就去扩容为原来的1.5倍 this.elementData = Arrays.copyOf(this.elementData, arg2); }
private static int hugeCapacity(int var0) {
if (var0 < 0) {
//如果传入的小于0的话 报错 throw new OutOfMemoryError(); } else {
//大于0的话 如果大于 ((2^31)-1) = 2147483647-8 = 2147483639 return var0 > 2147483639 ? 2147483647 : 2147483639; } }

add(int index, E element)

这个方法是指定了位置,但是仔细看一下,跟上面那个不知道位置的方法差不太多。

public void add(int index, E element) {
//判断插入的位置是否在容量范围内 rangeCheckForAdd(index); //判断是否需要扩容 ensureCapacityInternal(size + 1); //确保有足够的容量之后,使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位。这样的移动方法,代价是不是太大了? System.arraycopy(elementData, index, elementData, index + 1, size - index); //在指定的位置添加元素 elementData[index] = element; //改变size size++; }

像确保元素有位置存储、判断是否扩充数组及扩容数组等方法跟上面一样,这里就不过多赘述了。

get方法

//传入index,返回指定位置的元素        public E get(int index) {
rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); }

set方法

public E set(int index, E element) {
rangeCheck(index);//检查set的位置 //把index上的元素交给oldValue E oldValue = elementData(index); //将传进来的element放在指定位置上 elementData[index] = element; //return之前存的oldValue return oldValue; }

remove方法

根据传入的参数删除某一元素,有两种,分别为根据索引和对象。删除后,该index位置后的元素都要统一前移一位。只是把最后一位置空,并不会影响数组长度。

根据索引remove元素

很明显,这里我们需要传入的是index索引位置,根据这个index去删除。

public E remove(int index) {
rangeCheck(index);//判断这个index有没有越界? //修改次数自增处理 modCount++; //把指定index上的元素交给oldValue E oldValue = elementData(index); //要移动的长度 int numMoved = size - index - 1; if (numMoved > 0) //前移 System.arraycopy(elementData, index+1, elementData, index, numMoved); //将最后一个元素置空,使得被垃圾回收器回收,不会影响数组长度 elementData[--size] = null; // clear to let GC do its work return oldValue; }

根据对象remove

针对所有对象进行遍历,得到目标index,根据index去remove

public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++) if (elementData[index] == null) {
fastRemove(index); return true; } } else {
for (int index = 0; index < size; index++) if (o.equals(elementData[index])) {
//执行remove操作 fastRemove(index); return true; } } return false; }

fastRemove方法同样不会影响数组长度的!

private void fastRemove(int index) {
modCount++; int numMoved = size - index - 1; if (numMoved > 0) //index后面位置的元素统一前移 System.arraycopy(elementData, index+1, elementData, index, numMoved); //置空最后位置 elementData[--size] = null; // clear to let GC do its work }

contains方法

查看ArrayList是否包含有某一元素

//找到对应元素就返回true,否则return false    public boolean contains(Object o) {
//调用indexOf方法,遍历每个元素作对比 return indexOf(o) >= 0; }
//返回的值再去跟0作比较,从而得到true or false    public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else {
for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; }

clear方法

置空所有位置的元素,不影响数组容量!

public void clear() {
modCount++;//操作次数自增处理 // 循环遍历所有元素,置空处理 for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }

trimToSize方法

添加元素可能要扩容,但是删除元素并不会改变容量。如果要减少容量,就要调用该方法。

public void trimToSize() {
modCount++; if (size < elementData.length) {
elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }

interator方法

返回一个内部类

public Iterator
iterator() {
return new Itr(); }

使用iterator进行遍历,调用next的时候,修改次数与调用iterator方法时候(这回期望修改次数已经确定,之后add、remove会改变modCount)的修改次数不一致,就会发生ConcurrentModificationException

@SuppressWarnings("unchecked")        public E next() {
checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; }
final void checkForComodification() {
//修改次数和期望修改次数不等,就要抛出这个异常了 if (modCount != expectedModCount) throw new ConcurrentModificationException(); }

Arrays.copyOf方法

底层调用System.arraycopy实现

//基本数据类型public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength]; System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }

为啥ArrayList线程不安全?

先猜一下,应该是两个线程在操作,一个修改了,另一个没统计上…这里得重提一下两个重要的成员变量了:

transient Object[] elementData;//ArrayList基于动态数组实现,用此保存数据,容量就是该数组的长度
private int size;//保存当前数组中已经添加了多少元素
不知道大家还记不记得,当初我们add的时候,要先判断空间够不够,不够就扩容,然后再把这个元素添加进去…
总结一下就是:1.检查elementData容量是否够大;2.在elementData对应位置上添加值;
首先是对elementData容量的判断,再多线程情景下,线程A和线程B同时调用add方法。如果线程A判断需要扩容,且数组真的扩容后,若线程B判断不需扩容,此时就会抛出数组越界的异常。
第二个就是elementData[size++] = e,这个操作可不是原子操作,是分为两步的:1.elementData[size] = e;2.size = size + 1;在多线程的环境中,比方说线程A要在index=0的位置上放元素,碰巧线程B也要往这添加,更巧的是size=0;于是乎,线程B也把元素放在了index=0的位置上。理想情况是index=0的位置为元素1,index=1的位置放的是元素2。但是实际可不是这样,这样的话,就gg了…

如何使ArrayList线程安全?

1.使用synchronized

2.Collections.synchronizedList(),比方:
List list = Collections.synchronizedList(new ArrayList());

上一篇:这个坑
下一篇:mysql中没有boolean,而是tinyint

发表评论

最新留言

初次前来,多多关照!
[***.217.46.12]2025年04月03日 03时02分24秒