JDK源码分析(一)——ArrayList
ArrayList分析
JDK api对ArrayList是这样介绍的:
Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)
大意是ArrayList是List接口的可变长度数组形式的实现,允许插入包括Null在内的所有元素,大致相当于Vector类,但ArrayList类不是线程安全的。
ArrayList继承结构
ArrayList是List的数组形式实现,所以实现了List接口,而List又从属于整个大的Colletction集合内,所以Collection是ArrayList的父接口,ArrayList实现了Iterable接口,可以使用迭代器和for-each循环对ArrayList对象进行遍历。Cloneable接口,允许对对象进行拷贝,Serializable接口,序列化相关接口,RandomAccess是一个标记接口,用于标明实现该接口的List支持快速随机访问,主要目的是使算法能够在随机和顺序访问的list中表现的更加高效。AbstractList和AbstractCollection提供了List接口和Collection接口的骨干实现。
ArrayList字段属性
//序列化ID
private static final long serialVersionUID = 8683452581122892189L;
//初始化ArrayList对象时Object数组默认的长度,是不可变常量
private static final int DEFAULT_CAPACITY = 10;
//空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//使用无参的构造方法时,Object数组会用这个常量初始化
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList存储数据的底层数组,transient修饰表示字段不会被序列化
transient Object[] elementData;
//存储元素的个数
private int size;
ArrayList构造函数
无参构造函数
在使用无参构造器初始化ArrayList对象的时候,底层的Object数组用DEFAULTCAPACITY_EMPTY_ELEMENTDATA常量数组来初始化,可以看到这是一个空数组,JDK的注释 Constructs an empty list with an initial capacity of ten,但实际上初始化的是一个空数组,可能早期的会初始一个容量为10的数组。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
有参构造函数
这是带有int整形参数的构造器,参数的作用也很明显,作为底层的Object数组的初始容量。initialCapacity参数大于0,初始化一个容量为initialCapacity的Object数组;initialCapacity等于0,则初始化为空数组;小于0,抛出IllegalArgumentException异常。
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对象,包含传入集合对象的元素,方法的实现先把集合对象c转换为数组,用这个数组来初始化Object数组elementData,到这里为什么似乎size = elementData.length再判断一下size是否为0就结束了,然而程序里还要判断这个elementData数组的类型是不是Object数组,这是为什么呢?这是因为toArray这个方法由于实现方式不一样,可能返回的不是Object数组,而可能是String数组等等,那么elementData就会向下转型,它就不再是Object数组了,如果不是Object类型,使用Arrays.copy()复制,并指定类型为Object。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
重要方法
add(E e)
使用add方法插入一个对象,首先得确保数组还能放一个对象,所以先执行ensureCapacityInternal(size + 1),看看size+1有没有出边界,如果大于elementDate数组长度,那就需要对数组进行扩容,否则就会超出边界报错了,当minCapacity - elementData.length > 0是就会执行grow方法扩容,扩容语句int newCapacity = oldCapacity + (oldCapacity >> 1);可以看到新的容量是以前的容量的1.5倍(>>1 右移一位,除以2),然后再把以前数组的元素拷贝到新数组中。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
add(int index, E element)
add(int index, E element)方法带有两个参数,第一个参数是要插入的位置索引,第二个是要插入的元素,并且方法是无返回值的。
由于add(int index, E element)是插入指定位置的,所以要先检查索引index,插入之前执行rangeCheckForAdd(index);这个方法也很简单,就是判断如果index大于ArrayList的size(注意不是Object数组容量而是实际元素个数)或index小于0,抛出IndexOutOfBoundsException异常,否则说明index位置处可以插入,当然还是要确保还有空间插入对象,执行ensureCapacityInternal(size + 1)方法,使用System.arrayCopy()将从index-size这段元素复制到elementDate数组,从index+1处开始接收,实际上就是把elementData数组从index-size这段元素依次向后移一位,那么index处就可以插入新元素了。计算复制元素的长度,最后一个元素索引是size-1,从index处元素开始后移,及index-1后面的元素都要后移,所以length=size-1-(index-1)=size-index
这个插入并不是把index处元素覆盖了,它相当于一个“插队”的效果,把原来index及后面的元素挤到后面去了。
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));
}
get(int index)
get方法,根据传入的index参数获取对应位置的值。首先检查索引是否有效,返回索引位置的元素。可以看出,由于ArrayList底层是基于数组实现的,所以根据索引查找较方便,但是插入,尤其是在中间位置的插入就比较麻烦了,需要把后面的元素依次后移,这是由数组的特性决定的。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
set(int index, E element)
根据传入index参数设置索引位置的值,并返回之前的元素,实现的是一种“更新”的操作。 先检查索引,索引有效,保存先前的值,再把新传入的元素的值设置进去,再返回先前的值。
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
remove(int index)
根据传入参数index移除指定位置的元素,并返回移除的元素。
检查索引,索引有效,先保留index处的值用于返回,我们要移除index的值,那么就相当于index处空了,后面的元素需要一次向前移,实现这个依次前移,依然要使用System.arrayCopy方法,把自己本身的部分元素复制到自己的另一处位置,实际上是把index+1-size这一段元素复制到index-(size-1)处,实现依次前移的效果。我们前移是使用复制实现的,前面的元素被后一个的元素覆盖了,最后一个元素elementData(size-1)没有被覆盖,需要手动置为null。 最后一个元素的索引是size-1,那 numMoved=size-1-index,注意索引是从0开始的,而size表示的当前存储的元素个数,从1开始的
这里的modCount属性统计list修改次数,每次执行add或remove方法modCount加一。这个字段在迭代器遍历是检查有没有其他线程对ArrayList进行修改是用到,因为我们知道ArrayList没有同步方法。
public E remove(int 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;
}
remove(Object o)
根据传入对象移除,首先循环遍历去找这个元素,按照传入参数是null还是非null,如果找到了,记下这个下标,调用fastRemove方法,根据这个下标移除。
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])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
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
}
clear( )
把ArrayList存储的元素全部清除,注意并不是把elementData数组赋值为空数组,而是把数组每个元素置为空,方便GC工作。
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
trimToSize()
把elementData数组的容量压缩到和实际存储元素个数一致,我们知道底层的Object数组是有一个最大容量的,假如容量为10,但是我们只存放了3个元素,调用trimToSize就会数组压缩为一个容量(数组length)为3的新数组。 先判断size是否小于数组的length,小于的话,如果size为0,直接把空数组赋值给elementData;size不为0,复制有数据的那一部分给elementData。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
ArrayList Iterator迭代器
ArrayList通过内部类Itr实现了Iterator接口,我么在使用迭代器时,通过ArrayList的成员方法 Iterator方法返回一个迭代器对象,就可以使用迭代器了,分析一下这个内部类。
内部类字段属性
内部类Itr实现了Iterator接口,Itr没有提供构造方法,编译器回味Itr类提供无参构造方法,供外部的iterator方法调用。
cursor,表示下一个要迭代的元素的下标索引,初始默认为0;lastRet,代表当前元素的上一个元素的下标索引,初始为-1;expectedModCount,表示在迭代时ArrayList更改的次数,通过比较expectedModCount与实际的modCount是否一致来确定是否有其他线程修改了ArrayList,类似于乐观锁的CAS机制。
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
hasNext()
一般使用迭代器遍历时,都会使用hasNext作为while循环遍历条件,hasNext方法在ArrayList中还有元素没有遍历完就返回true,它会比较当前迭代元素下标cursor和ArrayList的size,不相等说明还没有遍历完。hasNext方法要配合next()方法一起使用,Next()方法会返回cursor指向的元素并把cursor加1。
next()
next()返回cursor指向的元素,并把cursor加1。
执行next方法,首先执行checkForComodification方法,检查expectedModCount与实际的modCount是不是相等,就是检查有没有其他线程来使用了这个ArrayList(具体来说是执行了add或remove修改方法),检查通过,再检测cursor是否有效,是否超出了size,最后将cursor加1,i赋值给lastRet,实际上相当于lastRet++,再将索引为i处的元素返回。
remove( )
我们在平时的使用知道,remove方法要在next方法后使用的,否则要报错,从remove的源码我们可以看到移除是根据lastRet参数,首先检查lastRet是否小于0(lastRet初始化-1,执行remove也会赋值为-1),确定lastRet大于等于0,使用remove(int index)方法移除元素,此时将游标cursor指向lastRet,lastRet赋值为-1。执行完remove方法,lastRet为-1,此时再执行remove肯定会在if (lastRet < 0)抛出异常,必须通过next方法让lastRet指向恢复正常。
总结
ArrayList作为我们实际开发中常用的集合类,它存储元素是基于底层的Object数组,ArrayList实现了动态扩容,每次插入元素会检查数组容量是否足够,每次扩容会扩大至原有容量的1.5倍,由于数组特性,ArrayList根据索引查找元素较快,增加元素,尤其是在中间插入删除元素较慢,因为都会涉及到数组的赋值,JDK为了提高效率复制方法使用System.arraycopy()是原生native方法,提高效率。另外从源码也可以看出,ArrayList是线程不安全的,在使用迭代器迭代时,如果有其他线程改变了modCount,就会抛出ConcurrentModificationException异常,涉及到多线程环境就不宜使用ArrayList。
JDK源码分析(一)——ArrayList的更多相关文章
- 【jdk源码分析】ArrayList的size()==0和isEmpty()
先看结果 分析源码 [jdk源码解析]jdk8的ArrayList初始化长度为0 java的基本数据类型默认值 无参构造 size()方法 isEmpty()方法
- jdk源码分析之ArrayList
ArrayList关键属性分析 ArrayList采用Object数组来存储数据 /** * The array buffer into which the elements of the Array ...
- 【JDK】JDK源码分析-ArrayList
概述 ArrayList 是 List 接口的一个实现类,也是 Java 中最常用的容器实现类之一,可以把它理解为「可变数组」. 我们知道,Java 中的数组初始化时需要指定长度,而且指定后不能改变. ...
- 集合源码分析[3]-ArrayList 源码分析
历史文章: Collection 源码分析 AbstractList 源码分析 介绍 ArrayList是一个数组队列,相当于动态数组,与Java的数组对比,他的容量可以动态改变. 继承关系 Arra ...
- JDK源码分析(2)LinkedList
JDK版本 LinkedList简介 LinkedList 是一个继承于AbstractSequentialList的双向链表.它也可以被当作堆栈.队列或双端队列进行操作. LinkedList 实现 ...
- 【JDK】JDK源码分析-Vector
概述 上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理.本文分析 List 接口的另一个实现类:Vector. Vector 的内部实现与 ArrayList 类似 ...
- 【JDK】JDK源码分析-List, Iterator, ListIterator
List 是最常用的容器之一.之前提到过,分析源码时,优先分析接口的源码,因此这里先从 List 接口分析.List 方法列表如下: 由于上文「JDK源码分析-Collection」已对 Collec ...
- 【JDK】JDK源码分析-CountDownLatch
概述 CountDownLatch 是并发包中的一个工具类,它的典型应用场景为:一个线程等待几个线程执行,待这几个线程结束后,该线程再继续执行. 简单起见,可以把它理解为一个倒数的计数器:初始值为线程 ...
- JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue
JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue 目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlocki ...
随机推荐
- SharePoint 项目的死法(三)
拙劣的供应商(团队) 坦率来说, 说这个原因需要一点勇气, 但在我从业的经历中, 充斥这大量的这样的案例, 没有什么实施经验的团队, 对产品几乎没什么了解的供应商, 三脚猫的开发人员,之前只会做做微软 ...
- soj1166. Computer Transformat(dp + 大数相加)
1166. Computer Transformat Constraints Time Limit: 1 secs, Memory Limit: 32 MB Description A sequenc ...
- bzoj 5055: 膜法师——树状数组
Description 在经历过1e9次大型战争后的宇宙中现在还剩下n个完美维度, 现在来自多元宇宙的膜法师,想偷取其中的三个维度为伟大的长者续秒, 显然,他能为长者所续的时间,为这三个维度上能量的乘 ...
- [洛谷P1228]地毯填补问题 题解(分治)
Description 相传在一个古老的阿拉伯国家里,有一座宫殿.宫殿里有个四四方方的格子迷宫,国王选择驸马的方法非常特殊,也非常简单:公主就站在其中一个方格子上,只要谁能用地毯将除公主站立的地方外的 ...
- 20165320 预备作业2:技能学习心得与C语言学习
一.技能学习心得 1.你有什么技能比大多数人好? 我觉得我的篮球打得比一般的人好吧,但是也仅仅掌握了大部分基本的篮球技巧,算不上精通. 2.针对这个技能的获取你有什么成功的经验? 我觉得要打好篮球需要 ...
- 使用Picker的时候,让input输入框使用焦点,手机键盘不弹出
$("#address").click(function(){ document.activeElement.blur(); })
- SQL 根据关联表更新主表中字段数据
今天遇到一个客户的数据更新问题,两个相关联的表,一个主表用于保存单据主要信息,一个副表用于保存单据的明细信息:现在要把主表的其中一个字段的数据更新到副表的一个字段中保存.精通的SQL语法的,当然是很简 ...
- 14 Go's Declaration Syntax go语言声明语法
Go's Declaration Syntax go语言声明语法 7 July 2010 Introduction Newcomers to Go wonder why the declaration ...
- js权威指南---学习笔记01
1.当函数赋值给对象的属性时,就变为了方法:2.被零整除不报错,只会返回无穷大(Infinity)或者负无穷大.例外:零除以零等于非数字(NaN).3.NaN与任何值都不相等! 4.Javascrip ...
- centos7.2下caffe的安装及编译
1.前期准备 安装依赖 sudo yum install protobuf-devel leveldb-devel snappy-devel opencv-devel boost-devel hdf5 ...