文章目录
Java 集合之 ArrayList
ArrayList 与 LinkedList 的区别?
-
- 都不是同步的,不保证线程安全
-
-
底层数据结构:
ArrayList
底层使用的是 数组LinkedList
底层使用的是 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
-
-
-
插入和删除是否受元素位置的影响:
-
ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响比如:执行
add(E e)
方法的时候,ArrayList
会默认将元素追加到该 List 的末尾,这种情况的时间复杂度就是 O(1) 。如果要在指定位置 i 插入和删除元素的话add(int index, E element)
,时间复杂度就是 O(n-i) ,因为在进行上诉操作的时候,集合中的第 i 元素及之后的 (n-i) 个元素都要执行向后/向前 移动一位的操作。 -
LinkedList
采用链表存储,所以对于add(E e)
方法的插入、删除元素时间复杂度都不受元素位置的影响,近似 O(1) ;如果是要在指定 i 位置插入和删除的话 ,时间复杂度近似为 O(n) ,因为需要先移动到指定 位置再插入
操作 ArrayList LinkedList get() 根据下标查询,顺序存储知道首个元素的地址,其他的位置很快就能确定,时间复杂度为O(1) 链式存储,从首个元素开始查找,直到查找到第 i个位置,时间复杂度为O(n) add(E e) 直接尾部添加,时间复杂度O(1) 直接尾部添加,时间复杂度O(1) add(index,E e) 顺序存储需要查找到元素然后执行插入或删除,时间复杂度为O(1)+O(n)=O(n); 链式存储同样需要先查找到元素然后在插入或删除,时间复杂度为O(n)+O(1)=O(n) remove(E) 顺序存储删除指定元素,后面元素要向前移动,时间复杂度O(n) 链式存储,直接 指针操作(找到前驱节点,再删除),时间复杂度O(1) -
-
-
- 是否支持快速随机访问:
LinkedList
不支持高效的随机访问,而ArrayList
支持。快速随机访问就是通过元素的索引快速获取元素对象(get(int index)方法),时间复杂度为O(1)
-
- 内存空间占用:
ArrayList
的空间浪费主要体现在 List 的结尾会预留一定的容量空间LinkedList
的空间花费则体现在它的每一个元素都要消耗比 ArrayList 更多的空间(因为要多存储一个直接后继和直接前驱的存储位置(指针域)。双向链表: 包含两个指针,⼀个prev指向前⼀个节点,⼀个next指向后⼀个节点。) -
补充内容: RandomAccess 接口
public interface RandomAccess { }
查看源码我们发现实际上
RandomAccess
接口中什么都没有定义。所以,在我看来,
RandomAccess
接口不过是一个标识罢了。标识什么?标识实现这个接口的类具有随机访问功能。在
java.util.Collections
的binarySearch()
方法中,它要判断传入的 List 是否RamdomAccess
的实例,如果是,调用indexedBinarySearch()
方法,如果不是,调用iteratorBinarySearch()
。binarySearch()
提供一个二分法查找的方法,当一个 List 支持高性能随机访问,或者大小小于二分查找阀值的时候,按下标二分查找;否则,用迭代器实现二分查找:public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) { if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD) return Collections.indexedBinarySearch(list, key); else return Collections.iteratorBinarySearch(list, key); }
ArrayList
实现了RandomAccess
接口,而LinkedList
没有实现,为什么呢? 我觉得还是和底层数据结构有关,ArrayList
底层是数组, 而LinkedList
底层是链表。数组天生支持随机访问,时间复杂度为 O(1),所以可以支持快速随机访问。链表需要遍历到特定的位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。ArrayList
实现了RandomAccess
接口,就表明了他具有快速随机访问功能,RandomAccess
接口只是标识,并不是说ArrayList
实现了RandomAccess
接口才具有快速随机访问功能的! -
再总结下 List 的遍历方式:
- 实现了
RandomAccess
接口的 list,优先选择普通 for 循环,其次 foreach - 未实现
RandomAccess
接口的list,优先选择 iterator 遍历(foreach 遍历底层也是通过 iterator 实现的),大 size 的List,千万不要使用普通 for 循环
- 实现了
ArrayList 和 Vector 区别呢? 为什么要用 ArrayList 取代 Vector?
Vector
类的所有方法都是同步的。可以由两个线程安全的访问一个 Vector 对象,但是一个线程访问 Vector 的话代码要在同步操作上耗费大量的时间。
ArrayList
不是同步的,所以在不需要保证线程安全时建议使用 ArrayList
说一说 ArrayList 的扩容机制吧
先从 ArrayList 的构造函数说起
ArrayList 有三种方式来初始化,构造方法源码如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;
/**
* 带初始容量参数的构造函数。(用户自己指定容量)
*/
public ArrayList(int initialCapacity) {
// 初始容量大于0
if (initialCapacity > 0) {
// 创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { // 初始容量等于0
// 返回空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {// 初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 默认构造函数,使用初始容量10构造一个空列表(无参数构造)
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
* 如果指定的集合为null,throws NullPointerException。
*/
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;
}
}
}
细心的同学一定会发现 :以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。 下面在我们分析 ArrayList 扩容时会讲到这一点内容!
一步一步分析 ArrayList 扩容机制
这里以无参构造函数创建的 ArrayList 为例分析
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
直接返回一个空数组
1. 先来看看 add()
方法
/**
* 将指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {
// 添加元素之前,先调用ensureCapacityInternal方法
ensureCapacityInternal(size + 1); // Increments modCount!!
// 这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
注意: JDK11 移除了
ensureCapacityInternal()
和ensureExplicitCapacity()
方法
2. 再来看看 ensureCapacityInternal()
方法
可以看到 add
方法 首先调用了ensureCapacityInternal(size + 1)
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
ensureCapacityInternal()
又调了 calculateCapacity()
// 计算容量 得到最小扩容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 当数组为空数组时
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值 (10,1)
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
当要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10。
3. ensureExplicitCapacity()
方法
如果调用 ensureCapacityInternal()
方法就一定会执行这个方法,下面我们来研究一下这个方法的源码!
// 判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
// 调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
我们来仔细分析一下:
- 当我们要 add() 进第一个元素到 ArrayList 时, elementData.length 为 0 (此时还是一个空的 list)
- 因为执行了
ensureCapacityInternal()
方法,所以 minCapacity (最小扩容量)为10。此时minCapacity - elementData.length > 0
(10-0)为 true ,所以会进入grow(minCapacity)
方法进行扩容为10。 - 当 add 第二个元素时,minCapacity 为2,此时 elementData.length(容量)在添加第一个元素后扩容为10了,所以
minCapacity - elementData.length > 0
(2-10)为 false,不会执行 grow() 扩容操作 - 添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。
- 直到添加第11个元素,minCapacity (11) 比 elementData.length(为10)要大。进入grow方法进行扩容。
4. grow()
方法
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) { //11
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length; 10
// 将oldCapacity 右移一位,其效果相当于oldCapacity /2,
// 我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1); //10 + (10 >> 1) = 15
// 然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
// 如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
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);
}
int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity为偶数就是1.5倍,否则是1.5倍左右)! 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数.
“>>”(移位运算符):>>1 右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源
我们再来通过例子探究一下grow()
方法 :
- 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入
hugeCapacity
方法。数组容量为10,add方法中 return true,size增为1。 - 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。
- 以此类推······
这里补充一点比较重要,但是容易被忽视掉的知识点:
- java 中的
length
属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. - java 中的
length()
方法是针对字符串说的,如果想看这个字符串的长度则用到length()
这个方法. - java 中的
size()
方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!
5. hugeCapacity()
方法。
从上面 grow()
方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity()
方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE
,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8
。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//对minCapacity和MAX_ARRAY_SIZE进行比较
//若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
//若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
System.arraycopy()
和 Arrays.copyOf()
方法
阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)
、toArray()
等方法中都用到了该方法!
System.arraycopy()
方法
/**
* 在此列表中的指定位置插入指定的元素。
* 先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
* 再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
// arraycopy()方法实现数组自己复制自己
// 源数组,源数组中的起始位置,目标数组,目标数组中的起始位置,要复制的数组元素的数量;
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
我们写一个简单的方法测试以下:
public class ArraycopyTest {
public static void main(String[] args) {
int[] a = new int[10];
int[] b = new int[10];
a[0] = 0;
a[1] = 1;
a[2] = 2;
a[3] = 3;
System.out.println("数组a的值" + Arrays.toString(a));
System.out.println("数组b的值" + Arrays.toString(b));
System.arraycopy(a, 2, b, 3, 3);
a[2]=99;
System.out.println("复制后数组a的值" + Arrays.toString(a));
System.out.println("复制后数组b的值" + Arrays.toString(b));
}
}
结果:
数组a的值[0, 1, 2, 3, 0, 0, 0, 0, 0, 0]
数组b的值[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
复制后数组a的值[0, 1, 99, 3, 0, 0, 0, 0, 0, 0]
复制后数组b的值[0, 0, 0, 2, 3, 0, 0, 0, 0, 0]
Array.copyOf()
方法
/**
* 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。
*/
public Object[] toArray() {
// elementData:要复制的数组;size:要复制的长度
return Arrays.copyOf(elementData, size);
}
个人觉得使用 Arrays.copyOf()
方法主要是为了给原有数组扩容,测试代码如下:
public class ArrayscopyOfTest {
public static void main(String[] args) {
int[] a = new int[3];
a[0] = 0;
a[1] = 1;
a[2] = 2;
System.out.println("a.length:" + a.length);
int[] b = Arrays.copyOf(a, 10);
System.out.println("b.length:" + b.length);
}
}
结果:
a.length:3
b.length:10
两者联系和区别
联系:
看两者源代码可以发现 copyOf() 内部实际调用了 System.arraycopy()
方法
区别:
arraycopy()
需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf()
是系统自动在内部新建一个数组,并返回该数组。
ensureCapacity
方法
ArrayList 源码中有一个 ensureCapacity
方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢?
/**
* 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
最好在 add 大量元素之前用 ensureCapacity
方法,以减少增量重新分配的次数
我们通过下面的代码实际测试以下这个方法的效果:
public class EnsureCapacityTest {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<Object>();
final int N = 10000000;
long startTime = System.currentTimeMillis();
for (int i = 0; i < N; i++) {
list.add(i);
}
long endTime = System.currentTimeMillis();
System.out.println("使用ensureCapacity方法前:"+(endTime - startTime));
}
}
运行结果:
使用ensureCapacity方法前:2158
public class EnsureCapacityTest {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<Object>();
final int N = 10000000;
list = new ArrayList<Object>();
long startTime1 = System.currentTimeMillis();
list.ensureCapacity(N);
for (int i = 0; i < N; i++) {
list.add(i);
}
long endTime1 = System.currentTimeMillis();
System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1));
}
}
运行结果:
使用ensureCapacity方法前:1773
通过运行结果,我们可以看出向 ArrayList 添加大量元素之前最好先使用ensureCapacity
方法,以减少增量重新分配的次数。