List是Java中比较常用的集合类,关于List接口有很多实现类,本文就来简单介绍下其中几个重点的实现ArrayList、LinkedList和Vector之间的关系和区别。Vector和ArrayList一样,都是通过数组实现的,其中的很多方法都通过同步(synchronized)处理来保证线程安全,所以我们重点讲解ArrayList和LinkedList这两种数据结构的区别。
集合和接口的关系
Collection接口是 (java.util.Collection)是Java集合类的顶级接口之一,整个集合框架就围绕一组标准接口而设计,Collection 接口有 3 种子类型集合: List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类。
数据结构
- ArrayList:ArrayList底层是基于数组实现的,可以认为ArrayList是一个可变动大小的数组,随着越来越多的元素被添加到ArrayList中,其规模是动态增加的,内存分配是一块儿连续的内存
- LinkedList:LinkList底层是基于双向链表实现的,每一个元素Node除了记录本身的值之外还需要保存前、后数据节点的引用
源码解析
ArrayList核心源码解读
构造方法及属性值:
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10
/**
* 空数组(用于空实例)
*/
private static final Object[] EMPTY_ELEMENTDATA = {}
/**
* 用于默认大小空实例的共享空数组实例
* 从EMPTY_ELEMENTDATA数组中区分出来,在使用默认构造函数的时候赋值(不指定初始容量)
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
/**
* 存储数据的对象,是一个Object数组
*/
transient Object[] elementData;
/**
* 数组包含的数据个数
*/
private int size
/**
* 数组最大容量
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 带初始容量的构造函数,可以指定初始化数据容量大小
*如果容量大于0就创建指定容量大小的数据,如果等于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);
}
}
/**
* 默认无参构造函数,不指定初始容量,数组对象赋值DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* 初始容量为0,当第一次添加元素时,容量变成10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
*/
public ArrayList(Collection<? extends E> c) {
//将指定集合转换为数组
elementData = c.toArray();
//如果elementData数组的长度不为0
if ((size = elementData.length) != 0) {
// 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断)
if (elementData.getClass() != Object[].class)
//将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 其他情况,用空数组代替
this.elementData = EMPTY_ELEMENTDATA;
}
}
核心方法解析:
/**
* 添加元素
*/
publicboolean add(E e) {
//判断数据容量,如果空数组就初始化,如果容量小了就扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
* 计算数据容量,如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA对象(默认无参构造函数创建)
* 容量设置为10,否则就是指定 初始化容量+1,这个也区分了EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的区别
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
/**
* 判断数组是否需要扩容
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//数组扩容方法
grow(minCapacity);
}
/**
* 用于默认大小空实例的共享空数组实例
* 从EMPTY_ELEMENTDATA数组中区分出来,在使用默认构造函数的时候赋值(不指定初始容量)
*/
private void grow(int minCapacity) {
//oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//再检查新容量是否超出了ArrayList所定义的最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
//如果超出了最大数组容量,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* 比较新容量和最大数组容量的大小,如果超出就设置Interger.MAX_VALUE
* 否则就设置MAX_ARRAY_SIZE
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
注:细心的同学可能会发现MAX_ARRAY_SIZE的值是Integer.MAX_VALUE-8,为什么会这样设置,而不是设置成
Integer.MAX_VALUE,这里涉及到Java对象结构,数组对象有一个属性_length用来记录数组的长度,2^31 = 2,147,483,648
所以自身需要8byte来记录数组的长度,数组最大长度就是Integer.MAX_VALUE-8
/**
* 删除ArrayList指定位置元素
* 将待删除元素后面的所有元素向左边移动一位,然后删除最后一位数据
*/
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
//计算移动的位置,将index之后所有元素左移一位
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//最后一位删除
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
LinkedList核心源码解读
LinkedList是一个双向链表结构,所以它的操作主要就是链表的遍历、断开以及链接操作,来实现List本身的增删改查功能。
/**
* 链表节点对象Node,包括三个属性
* item:节点值 next:后节点引用 prev:前节点引用
*/
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
/**
* 添加元素
* 添加元素就是直接在链表尾端加入元素,所以主要调用linkLast方法实现
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* 链表尾端加入元素
*/
void linkLast(E e) {
final Node<E> l = last;
//创建节点对象,prev指向last,next为空
final Node<E> newNode = new Node<>(l, e, null);
//将last赋值为新节点
last = newNode;
if (l == null)
//空链表,第一次插入首节点赋值为当前节点
first = newNode;
else
//原尾节点的next指向新节点
l.next = newNode;
size++;
modCount++;
}
/**
* 删除指定位置参数,主要操作是链表的断开,调用unlink方法
*/
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/**
* 断开删除节点的前后联系,链接删除节点的前后两个几点
*/
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;
//前节点为空,当前节点是first节点
if (prev == null) {
//first节点引用指向后节点
first = next;
} else {
//将前节点的next指向当前节点的next
prev.next = next;
//断开前节点的引用
x.prev = null;
}
//后节点为空,当前节点是last节点
if (next == null) {
//last节点引用指向前节点
last = prev;
} else {
//将后节点的prev指向当前节点的prev
next.prev = prev;
//断开后节点的引用
x.next = null;
}
//断开了前后链接,值设置为空,等GC回收
x.item = null;
size--;
modCount++;
return element;
}
/**
* 比较新容量和最大数组容量的大小,如果超出就设置Interger.MAX_VALUE
* 否则就设置MAX_ARRAY_SIZE
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
注:细心的同学可能会发现MAX_ARRAY_SIZE的值是Integer.MAX_VALUE-8,为什么会这样设置,而不是设置成
Integer.MAX_VALUE,这里涉及到Java对象结构,数组对象有一个属性_length用来记录数组的长度,2^31 = 2,147,483,648
所以自身需要8byte来记录数组的长度,数组最大长度就是Integer.MAX_VALUE-8
/**
* 删除ArrayList指定位置元素
* 将待删除元素后面的所有元素向左边移动一位,然后删除最后一位数据
*/
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
//计算移动的位置,将index之后所有元素左移一位
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//最后一位删除
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
差异比较
前面我们从数据结构和源码两个维度对ArrayList和Linked进行了深层的剖析,两者的差异其实就是数组和双向链表的操作差异。数组是个连续的内存块,随机访问通过下标直接找到相应的值,查询快,但是插入或者删除都涉及到数据的移动,效率低。而链表刚好想法,随机访问需要遍历所有元素,速度慢,但是插入和删除只需要断开前后节点的链接关系就行,效率高。
时间复杂度
操作 | 数组 | 链表 |
---|---|---|
随机访问 | O(1) | O(N) |
头部插入 | O(N) | O(1) |
头部删除 | O(N) | O(1) |
尾部插入 | O(1) | O(1) |
尾部删除 | O(1) | O(1) |
关注我的个人微信公众号:八阿哥技术圈,学习程序设计、系统架构知识,更多干货等你来哦!!!