Java 集合深入理解 :ArrayList源码解析,及动态扩容机制
Posted 踩踩踩从踩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合深入理解 :ArrayList源码解析,及动态扩容机制相关的知识,希望对你有一定的参考价值。
文章目录
简介:
- ArrayList是java集合框架很常用数据结构;
- 实现list接口,继承 AbstractList ,基于数组实现容量大小动态变化
- 支持指针快速访问、浅拷贝
- 允许 null 元素的存在
- 实现RandomAccess、Cloneable、Serializable 接口 ,arraylist是支持可序列化的。
关键方法时间复杂度
- get() 直接读取第几个下标,复杂度 O(1)
- add(E) 添加元素,直接在后面添加,复杂度O(1)
- add(index, E) 添加元素,在第几个元素后面插入,后面的元素需要向后移动,复杂度O(n)
- remove()删除元素,后面的元素需要逐个移动,复杂度O(n)
代码示例
public static void main(String[] args) {
System.out.println("--------------ArrayList--------------");
ArrayList<Integer> v=new ArrayList<Integer>();
v.add(1);
v.add(2);
v.add(3);
v.add(7);
v.add(5);
v.add(6);
v.stream().forEach(m->{
System.out.println(m);
});
}
--------------ArrayList--------------
1
2
3
7
5
6
动态数组实现的集合,能保证数据插入顺序和取出顺序,不保证多线程情况下数据的安全性
全篇注释
/**
* 可调整大小的数组实现集合接口。实现所有可选的集合操作
*,并允许所有元素,包括null元素。除了实现list接口
*这个类提供了一些方法来操纵数组的大小
*在内部用于存储集合数据(这个类大致相当于
*Vector,但不同步(unsynchronized)。)
*The size,isEmpty,get,set,迭代器(iterator)和列表迭代器(listIterator)操作
*以常量运行时间。add操作 摊销固定时间内运行,
*也就是说,添加n个元素需要O(n)个时间。所有其他操作
*以线性时间运行(粗略地说)。常数因子较低
*对于LinkedList实现。
*<p>每个<tt>ArrayList</tt>实例都有一个<i>容量(capacity)</i>。容量为
*用于存储列表中元素的数组的大小。总是这样
*至少和集合列表大小一样大。当元素添加到ArrayList时,
*它的容量自动增长。增长政策的细节并不清楚
*在添加一个元素的过程中有一个固定的摊销
*时间成本。
*<p>应用程序可以增加<tt>ArrayList</tt>实例的容量
*在使用ensureCapacity添加大量元素之前
*操作。这可能会减少增量重新分配的数量。
*<p><strong>请注意,此实现不同步。</strong>
*如果多个线程同时访问<tt>ArrayList</tt>实例,
*至少有一个线程在结构上修改列表,它
*<i>必须从外部同步(一个结构修改是
*添加或删除一个或多个元素或显式调整背衬阵列的大小;
* 仅仅设置一个元素的值是不够的
*这通常是通过在自然封装列表的某个对象上进行同步。
*如果不存在这样的对象,则应该使用
*{@link Collections#synchronizedList集合.synchronizedList}
*方法。这最好在创建时完成,以防止意外
*对列表的非同步访问:<pre>
*List=Collections.synchronizedList(新ArrayList(…))</预处理>
*这个类迭代器的{@link#iterator()iterator}和
*{@link#lisiterator(int)lisiterator}方法是应用于快速故障的:</a>
*如果在迭代器运行后的任何时候列表在结构上被修改
*以任何方式创建,除了通过迭代器自己的
*{@link ListIterator#remove()remove}或
*{@link ListIterator#add(Object)add}方法,迭代器将抛出
*{@link ConcurrentModificationException}。因此,面对
*并发修改时,迭代器会快速而干净地失败,而不是
*而不是冒着武断的、不确定的行为
*未来的时间。
*<p>请注意,不能保证迭代器的快速故障行为
*一般说来,不可能在未来作出任何硬性保证
*存在未同步的并发修改。故障快速迭代器
*尽最大努力抛出{@code ConcurrentModificationException}。
*因此,编写依赖于此的程序是错误的
*其正确性例外:<i>迭代器的快速失败行为
*应仅用于检测错误。</i>
*<p>这个类是
*<a href=“{@docRoot}/./technotes/guides/collections/index.html”>
*Java集合框架</a>。
* @author Josh Bloch
* @author Neal Gafter
* @see Collection
* @see List
* @see LinkedList
* @see Vector
* @since 1.2
*/
整段注释解释arrylist的整个功能
- 可调整大小的数组实现集合接口。实现所有可选的集合操作
- 解释arraylist不支持并发
- 不支持变迭代边操作
- 列表迭代器(listIterator)操作
- 面对并发修改时,迭代器会快速而干净地失败
- Collections.synchronizedList 从而达到数据安全
- 迭代器的快速失败行为应仅用于检测错误
接口继承
- 继承abstractlist 获得一些 addAll sort sublist等基本操作。
- serializable 可被序列化
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
成员属性
- DEFAULT_CAPACITY 默认初始容量 为10 这是在 改变数组大小时初始化
/**
* Default initial capacity. 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
- EMPTY_ELEMENTDATA 空实例的共享空数组实例 在 构造方法中给定初始化容量,容量等于0时,创建该空数组实例
/**
* Shared empty array instance used for empty instances. 用于空实例的共享空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA 将其与 EMPTY_ELEMENTDATA数据区分开来,这在无参构造方法中设置数据,并在添加第一个元素时,进行扩展为 DEFAULT_CAPACITY
/**
* *用于默认大小的空实例的共享空数组实例。我们
*将其与 EMPTY_ELEMENTDATA数据区分开来,以了解何时
*添加第一个元素
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
- elementData 存储ArrayList元素的数组缓冲区
/**
* 存储ArrayList元素的数组缓冲区。
*ArrayList的容量是此数组缓冲区的长度。任何
* empty ArrayList 在elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA
*当添加第一个元素时,将扩展到DEFAULT_CAPACITY。
*/
transient Object[] elementData; // 非私有以简化嵌套类访问
- size 包含的元素 并不是整个length的长度
/**
* The size of the ArrayList (the number of elements it contains). ArrayList的大小(它包含的元素数)
* @serial
*/
private int size;
transient 非私有以简化嵌套类访问
被transient修饰的变量不参与序列化和反序列化;而且arraylist是实现了Serializable说明他能够被序列化和反序列化传输的;后来查阅了一些资料了解到:
,假如elementData的长度为10,而其中只有5个元素,那么在序列化的时候只需要存储5个元素,而数组中后面5个元素是不需要存储的。于是将elementData定义为transient,避免了Java自带的序列化机制,并定义了两个方法writeObject和readObject,实现了自己可控制的序列化
什么是writeObject 和readObject?可定制的序列化过程
构造方法
ArrayList() 和ArrayList(int initialCapacity) 构造一个空的对象数组采用不同的默认数组。
在增加第一个元素时
- 数组为EMPTY_ELEMENTDATA 基于用户设置大小值进行1.5倍扩容 , 如果Listlist = new ArrayList<>(0);那就是基于你设置的大小0开始扩容,依次是0,1 ,2,3 ,4, 6这种1.5倍数扩容
/**
* Constructs an empty list with the specified initial capacity.
* 构造具有指定初始容量的空列表
* @param initialCapacity the initial capacity of the list 指定 initialCapacity集合的初始容量
* @throws IllegalArgumentException if the specified initial capacity
* is negative 如果指定的初始容量为复数
*/
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);
}
}
- 数组为默认空DEFAULTCAPACITY_EMPTY_ELEMENTDATA就会走基于默认值的大小10扩容进行1.5倍扩容。 默认初始化大小10扩容,依次是10,15,22,33这种1.5倍数扩容
EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA两个空数组的区别
/**
* 构造一个初始容量为10的空列表。 默认构建是一个空数组
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
ArrayList 传入Collection 的构造方法
- 将传入集合 直接转为数组赋值给elementData ,对于扩容 则也是 基于传入数据的容量进行 1.5倍数扩容
/**
*构造一个包含指定元素的列表集合,按集合的迭代器。
* @param c 要将其元素放入此列表的集合中
* @throws NullPointerException if the specified collection is null
*/
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;
}
}
trimToSize方法
该方法将数组大小压缩成为size, 这里有疑点 Arrays.copyOf(elementData, size) 方法把 主要作用将数组进行截取size的大小;从而达到将数组大小压缩成为size
/**
*将此<tt>ArrayList</tt>实例的容量修剪为
*列表的当前大小。应用程序可以使用此操作最小化
*<tt>ArrayList</tt>实例的存储。
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
clone浅复制
也就是复制出的对象还是原来那个对象
/**
*返回此<tt>ArrayList</tt>实例的浅层副本(这个
*不会复制图元本身。)
* @return a clone of this <tt>ArrayList</tt> instance
*/
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
toArray 方法
返回的时包含此列表中所有元素的数组 也就是转换为object数组
/**
*返回包含此列表中所有元素的数组
*按正确的顺序(从第一个元素到最后一个元素)。
*<p>返回的数组将是“安全的”,因为没有对它的引用
*由此列表维护(换句话说,这个方法必须分配
*新阵列)。因此,调用者可以自由地修改返回的数组。
*<p>此方法充当了基于数组和基于集合之间的桥梁
*API。
*@return一个数组,数组中包含此列表中的所有元素
*正确的顺序
*/
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
add方法
- 每次添加元素到集合中时都会先确认下集合容量大小 (ensureCapacityInternal)并判断是否扩容
- 给元素进行赋值 elementData size++ 位
/**
*将指定的元素追加到此列表的末尾。
*@param e元素添加到此列表
*@return<tt>true</tt>(由{@link Collection#add}指定)
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ensureCapacityInternal 确认内部容量方法
这里ensureCapacityInternal方法是公共方法,判断扩容的;在add(e)和add(int index, E element) 和 addAll 和readObject 方法一切需要扩容的方法上使用
调用calculateCapacity 方法 (计算容量)这里根据版本来判断,例如 jdk7版本 会有方法,在有些版本就直接写的逻辑
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
- 当集合为DEFAULTCAPACITY_EMPTY_ELEMENTDATA 空集合 时取 DEFAULT_CAPACITY 和
minCapacity 的最大值也就是 10 (指定容量大小和无参构造时会 分别 弄一个不同的空数组) - 不直接赋值默认容量形式;主要源于 该方法也会被 addall方法调用 从代码可以看出 传入的数据可能size大于默认容量的
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
ensureExplicitCapacity
- 操作对 modCount 自增 1,记录操作次数
- 当 minCapacity 大于 elementData 的长度,调用对grow方法集合进行扩容
- 这个方法主要进行操作相加 ,变相的证明只有对容量进行数据添加或删除的才调用该方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
grow 函数
- 获得老容量
int oldCapacity = elementData.length;
- 默认将扩容至原来容量的 1.5 倍,这里用右移一位就相当于老容量除以2
int newCapacity = oldCapacity + (oldCapacity >> 1);
- 但是扩容之后也不一定适用,有可能比较小,有可能比较大。
- 1.5倍扩容小于所需的最小容量小于minCapacity, minCapacity赋值给newCapacity 例如首次添加数据就有可能出现
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
- 大于MAX_ARRAY_SIZE( Integer.MAX_VALUE - 8),那就直接拿 newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE 来扩容。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
- 将新数组赋值给 elementData, Arrays.copyOf(elementData, newCapacity); 这里的作用就是将旧容量进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
add(int index, E element)
在指定位置插入指定元素
- rangeCheckForAdd 判断插入指针是否越界 等
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
- 调用ensureCapacityInternal 方法确定容量是否足够,并是否扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
- System.arraycopy 进行复制数据 这里有几个参数
- elementData 来源数组 index 开始复制原数据的指针 elementData 目标数组 index + 1 目标数组下标指针 size - index 复制多长数据
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
- 最后对size进行加1
size++;
remove(int index)方法
删除此列表中指定位置的元素。
- rangeCheck 判断inex是否超过数据的大小
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
- 获取 当前指针下的数据
E oldValue = elementData(index); //elementData[index] 获取数据
- 这里删除数据为保证顺序结构的特性 ,连续性 就需要移动删除指针后面的数据 ,这里求到需要移动的长度
int numMoved = size - index - 1;
- 这里在于移动删除指针后面的数据到前一位
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
- 这里主要是将原来数组指针下的数据设置为null,等待垃圾回收器进行回收
elementData[--size] = null; // clear to let GC do its work
解决ConcurrentModificationException问题
从 迭代器(iterator)入手
重写了AbstractList.Itr的优化的版本
/**
*按正确的顺序返回此列表中元素的迭代器。
*<p>返回的迭代器是<a href=“#fail fast”><i>fail fast</i></a>。
*@按正确顺序返回此列表中元素的迭代器
*/
public Iterator<E> iterator() {
return new Itr();
}
/**
* AbstractList.Itr的优化版本
*/
private class Itr implements Iterator<E> {
int cursor; //要返回的下一个元素的索引
int lastRet = -1; // 返回的最后一个元素的索引-1如果没有
int expectedModCount = modCount;
Itr() {}
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();
}
}
迭代器分析
- ArrayList 定义了一个内部类 Itr 实现了 Iterator 接口。
- 在 Itr 内部有三个成员变量。 cursor:代表下一个要访问的元素下标。 lastRet:代表上一个要访问的元素下标。
expectedModCount:代表对 ArrayList 修改次数的期望值,初始值为 modCount。
其中 hasNext 方法
下个索引已经是元素的大小(cursor != size)
next 方法
- 判断 expectedModCount 和 modCount 是否相等。
- 对 cursor 进行判断,看是否超过集合大小和数组长度。
- 将 cursor 赋值给 lastRet ,并返回下标为 lastRet 的元素。
- 将 cursor 自增 1,开始时,cursor = 0,lastRet = -1;每调用一次 next 方法, cursor 和 lastRet 都会自增 1。
remove 方法
-
判断 lastRet 的值是否小于 0
-
在检查 expectedModCount 和 modCount 是否相等
-
接下来是关键,直接调用 ArrayList 的 remove 方法删除下标为 lastRet 的元素。然后将 lastRet 赋值给
cursor ,将 lastRet 重新赋值为 -1,并将 modCount 重新赋值给 expectedModCount。 -
调用 ArrayList 的 remove,而不是 Itr 的 remove。会将 D E 两个元素直接往前移动一位,最后一位置空,并且
modCount 会自增 1。从 remove 方法可以看出 ,已经长度改变了所以会导致 校验时会抛出异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
解决办法 就是调用迭代器中 remove方法
因为在该方法中增加了 expectedModCount = modCount 操作。但是这个 remove 方法也有弊端。
- 只能进行remove操作,add、clear 等 Itr 中没有。
- 调用 remove 之前必须先调用 next。因为 remove 开始就对 lastRet 做了校验。而 lastRet 初始化时为
-1。 - next 之后只可以调用一次 remove。因为 remove 会将 lastRet 重新初始化为 -1
- 多线程也会导致问题出现
- 用另外线程安全的集合
java.util中list实现结构图
总结
整个arraylist其实比较简单的一个集合工具,虽然很简单,但是其中用了很多优化的东西 可以供给我们学习开发,以及数组动态扩容,总之解析完这篇源码还是很有收获的
以上是关于Java 集合深入理解 :ArrayList源码解析,及动态扩容机制的主要内容,如果未能解决你的问题,请参考以下文章
Java 集合深入理解 :ArrayList源码解析,及动态扩容机制
深入理解java集合框架之---------Arraylist集合