简介:
首先我们要知道 ArrayList 与 LinkedList 的底层是什么数据据结构:ArrayList的底层数据结构就是一个数组,数组元素的类型为Object类型,对ArrayList的所有操作底层都是基于数组的。
而LinkedList的底层是一个双向链表,在jdk 1.6,LinkedList是双向循环链表,从 jdk 1.7 后,LinkedList 是简单的双向链表。下面我们主要以 jdk 1.8 的 LinkedList 说起。
ArrayList 与 LinkedList 的继承关系:
可以看出它们都实现了List接口,都继承于 AbstractList,只是 LinkedList 的具体方法都是在AbstractList与 LinkedList 之间拦了一层的 AbstractSequentialList 类里实现的。下面让我们一起来看看它们各自方法的具体实现吧。
ArrayList
- add():
// 我们先了解几个默认的参数
// 数组默认初始大小为10
private static final int DEFAULT_CAPACITY = 10;
// 缓冲数组
transient Object[] elementData; // non-private to simplify nested class access
// ArrayList 的大小(它包含的元素数)。
private int size;
/**
* modCount:
* 该字段表示list结构上被修改的次数,该字段被Iterator以及ListIterator的实现类所使用,如果该值被意外更改,
* Iterator或者ListIterator 将抛出ConcurrentModificationException异常,这是jdk在面对迭代遍历的时候为了避免不确定性而采取的快速失败原则
*/
public boolean add (E e){
modCount++;//数组修改次数加一
add(e, elementData, size);
return true;
}
}
add(具体实现):
/**
* 添加元素的具体实现
* ArrayList.add("1");
*/
private void add (E e, Object[]elementData,int s){
//先判断数组是否需要扩容
if (s == elementData.length)
elementData = grow();
//添加元素
elementData[s] = e;
// 数组长度加一
size = s + 1;
}
grow(扩容机制):
private Object[] grow () {
return grow(size + 1);
}
private Object[] grow ( int minCapacity){
return elementData = Arrays.copyOf(
elementData,
newCapacity(minCapacity) //newCapacity(int):返回合理的扩容长度
);
}
/**
* 扩容长度的值的具体计算
*/
private int newCapacity ( int minCapacity){
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容为原来的1.5倍
if (newCapacity - minCapacity <= 0) { //如果扩容后的数组小于或等于原数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//max(int a, int b) :return (a >= b) ? a : b;
//a: DEFAULT_CAPACITY=10;=>默认大小
//b: minCapacity=> 当前数组大小
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();//数组长度小于0抛出异常
return minCapacity; //返回原数组长度
}
//MAX_VALUE = 0x7fffffff
//MAX_ARRAY_SIZE=MAX_VALUE-8
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
/**
* 如果扩容后数组长度超出最大限制(MAX_VALUE = 0x7fffffff)
*/
private static int hugeCapacity ( int minCapacity){
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE)
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE;
}
/**
* 进行数组拷贝
*/
public static <T > T[]copyOf(T[]original,int newLength){
return (T[]) copyOf(original, newLength, original.getClass());
}
/**
* 数组拷贝的具体实现
*/
@HotSpotIntrinsicCandidate
public static <T, U > T[]copyOf(U[]original,int newLength, Class<? extends T[]>newType){
@SuppressWarnings("unchecked")
T[] copy = ((Object) newType == (Object) Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
到这里 ArrayList 的add()也就差不多可以结束了。当然数据对象的存储必然会涉及到地址的存储,与hashCode的生成与存储,有兴趣的可以去debug看看,这里就不作赘述了。
-set():
/**
* 更新操作
* ArrayList.set(1,"2");
*/
public E set ( int index, E element){、
Objects.checkIndex(index, size);//检查index的合法性
E oldValue = elementData(index);//拿到当前的下标的元素
elementData[index] = element;//重新赋值
return oldValue;//返回当前下标元素,数据更新成功
}
checkIndex(检查index的合法性):
@ForceInline
public static
int checkIndex ( int index, int length){
return Preconditions.checkIndex(index, length, null);
}
/**
* index检查的具体实现
*/
@HotSpotIntrinsicCandidate
public static <X extends RuntimeException >
int checkIndex ( int index, int length, BiFunction<String, List < Integer >, X > oobef){
if (index < 0 || index >= length)//如果index小于0或大于当前数组长度,抛出异常。
throw outOfBoundsCheckIndex(oobef, index, length);
return index;
}
-get():
// ArrayList.get(1);
public E get(int index) {
Objects.checkIndex(index, size);//检查index的合法性,上面有源码解析,不多说
return elementData(index);//返回当前下标的元素
}
- remove():
/**
* 删除操作
* ArrayList.remove(1);
*/
public E remove ( int index){
Objects.checkIndex(index, size);
final Object[] es = elementData;//复制一份当前的数组
@SuppressWarnings("unchecked") E oldValue = (E) es[index];//获取到当前下标的元素
fastRemove(es, index);
return oldValue;
}
fastRemove(删除元素的具体实现):
private void fastRemove (Object[]es,int i){
modCount++;//数组修改次数加一
final int newSize;
if ((newSize = size - 1) > i) //如果我们删除的不是最后一个元素
//数组拷贝(浅拷贝)
//arraycopy(Object src, int srcPos,Object dest, int destPos,int length);
//src - 源数组。
//srcPos - 源数组中的起始位置。
//dest - 目标数组。
//destPos - 目的地数据中的起始位置。
//length - 要复制的源数组元素的数量。
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;//如果是最后一个元素,直接让最后一个元素为null
}
/**
* 拷贝的实现
*
*/
@HotSpotIntrinsicCandidate
public static native void arraycopy (Object src,int srcPos, Object dest,int destPos, int length);
整个remove()的重点可能是arraycopy ()方法的拷贝原理。
当然,数组拷贝的方法还可以用其他的方式实现吗,以下举例:
1)、使用for循环依次的遍历移动
2)、使用System.arraycopy()方法
3)、使用Arrays.copyOf()方法 ,##如果去看源码,底层其实 还是arraycopy() 方法的实现;
4)、使用Object.clone()方法
其实不管数组的如何拷贝最后还是调用了System的arrayCopy的本地方法去进行操作,因为Java最后还是通过jni调用底层的C语言来实现的,由于Android是开源的所以我就直接找到了 /dalvik/vm/native/java_lang_System.cpp来看看其内部实现,如果你也想研究Java的该类底层是如何实现的话,你可以直接下载OpenJDK,因为Oracle的JDK好像是看不到底层的实现的。
-clear():
// ArrayList.clear();
public void clear() {
modCount++;//数组修改次数加一
final Object[] es = elementData;//拷贝一份数组
for (int to = size, i = size = 0; i < to; i++)
es[i] = null;//遍历删除每一个元素。
}
ArrayList的优缺点:
1)、ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快。
2)、ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已。
3)、根据下标遍历元素,效率高。
4)、根据下标访问元素,效率高。
5)、可以自动扩容,默认为每次扩容为原来的1.5倍。
ArrayList的缺点:
1)、插入和删除元素的效率不高。
2)、根据元素下标查找元素需要遍历整个元素数组,效率不高。
3)、线程不安全。如果需要可以使用同步列表copyOnWriteArrayList
LinkedList:
首先我们得知道LinkedList的底层是一个双向链表
-add():
//首先还是先了解几个基本的属性
//链表长度,,默认为0
transient int size = 0;
/**
* 指向第一个节点的指针,
*/
transient LinkedList.Node<E> first;
/**
* 指向最后一个节点的指针,
*/
transient LinkedList.Node<E> last;
/**
* 可以看出该节点有两个指针分别为:next,prev
* 可以推断出为双向链表。
*/
private static class Node<E> {
E item;
LinkedList.Node<E> next;
LinkedList.Node<E> prev;
Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
public boolean add (E e){
//实现方法
linkLast(e);
return true;
}
/**
* 添加节点的具体实现(尾插)
* LinkedList.add("1");
*/
void linkLast (E e){
//last=null=I
//拿到指向最后一个节点的指针的节点
final LinkedList.Node<E> l = last;
//创建一个节点,并有一个指向上一个节点的指针
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
//指向最后一个节点的指针指向当前节点
last = newNode;
//如果指向最后一个节点的指针为null(也就是当前的这个节点为第一个节点)那么,我们就把指向第一个节点的指针指向该节点
if (l = null)
first = newNode;
//如果指向最后一个节点的指针不为null(也就是说有一个节点为最后一个节点)那么,我们直接把最后一个节点的指向下一个节点的指针指向当前节点
else
l.next = newNode;
//链表长度和链表修改的次数加一
size++;
modCount++;
}
/**
* index校验的具体实现
*/
private void checkPositionIndex ( int index){
//具体实现:return index >= 0 && index <= size;
//即 如果 index 小于0或超出当前链表长度,抛出异常
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 与子对立的头插法
* LinkedList.addFirst("2");
*/
private void linkFirst (E e){
//拿到指向第一个节点的指针
final LinkedList.Node<E> f = first;
//创建一个节点,因为是头插,所有有一个指向下一个节点的指针
final LinkedList.Node<E> newNode = new LinkedList.Node<>(null, e, f);
//指向第一个节点的指针指向当前节点
first = newNode;
//这里就不多说了,上面的理解了,这儿肯定也没问题
if (f == null)//可以理解为当前链表为空
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
add(指定位置添加节点):
/**
*add(指定位置添加节点):
* LinkedList.add(1,"3");
*/
public void add ( int index, E element){
checkPositionIndex(index);//检验index的合法性
if (index == size)
//如果指定的位置为最后一个节点位置,那么我们直接调用尾插方法,将节点添加到链表最后
linkLast(element);
else
//如果自定的位置不是最后
linkBefore(element, node(index));
}
linkBefore(具体实现):
void linkBefore (E e, Node < E > succ){
// assert succ != null;
//拿到指定位置的上一个节点
final LinkedList.Node<E> pred = succ.prev;
//创建节点
final LinkedList.Node<E> newNode = new LinkedList.Node<>(pred, e, succ);
succ.prev = newNode;
//如果指定位置的上一个节点为null(也就是我们的链表还没有节点,或者我们要插入的位置为头部),那么,我们把指向第一个节点的指针指向当前节点
if (pred == null)
first = newNode;
else
//如果指定位置的上一个节点不为null(也就是说我们插入的位置为链表中间(不等价于绝对中间)),那么,我们就把指定位置的上一个节点的指向下一个节点的指针指向当前节点。
pred.next = newNode;
//链表长度和链表修改的次数加一
size++;
modCount++;
}
-set():
/**
* 更新操作
* LinkedList.set(2, "4");
*/
public E set ( int index, E element){
checkElementIndex(index);//检查index的合法性
LinkedList.Node<E> x = node(index);//拿到当前节点
E oldVal = x.item;//拿到当前节点的值
x.item = element;//更新当前节点的值
return oldVal;
}
/**
* index校验
*/
private void checkElementIndex ( int index){
//isElementIndex: return index >= 0 && index < size;
//即 如果 index 小于0或超出当前链表长度,抛出异常
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
-remove():
/**
* 删除操作
* LinkedList.remove(2);
*/
public E remove ( int index){
checkElementIndex(index);//index下标的校验
//node(index):找到当前节点是索引位置
return unlink(node(index));
}
/**
* 获取当前下标节点在链表中的位置
*/
LinkedList.Node<E> node ( int index){
// assert isElementIndex(index);
if (index < (size >> 1)) {
LinkedList.Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
LinkedList.Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
*删除的具体实现
*/
E unlink (Node < E > x) {
// assert x != null;
///拿到当前节点的类容与前后指针
final E element = x.item;
final LinkedList.Node<E> next = x.next;
final LinkedList.Node<E> prev = x.prev;
//如果当前节点的上一个节点为null(说明当前节点为第一个节点),那么,我们就把指向第一个节点的指针指向当前节点的下一个节点
if (prev == null) {
first = next;
} else {
//如果当前节点的上一个节点不为null(说明该节点可能在链表中间),那么。就把当前节点的上一个节点的指向下一个节点的指针指向当前节点的下一个节点。
prev.next = next;
//再把当前节点指向上一个节点的指针置为null,即断掉与前一个节点的连接(保证链表的完整性)
x.prev = null;
}
//如果当前节点的下一个节点为null(说明当前节点为最后一个节点),那么,我们就把指向最后一个节点的指针指向当前节点的上一个节点
if (next == null) {
last = prev;
} else {
//如果当前节点的下一个节点不为null(说明该节点可能在链表中间),就把当前节点的下一个节点的指上一个节点的指针指向当前节点的上一个节点。
next.prev = prev;
//再把当前节点指向上下一个节点的指针置为null,即断掉与前一个节点的连接(保证链表的完整性)
x.next = null;
}
//清除当前节点的数据
x.item = null;
//链表长度减一
size--;
//链表修改记录加一
modCount++;
return element;
}
-clear():
/**
* 清空链表
* LinkedList.clear();
*/
public void clear () {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
//循环遍历删除所有节点
for (LinkedList.Node<E> x = first; x != null; ) {
LinkedList.Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
//指向第一与最后一个的节点指针置为null
first = last = null;
size = 0;
modCount++;
}
LinkedList的优缺点:
优点:
1)、删除和添加数据所消耗的资源较少,且比ArrayList效率高。
缺点:
1)、线程不安全。
2)、查找消耗的资源大,效率低
3)、不能随机访问。
总结:
1)、ArrayList与LinkedList都是线程不安全的容器,
2)、我们在使用的时候应根据我们的实际需求选择具体的容器。
3)、这里只列出了常用的几种方法的源码实现,但相信看懂这些实现以后,其他的方法也不在话下。