转载 ArrayList源码分析

一、ArrayList介绍

Java 集合框架主要包括两种类型的容器:

  • 一种是集合(Collection),存储一个元素集合。
  • 一种是图(Map),存储键/值对映射。

Collection 接口有 3 种子类型:List、Set 和 Queue,常用的实现类有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。
List 接口是一个有序的Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素在List中位置,类似于数组的下标)来访问List中的元素,第一个元素的索引为 0,而且允许有相同的元素,即存储一组不唯一,有序(插入顺序)的对象

什么是ArrayList?

  为弥补普通数组无法扩容的不足,ArrayList对数组进行了封装,使其可以动态的扩容和缩小长度。在ArrayList集合中可以存储任意类型的数据,ArrayList是线程不安全的。ArrayList对于元素的修改和查找效率高,对于元素的插入和删除效率低。

二、ArrayList源码分析

1.ArrayList的顶部注释:

ArrayList实现List接口,是可以调整大小的数组。实现List所有的操作和允许的元素,包括null。除了实现List接口外,ArrayList类还提供一些内部的方法类操作(用于存储列表的)数组大小。ArrayList类大致相当于Vector,除了它不是线程同步的。

对于size,isEmpty,get,set,iterator和listIterator操作以恒定时间运行。对于add操作以分摊的常量时间运行,即添加n个元素需要O(n)时间。所有的其他操作以线性时间运行(粗滤地说)。与LinkedList实现相比,常数因子较低。

每一个ArrayList实例都有一个容量。容量是用于存储列表中的元素的数组的大小。它始终至少与列表大小一样大。当元素被添加到ArrayList是,容量会自动增长。除了添加元素具有恒定的摊销时间成本这一事实之外,未指定增长策略的详细信息。

在使用ensureCapacity操作添加大量元素之前,应用程序可以增加ArrayList实例容量。这可能会减少增量重新分配的数量。

请注意,此实现不同步。 如果多个线程同时访问ArrayList实例,并且至少有一个线程在结构上修改了列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素的任何操作,或显式调整后备数组的大小;仅设置元素的值不是结构修改。)这通常通过同步一些自然封装的对象来实现。
如果不存在此类对象,则应使用{@link Collections#synchronizedList Collections.synchronizedList}方法“包装”该列表。 这最好在创建时完成,以防止对列表的意外不同步访问:List list = Collections.synchronizedList(new ArrayList(...)); 此类的{@link #iterator()iterator}和{@link #listIterator(int)listIterator}方法返回的迭代器是快速失败的:如果在创建迭代器后的任何时候对列表进行结构修改,则以任何方式 除了通过迭代器自己的{@link ListIterator #remove()remove}或{@link ListIterator #add(Object)add}方法之外,迭代器将抛出{@link ConcurrentModificationException}。 因此,在并发修改的情况下,迭代器快速而干净地失败,而不是在未来的未确定时间冒任意,非确定性行为的风险。 请注意,迭代器的故障快速行为无法得到保证,因为一般来说,在存在不同步的并发修改时,不可能做出任何硬性保证。 失败快速的迭代器会尽最大努力抛出{@code ConcurrentModificationException}。因此,编写依赖于此异常的程序以确保其正确性是错误的:迭代器的快速失败行为应仅用于检测错误。

2.类关系图如下:

  • Iterable:定义了集合内元素的循环。
  • Collection:集合类的老大,定义了一些常规操作。
  • AbstractCollection:老大的抽象实现类,实现了简单的方法。

ArrayList类的定义:

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • ArrayList<E>:ArrayList存储数据的类型是Object子类。
  • extends AbstractList:提供 List 接口的骨干实现,最大限度地减少了实现由“随机访问”数据存储(如数组)支持的接口所需的工作量。
  • implements List:是ArrayList的父接口。
  • implements Serializable:标志着类对象可以进行序列化。
  • implements RandomAccess:支持随机访问。
  • implements Cloneable:表明对象可以调用克隆方法clone()进行克隆。

3.类成员变量:

/**
* 默认初始化容量
*/
private static final int DEFAULT_CAPACITY = 10; /**
* 如果自定义容量为0,则会默认用它来初始化ArrayList。或者用于空数组替换。
*/
private static final Object[] EMPTY_ELEMENTDATA = {}; /**
* 如果没有自定义容量,则会使用它来初始化ArrayList。或者用于空数组比对。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /**
* 这就是ArrayList底层用到的数组 * 非私有,以简化嵌套类访问
* transient 在已经实现序列化的类中,不允许某变量序列化
*/
transient Object[] elementData; /**
* 实际ArrayList集合大小
*/
private int size; /**
* 可分配的最大容量
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

4.构造方法:

/**
* 构造一个初始容量为10的空列表。
* 默认为一个空数组,当有元素被加入时,会被初始化为10。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造具有指定初始容量的空列表。。
* 建立一个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);
}
} /**
* 按照集合的迭代器返回的顺序构造一个包含指定集合元素的列表。
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 替换空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}

5.核心方法:

trimToSize()方法:

/**
* 这个方法用来最小化实例存储。
* 将容器大小调整为当前元素所占用的容量大小。
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}

clone()方法:

/**
* 用来克隆出一个新数组。
*/
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// 这不应该发生,因为我们是克隆的
throw new InternalError(e);
}
}

add(E e)方法:

/**
* 在数组末尾添加元素
*/
public boolean add(E e) {
// 确保容量可用
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
} private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
} /**
* 计算容量
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果elementData为空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
} /**
* 确保容量可用
*/
private void ensureExplicitCapacity(int minCapacity) {
// 计算修改次数
modCount++; // 如果size+1 > elementData.length,说明数组已经放满,则增加容量
if (minCapacity - elementData.length > 0)
grow(minCapacity);
} private void grow(int minCapacity) { int oldCapacity = elementData.length;
// 1.5倍扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量比最大值还要大,则将新容量赋值为VM要求最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将elementData拷贝到一个新的容量中
elementData = Arrays.copyOf(elementData, newCapacity);
}

size+1代表的含义是:

  • 如果集合添加元素成功后,集合中的实际元素个数。
  • 为了确保扩容不会出现错误。

假如不加一处理,如果默认size是0,则0+0>>1还是0。如果size是1,则1+1>>1还是1。
有人问:不是默认容量大小是10吗?事实上,jdk1.8版本以后,ArrayList的扩容放在add()方法中。之前放在构造方法中。我用的是1.8版本,所以默认ArrayList arrayList =

add(int index, E element)方法:

public void add(int index, E element) {
// 越界异常检测方法
rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
} private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
} /**
* Object src : 原数组
* int srcPos : 从元数据的起始位置开始
* Object dest : 目标数组
* int destPos : 目标数组的开始起始位置
* int length : 要copy的数组的长度
*/
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

set(int index,E element)方法:

public E set(int index, E element) {
rangeCheck(index); E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
/**
* 覆盖旧值并返回。
*/
E elementData(int index) {
return (E) elementData[index];
}

indexOf(Object o)方法:

/**
* 根据Object对象获取数组中的索引值。
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
// 如果o为空,则返回数组中第一个为空的索引
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

get(int index)方法:

/**
* 返回指定下标处的元素的值
*/
public E get(int index) {
// 检测index值是否合法,如果合法则返回索引对应的值
rangeCheck(index); return elementData(index);
}

remove(int index)方法:

/**
* 删除指定下标的元素
*/
public E remove(int index) {
// 检测index是否合法
rangeCheck(index);
// 数据结构修改次数
modCount++;
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;
}

三、总结

什么是数组?怎么访问数组的元素?数组在内存中是如何存储的?

  定义一个数组,只需指定一个长度即可。然后就可以通过变量名+索引(或者说下标)的形式访问数组元素了,下标不能超过数组长度,否则就会发生索引越界异常。
  比如数组a,长度是10,那么第一个元素就是a[0],最后一个就是a[9]。想访问哪个元素只要指定下标就可以了。像这种可以随意访问任何元素的,有个专用名词叫做随机访问。那我们来看下它在内存中是如何分布的,才支持随机访问。
  其实数组在内存中是一段连续的空间,你可以把它想象成一个梯子,一个格子紧挨着一个格子。数组名,也就是这个a,指向了这个空间的起始处地址,也就是数组的第一个元素的地址,所以其实和a[0]指向的是同一个地方。但a和a[0]的含义不一样,a表示内存地址,a[0]表示这个地址上存的元素
  这里的下标0其实指的是相对于起始处地址的偏移量。0表示没有偏移,所以就是起始处地址的那个元素,也即第一个元素。a[1]表示相对于起始处地址偏移量为1的那个元素,实际可以认为底层执行的是*(a + 1)。a+1表示从起始地址开始向后偏移1个之后的地址,那么*(星号)的意思就是取出那个地址上存储的元素。因为向后偏移了1个,其实就是第二个,所以a[1]叫取出数组的第二个元素。
  因数组在内存中是一段连续的空间,所以不管访问哪个元素都是这两步,加上偏移量,然后取数据。这就是它支持随机访问的原因。说白了就是所有元素按顺序挨在了一起。也可以看出来,不管数组的长度是多长,访问元素的方式都是这两步,都在常量的时间内完成。所以按索引访问数组元素的时间复杂度就是O(1)

ArrayList与数组的关系是什么? 

  ArrayList只不过是数组的包装,因为数组在内存中分配时必须指定长度,且一旦分配好后便无法再增加长度,即不可能在原数组后面再接上一段的。ArrayList之所以可以一直往里添加,是因为它内部做了处理。当底层数组填满后,它会再分配一个更大的新的数组,把原数组里的元素拷贝过来,然后把原数组抛弃掉。使用新的数组作为底层数组来继续存储元素。

LinkedList与数组的关系是什么?

  LinkedList也实现了List接口,也可以按索引访问元素,表面上用起来感觉差不多,但是其底层却有天壤之别。
  与数组一下子分配好指定长度的空间备用不同,链表不会预先分配空间。而是在每次添加一个元素时临时专门为它自己分配一个空间。因为内存空间的分配是由操作系统完成的,可以说每次分配的位置都是随机的,并没有确定的规律。所以说链表的每个元素都在完全不同的内存地址上,那我们该如何找到它们呢?
  唯一的做法就是把每个元素的内存地址都要保存起来。怎么保存呢?那就让上一个元素除了存储具体的数据之外,也存储一份下一个元素在内存中的地址。整个就像前后按顺序依次相连的一条链,我们只要保存第一个元素的内存地址,就可以顺藤摸瓜找到所有的元素。这其实就像一个挖宝藏游戏,假设共10步,告诉你第一步去哪里挖。然后挖出一个字条,上面写着第二步去哪里挖。依次这样挖下去。第九步挖出字条后才知道宝藏的位置,然后第十步就把它挖出来了。可见为了得到宝藏必须这样一步一步挖下去。中间的任何一步都不能跳过,因为第十步宝藏的位置在第九步里放着呢,第九步的位置在第八步里放着呢,依次倒着下来就到了第一步的位置,而第一步的位置已经告诉你了。所以,数组更像是康庄大道、四平八稳。链表更像是曲径通幽、人迹罕至。一个像探险,步步为营。一个像回家,轻车熟路。
  可见按索引访问链表元素时,必须从头一个个遍历,而且链表越长,位置越靠后,所需花费的时间就越长。所以按索引访问链表元素的时间复杂度就是O(n),n为链表的长度。也说明了链表不支持随机访问。所以ArrayList就实现了RandomAccess(随机访问)接口,而LInkedList就没有。

最新文章

  1. SQL Server利用RowNumber()内置函数与Over关键字实现通用分页存储过程(支持单表或多表结查集分页)
  2. 疯狂java学习笔记之面向对象(八) - static和final
  3. animation of android (2)
  4. vim中的查找和替换
  5. zw版【转发&#183;台湾nvp系列例程】HALCON MirrorRegion (Delphi)
  6. 关于python中字典的一些总结
  7. 3DSoftRenderer
  8. DIR和dirent结构体
  9. python 时间戳 datetime string 转换
  10. 创建控制器的3种方式、深入了解view的创建和加载顺序
  11. C#程序遍历数组A中所有元素
  12. java并发容器(Map、List、BlockingQueue)
  13. ES5与ES6的小差异
  14. ElasticSearch查询 第四篇:匹配查询(Match)
  15. 【dfs】p1451 求细胞数量
  16. nodejs的某些api~(四)udp&amp;dns
  17. serving inference
  18. 入门dp总结
  19. 腾讯正式开源高性能超轻量级 PHP 框架 Biny
  20. 我读《大数据时代的IT架构设计》

热门文章

  1. python学习之老男孩python全栈第九期_数据库day001 -- 作业
  2. chrome跨域访问
  3. windows下生成上传git时需要用的SSH密钥
  4. 网络I/O模型--03非阻塞模式(ServerSocket与Socket的超时处理)--解除accept()、 read()方法阻塞
  5. OpenGL学习&mdash;04--彩色立方体
  6. 将ArcGIS Server的JSON转化为SHP文件
  7. Global Average Pooling Layers for Object Localization
  8. is_palindrome 回文递归
  9. C语言图形编程
  10. 深入理解abstract class和interface