剧透:ArrayList的插入操作与查询操作都比LinkedList快很多
ArrayList
特点
- 底层使用数组实现,查询速度快,增删改速度慢(理论上)
- 线程不安全
ArrayList常用方法
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author 张宝旭
*/
public class ArrayListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>(); // 创建ArrayList
list.add("北京"); // 添加元素
list.add("上海");
list.add("杭州");
list.add("沈阳");
list.add("东戴河");
System.out.println(list.toString()); // 打印集合
System.out.println(list.size()); // 获取集合大小
System.out.println(list.get(0)); // 获取指定下标的元素
list.remove(0); // 删除指定下标的元素
list.remove("上海"); // 删除指定元素
System.out.println(list.isEmpty()); // 判断集合是否为空
System.out.println(list.contains("北京")); // 判读集合中是否包含某个元素
// 迭代遍历集合元素
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
System.out.println();
// 增强for遍历
for (String value : list) {
System.out.print(value + " ");
}
}
}
对基本数据类型的操作
当向列表中添加基本数据类型的时候,会自动装箱成包装类型
List<Integer> lit = new ArrayList<>();
lit.add(12);
lit.add(25);
lit.add(99);
而当我们删除其中的指定元素时
lit.remove(12);
因为12是基本数据类型,所以这种删除方式删除的是下标为12的元素
所以需要将此删除语句更改,将12强转为包装类型
lit.remove((Integer)12);
这样就可以正确删除列表中为12的元素了
ArrayList源码分析
底层使用数组保存元素
transient Object[] elementData;
默认容量:10
private static final int DEFAULT_CAPACITY = 10;
ArrayList构造方法
当创建列表时,没有向列表中添加元素时,容量为0
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
add()方法(扩容)
1、当列表中没有元素时,添加一个元素后,容量扩充为10
public boolean add(E e) {
// 初始时,size为0,所以将1传入
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 最开始时,数组为空,所以条件成立
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 令minCapacity = 两者之间最大的数
// DEFAULT_CAPACITY为默认容量10,minCapacity传入的参数为1
// 所以minCapacity = 10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 然后调用此方法,传入参数为10
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 修改的次数,加一
modCount++;
// overflow-conscious code
// 最开始数组长度为0,所以 10-0 > 0 成立
if (minCapacity - elementData.length > 0)
// 调用此方法
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
// 原来的数组长度为0
int oldCapacity = elementData.length;
// 新的数组长度 = 原来的长度 + (原来的长度除2) = 0 + 0/2 = 0
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 0 - 10 < 0 成立
if (newCapacity - minCapacity < 0)
// 所以新数组长度赋值 = 10
newCapacity = minCapacity;
// 10 - 一个很大的数 > 0 不成立,所以不会执行
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 然后调用此方法进行数组复制
// 将原来的数组,给新的长度10,还复制到这个数组中,实现了数组扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
所以,到这里就实现了最开始添加一个元素时,数组扩容为10的操作
然后再接着执行add()方法剩下的操作,将值添加到数组中,size大小加一
再次梳理一遍流程
- 首先调用add()方法
- 然后调用ensureCapacityInternal()方法,判断列表中元素是否为空,如果为空,则进行赋值扩容参数为10
- 然后调用ensureExplicitCapacity()方法,判断最小的容量是否大于数组的长度。如果不大于,则无需扩容,如果大于,则进行扩容,调用grow(minCapacity)方法
- 然后调用的grow(minCapacity)方法,给出扩容的长度,然后用数组复制的方法进行扩容
- 最后接着执行add()方法,将值添加到数组中,size大小加一
2、当列表中有元素时,但是没有超过数组容量的大小时,向列表中添加元素
(假如此时集合中有一个元素,size=1, 数组长度为10)
// 首先调用add方法,进行添加
public boolean add(E e) {
// 判断是否需要扩容,传入参数为2
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
// 2 - 10 > 0,条件不成立,不向下执行
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
最后接着执行add()方法
// 将size的位置添加新的元素e,然后长度size加一
elementData[size++] = e;
3、当列表中有元素,而且长度已经满了,需要进行第二次扩容的时候
(假如当前数组大小size = 10)
// 先调用add()方法进行添加
public boolean add(E e) {
// 进行扩容,传入参数为11
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);
}
// 直接调用此方法,传入参数为11
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 修改次数加一
modCount++;
// overflow-conscious code
// 11 - 10 > 0,条件成立,调用grow方法
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
// 原来数组长度 = 10
int oldCapacity = elementData.length;
// 新数组长度 = 10 + (10 / 2) = 15
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 15 - 11 < 0 条件不成立
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 15 - 很大的数 > 0 条件不成立
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 所以直接执行此方法,将原数组复制,还复制到原数组中(相当于覆盖)
// 复制长度为15,这样就实现了数组扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
最后接着执行add()方法,将元素添加到后面,然后size加一
由此代码可知
int newCapacity = oldCapacity + (oldCapacity >> 1);
在除第一次添加元素的时候进行扩容,每次扩容大小是原来的1.5倍(向右移一位就是除2)
remove()方法
假如有10个元素,要删除下标为3的元素
public boolean remove(Object o) {
// 先判断传入的对象是否为null
if (o == null) {
// 如果为null,就再遍历数组,查找有没有值为null的元素
for (int index = 0; index < size; index++)
// 当找到值为null的元素时,就进行快速删除,然后返回true,表示删除成功
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 如果传入的对象不是null,也进行遍历
for (int index = 0; index < size; index++)
// 在数组中找到和此对象相匹配的
if (o.equals(elementData[index])) {
// 然后使用快速删除方法,删除找到的下标为index的元素
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
// 修改次数加一
modCount++;
// 元素需要移动的次数 10 - 3 - 1 = 6
// 0 1 2 3 4 5 6 7 8 9
// 因为3位置的后面有6个元素,删除之后,这6个元素都要向前移动一位
int numMoved = size - index - 1;
// 如果需要移动元素的时候
if (numMoved > 0)
// 使用数组复制的方法,从index+1的位置开始,将后面的元素向前移动一位(也就是向前覆盖)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 移动之后,需要将最后一个元素的值清空,然后将size减一,则完成删除操作
elementData[--size] = null; // clear to let GC do its work
}
LinkedList
特点
- 底层使用链表方式实现,插入删除速度快(理论上),查询速度快
- 可以用LinkedList来实现栈、队列等数据结构
LinkedList常用方法
import java.util.*;
/**
* @author 张宝旭
*/
public class LinkedListTest {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("I"); // 添加元素
linkedList.add("Love");
linkedList.add("You");
linkedList.add("Very");
linkedList.remove("Very"); // 删除指定元素
linkedList.size(); // 获取列表长度
linkedList.get(0); // 获取指定下标的元素
linkedList.contains("Love"); // 判断是否包含指定元素
linkedList.indexOf("I"); // 获取指定元素的下标
linkedList.set(1, "love"); // 修改指定下标的元素
// 将列表转换成数组
String[] strings = linkedList.toArray(new String[0]);
System.out.println(Arrays.toString(strings));
// for循环遍历
for(int i = 0; i < linkedList.size(); i++) {
System.out.print(linkedList.get(i) + " ");
}
System.out.println();
// 增强for遍历
for (String s : linkedList) {
System.out.print(s + " ");
}
System.out.println();
// 迭代器遍历
Iterator<String> iterator = linkedList.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
System.out.println();
// 列表迭代器遍历 (反向遍历)
ListIterator<String> listIterator = linkedList.listIterator(linkedList.size());
while (listIterator.hasPrevious()) {
System.out.print(listIterator.previousIndex() + " : " + listIterator.previous() + " ");
}
}
}
LinkedList源码分析
基本属性
构造方法
public LinkedList() {
}
链表长度
transient int size = 0;
头指针
transient Node<E> first;
尾指针
transient Node<E> last;
add()方法
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
// 创建一个临时的指针,指向last,也就是l就是最后一个结点
final Node<E> l = last;
// 创建一个结点,前驱指针指向l,元素为e,后继结点指向null
final Node<E> newNode = new Node<>(l, e, null);
// 令last指针指向新结点
last = newNode;
// 当l为空时,说明链表为空,直接添加,令头指针指向新结点
// 此时只有一个结点,last指向它,first也指向它
if (l == null)
first = newNode;
else
// 当l不为空时,说明链表中有元素,所以将l的后继指针指向新结点
// 此时就完成了双向链表的连接(第二部完成了前驱指针指向l)
l.next = newNode;
size++;
modCount++;
}
remove()方法
public boolean remove(Object o) {
// 先判断要删除的元素是否是null
if (o == null) {
// 如果是,就遍历链表,查找结点值为null的结点
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
// 找到之后调用此方法删除元素
unlink(x);
return true;
}
}
} else {
// 如果要删除的元素不是null,也遍历链表,查找此元素
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
// 找到之后调用此方法删除元素
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
// 取出结点的值、后继结点、前驱结点
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 如果前驱结点为空,说明要删除的是头结点
if (prev == null) {
// 所以直接将头指针指向它的后一个,就把原来的头结点跳过了
first = next;
} else {
// 否则要删除的就是中间结点
// 那么久将那个要删除结点的前驱结点的后继指针指向要删除元素的后继结点
// 也就是将它的前面指向它的后面,就把它自己跳过了
prev.next = next;
// 然后令要删除元素的前驱结点为空,释放了这跳前驱指针
x.prev = null;
}
// 上面是修改前驱,下面是修改后继
// 当要删除元素的后继结点为空是,此结点就是尾结点
if (next == null) {
// 所以直接领其尾指针指向它的前一个就把最后一个结点跳过了
last = prev;
} else {
// 否则就是删除中间结点
// 将那个结点的后一个结点的前驱指针指向那个结点个前驱结点,实现了将它自己跳过
next.prev = prev;
// 然后将那个结点的后继指针置空,释放这条指针
x.next = null;
}
// 前驱指针释放了,后继指针释放了,然后现在释放此结点的值,将值置空
x.item = null;
size--; // 链表长度减一
modCount++; // 修改次数加一
return element; // 返回删除的元素
}
测试ArrayList和LinkedList的性能
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* 测试ArrayList和LinkedList的性能。
*
* @author 张宝旭
*/
public class PerformanceTest {
// 一百万次
static final int QUERY_COUNT = 10000000;
static List<String> arrayList = new ArrayList<>();
static List<String> linkedList = new LinkedList<>();
public static void main(String[] args) {
// 插入
PerformanceTest.arrayListInsert();
PerformanceTest.linkedListInsert();
// 查询
PerformanceTest.arrayListQuery();
PerformanceTest.linkedListQuery();
}
/**
* 测试ArrayList插入性能
*/
public static void arrayListInsert() {
long startTime = System.currentTimeMillis();
for(int i = 0; i < QUERY_COUNT; i++) {
arrayList.add(String.valueOf(i));
}
long endTime = System.currentTimeMillis();
System.out.println("ArrayList插入消耗时间: " + (endTime - startTime));
}
/**
* 测试LinkedList插入性能
*/
public static void linkedListInsert() {
long startTime = System.currentTimeMillis();
for(int i = 0; i < QUERY_COUNT; i++) {
linkedList.add(String.valueOf(i));
}
long endTime = System.currentTimeMillis();
System.out.println("LinkedList插入消耗时间: " + (endTime - startTime));
}
/**
* 测试ArrayList查询性能
*/
public static void arrayListQuery() {
long startTime = System.currentTimeMillis();
for(int i = 0; i < QUERY_COUNT; i++) {
arrayList.get(i);
}
long endTime = System.currentTimeMillis();
System.out.println("ArrayList查询消耗时间: " + (endTime - startTime));
}
/**
* 测试LinkedList性能
*/
public static void linkedListQuery() {
long startTime = System.currentTimeMillis();
for(int i = 0; i < QUERY_COUNT; i++) {
linkedList.get(i);
}
long endTime = System.currentTimeMillis();
System.out.println("LinkedList查询消耗时间: " + (endTime - startTime));
}
}
结果
ArrayList插入消耗时间: 96
LinkedList插入消耗时间: 1684
ArrayList查询消耗时间: 8
LinkedList查询消耗时间: 很久,没有执行完
结论
虽然ArrayList底层是数组,LinkedList的底层是链表,但是ArrayList的插入操作,确实要比LinkedList的插入操作效率更高
因为ArrayList底层使用了 elementData = Arrays.copyOf(elementData, newCapacity);
这是一个native方法,所以效率比较高
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
当修改执行次数的时候,之前测试是一百万次,现在修改为10000次
ArrayList插入消耗时间: 3
LinkedList插入消耗时间: 2
ArrayList查询消耗时间: 1
LinkedList查询消耗时间: 48
可以看到,当执行次数小的时候,两者的插入操作差别并不明显,但是查询操作,还是ArrayList更快
当执行次数大的时候,ArrayList的插入效率要高于LinkedList
总结
ArrayList底层使用数组实现,LinkedList底层使用链表实现
ArrayList的查询操作比LinkedList的查询操作快
理论上:ArrayList的插入操作比LinkedList的插入操作慢
实际上:ArrayList的插入操作比LinkedList的插入操作快
因为ArrayList底层使用了
elementData = Arrays.copyOf(elementData, newCapacity);
这是一个native方法,所以效率比较高