【学习笔记】JDK源码学习之ArrayList(附带面试题)
引言:
什么是 ArrayList
?它和 List
又有什么关系?两者又有什么区别?
带着以上问题让我们来深入走进 ArrayList
。
1、ArrayList的使用
demo:
//创建一个ArrayList数组
List<String> list = new ArrayList<>();
//向ArrayList数组中添加元素
list.add("Test");
list.add("test");
System.out.println(list);
//输出["Test","test"]
//获取ArrayList中的元素
list.get(index);
//删除ArrayList中的元素
list.remove();
//....
2、ArrayList的继承体系
继承图:
源码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//.....
}
从 继承图 和 源码 中我们能看见 ArrayList
实现了 List、RandomAccess、Cloneable、Serializable
等。
- 实现了
List
可以表明ArrayList
具有增删改查和遍历等操作。 - 实现了
RandomAccess
表明ArrayList
具有随机访问等能力。 - 实现了
Cloneable
表明ArrayList
可以被 浅拷贝 。 - 实现了
Serializable
表明ArrayList
可以被序列化。
3、实现List、RandomAccess、Cloneable、Serializable 的具体表现
实现Cloneable接口,可以进行浅拷贝。
创建一个实体类:
public class User {
private String name;
public User(){};
public User(String name){this.name = name;}
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
@Override
public String toString(){
return "User{" +
"name='" + name + '\'' +
'}';
}
}
拷贝ArrayList:
ArrayList list = new ArrayList<>();
User xiaobao = new User("xiaobao");
list.add("tset"); //添加字符类型
list.add(xiaobao); //添加对象类型
ArrayList clone = (ArrayList)list.clone();
System.out.println("两者地址对比" + (clone == list));
//两者里面的元素地址对比
System.out.println("两者元素值的对比" + (clone.get(1) == list.get(1)));
}
输出:
两者地址对比false
两者元素值的对比true
从上述结果我们可以发现:
- 如果进行拷贝其实是重新创建了一个新的
ArrayList
对象。两者间地址引用不相同。 - 但是两者间的元素引用地址是相同的。
- 而如果进行深拷贝时, 就会在list被克隆新创建克隆对象时,对其存储的复杂对象类型也进行对象新创建 ,复杂对象类型数据获得新的地址引用,而不是像浅拷贝那样,仅仅拷贝了复杂对象类型的地址引用。
实现RandomAccess接口:
一般实现 RandomAccess 时会提高列表的访问顺序。
接下来我们用 一个小 demo 来对比一下:
import java.util.ArrayList;
import java.util.Iterator;
public class demo02 {
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
for (int i = 0; i < 9999999; i++) {
list.add(i);
}
// 随机遍历
long Start = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
list.get(i);
}
long End = System.currentTimeMillis();
System.out.println("随机遍历时间:" + (End - Start));
// 有序遍历
long start = System.currentTimeMillis();
Iterator iterator = list.iterator();
while (iterator.hasNext()){
iterator.next();
}
long end = System.currentTimeMillis();
System.out.println("顺序遍历时间:" + (end - start));
}
}
输出:
随机遍历时间:2
顺序遍历时间:3
从上述结果和代码能看出来,随机遍历是比顺序遍历的效率是高一些的。
如果我们使用没有实现 RandomAccess 接口的 LinkedList
来测试一下:
demo:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
public class demo03 {
public static void main(String[] args) {
LinkedList list = new LinkedList<>();
for (int i = 0; i < 9999; i++) {
list.add(i);
}
// 随机遍历
long Start = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
list.get(i);
}
long End = System.currentTimeMillis();
System.out.println("随机遍历时间:" + (End - Start));
// 有序遍历
long start = System.currentTimeMillis();
Iterator iterator = list.iterator();
while (iterator.hasNext()){
iterator.next();
}
long end = System.currentTimeMillis();
System.out.println("顺序遍历时间:" + (end - start));
}
}
运行结果:
随机遍历时间:49
顺序遍历时间:1
可从上述对比和测试结果中能发现,没有实现 RandomAccess 的LinkedList
的访问是十分低的。
4、ArrayList中的变量和方法
4.1 基础变量
private static final int DEFAULT_CAPACITY = 10;//数组默认初始容量
private static final Object[] EMPTY_ELEMENTDATA = {};//定义一个空的数组实例以供其他需要用到空数组的地方调用
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//定义一个空数组,跟前面的区别就是这个空数组是用来判断ArrayList第一添加数据的时候要扩容多少。默认的构造器情况下返回这个空数组
transient Object[] elementData;//数据存的地方它的容量就是这个数组的长度,同时只要是使用默认构造器(DEFAULTCAPACITY_EMPTY_ELEMENTDATA )第一次添加数据的时候容量扩容为DEFAULT_CAPACITY = 10
private int size;//当前数组的长度
- DEFAULT_CAPACITY: 默认容量,默认为10,一般使用
new ArrayList()
创建 List 集合实例默认是10。 - EMPTY_ELEMENTDATA :空数组,一般可以通过
new ArrayList(0)
创建实例中使用的空数组。 - DEFAULTCAPACITY_EMPTY_ELEMENTDATA: 定义一个空数组,跟前面的区别 就是这个空数组是用来判断ArrayList第一添加数据的时候要扩容多少 。默认的构造器情况下返回这个空数组 。
- elementData: 数据存的地方它的容量就是这个数组的长度,同时只要是使用默认构造器(
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
)第一次添加数据的时候容量扩容为DEFAULT_CAPACITY = 10
4.2 三种构造方法
源码:
/**
* Constructs an empty list with the specified initial capacity.
*
* 构造具有指定初始容量的空列表。
* @param initialCapacity the initial capacity of the list 列表初始容量
* @throws IllegalArgumentException if the specified initial capacity
* is negative
* 传入初始容量,如果大于0就初始化elementData为对应大小,如果等于0就使用EMPTY_ELEMENTDATA空数组,
* 如果小于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);
}
}
/**
* Constructs an empty list with an initial capacity of ten.
* 构造一个初始容量为10的空列表。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
* 按照集合的迭代器返回元素的顺序构造一个包含指定集合元素的列表。
*
* @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) {
// 将构造方法中的集合参数转换成数组
Object[] a = c.toArray();
if ((size = a.length) != 0) {
// 检查c.toArray()返回的是不是Object[]类型,如果不是,重新拷贝成Object[].class类型
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
// 数组的创建与拷贝
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
4.3 扩容机制
大致流程图:
结合着上图,我们再来看看 ArrayList
中的源代码。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 判断是否需要扩容
elementData[size++] = e;
return true;
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 判断当前数组是否是默认构造方法生成的空数组,如果是的话minCapacity=10反之则根据原来的值传入下一个方法去完成下一步的扩容判断
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
// 快速报错机制
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
// ArrayList数组中最大的值为Integer的最大值- 8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
// 获取当前数组的长度
int oldCapacity = elementData.length;
// 把当前的数组长度扩展为之前的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 进行判断,如果扩容后还是小于所需长度,则把扩容长度变为最小长度。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果所需最小扩容长度大于ArrayList中最大的长度
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);
}
private static int hugeCapacity(int minCapacity) {
// 如果越界,则抛出 OutOfMemoryError 异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 判断如果最小值在 Integer.MAX_VALUE - 8 到 Integer.MAX_VALUE 范围就直接使用其中的范围值。
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
通过上述 流程图 和 源码 的结合,我们能发现一下核心点:
-
int newCapacity = oldCapacity + (oldCapacity >> 1);
扩容的计算方式。一看是1.5
倍,其实是不完全正确的。因为oldCapacity >> 1必须是整数。比如:第一次扩容:应该是 0 + 0/2 = 0 ,
然后通过 if (newCapacity - minCapacity < 0) newCapacity = minCapacity;将默认值10 赋值给newCapacity。所以无参构造第一次扩容到10。
第二次扩容:是 10 + 10/2 = 15;
第三次扩容: 15 +15/2 = 22; 注意15/2 = 7 ,不是7.5。
通过第三步扩容可以得知,扩容倍数不是1.5倍。实际上是 a = a+a/2。当a 为偶数时,才是1.5倍。
所以所说,只能是
约等于1.5倍
。 -
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}
判断当前数组是否是默认构造方法生成的空数组,如果是的话minCapacity=10反之则根据原来的值传入下一个方法去完成下一步的扩容判断。
4.4 其他方法
add(int index, E element):
首先我们来看看这个方法的源码:
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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++;
}
/**
* A version of rangeCheck used by add and addAll.
*/
private void rangeCheckForAdd(int index) {
// 如果越界则抛出异常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
- 首先
add
方法是会判断这个元素的下标会不会越界,如果越界则会抛出IndexOutOfBoundsException
异常。 - 然后在判断是否需不需要扩容。
- 如果过程中没有抛出异常的话,则正常进行元素的添加
elementData[index] = element
。
addAll(Collection<? extends E> c):
源码展示:
/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the
* specified collection's Iterator. The behavior of this operation is
* undefined if the specified collection is modified while the operation
* is in progress. (This implies that the behavior of this call is
* undefined if the specified collection is this list, and this
* list is nonempty.)
*
* @param c collection containing elements to be added to this list
* @return <tt>true</tt> if this list changed as a result of the call
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
// 将c集合转化为数组
Object[] a = c.toArray();
int numNew = a.length;
// 检查是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
// 将c中元素全部拷贝到数组的最后
System.arraycopy(a, 0, elementData, size, numNew);
// 集合中长度的大小增加c的大小
size += numNew;
// 如果c不为空就返回true,否则返回false
return numNew != 0;
}
- 拷贝c中的元素到数组a中;
- 检查是否需要扩容;
- 把数组a中的元素拷贝到elementData的尾部;
get(int index):
源码展示:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
- 首先判断
index
是否越界。 - 如果没有则返回
elementData
中的元素。
remove(int index)
源码展示:
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
// 检查index是否越界
rangeCheck(index);
modCount++;
// 获取index位置的元素
E oldValue = elementData(index);
// numMoved 向下减1
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;
}
- 检查索引是否越界;
- 获取指定索引位置的元素;
- 如果删除的不是最后一位,则其它元素往前移一位;
- 将最后一位置为null,方便GC回收;
- 返回删除的元素。
注意:从源码中得出,ArrayList删除元素的时候并没有缩容。
remove(E e):
源码展示:
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
* (if such an element exists). Returns <tt>true</tt> if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return <tt>true</tt> if this list contained the specified element
*/
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);
// 将最后一个元素删除,帮助GC
elementData[--size] = null; // clear to let GC do its work
}
- 首先判断需要删除的值是否为
null
- 如果删除的值是
null
则循环遍历elementData
数组,且使用==
来判断。 - 如果不为
null
,则也是循环遍历elementData
数组,但是本次判断则是需要使用equal()
方法来进行判断。 - 如果不存在该值,则返回
false
。
fastRemove
: 整个删除逻辑是,把index下标后面的元素全部复制提前一位,然后将最后一个元素置为null,自然就会被GC回收.
5、ArrayList常见面试题
5.1、 ArrayList的扩容机制是什么样子的?
5.2、 ArrayList是线程安全的吗?
5.3、 ArrayList 和 LinkedList 的区别?
5.4、 ArrayList 如何解决频繁扩容带来的效率问题?
5.5、 ArrayList插入或删除元素是否一定比LinkedList慢?
5.6、 如何复制某个ArrayList到另外一个ArrayList中去呢?你能列举几种?
答案:
地址