文章目录
总结在前
ArrayList内部采用动态数组实现。非线程安全。
- 可以随机访问——通过索引位置访问,效率高;时间复杂度为O(1), 与长度无关;
- 除非已排序,否则,按内容查找效率比较低,复杂度为O(N);
- 添加元素会进行数组复制,可能还会重新分配内存长度,与添加的元素个数正相关,复杂度为O(N);
- 插入和删除元素效率比较低,同样会移动元素(也是数组赋值),复杂度为O(N);
LinkedList内部采用双向链表实现。
- 按需分配空间,维护了长度,首尾节点,但是没有对长度做限制;
- 不可以随机访问,按索引访问效率比较低,必须从头或尾进行遍历,效率为O(N/2);
- 不论是否已排序,按照内容查找元素效率低,O(N);
- 在两端添加、删除元素效率很高,为O(1);
- 在中间插入、删除元素,要先定位(即查找),效率为O(N);但是插入和删除本身效率很高,为O(1);
ArrayDeque内部采用循环数组实现。
- 在首尾添加、删除元素的效率很高,但是动态扩展需要内存分配以及数组复制,整体效率为O(N);
- 根据元素内容查找和删除的效率比较低,为O(N);
- 没有索引位置的概念,不能根据索引位置操作
ArrayList和LinkedList都实现了List接口:
如果列表长度未知,添加、删除操作比较多,尤其经常从两端进行操作,而按照索引位置访问相对比较少,LinkedList比较理想。
对于列表长度已知,只添加、不插入和删除的场景ArrayList足以应对,尤其是ArrayList支持随机访问。
LinkedList和ArrayDeque都实现了双端队列Deque接口:
如果只需要Deque接口,从两端进行操作,ArrayDeque效率要高于LinkedList。
但如果还要根据索引位置操作,或经常需要在中间进行插入、删除,则应该选择LinkedList。
ArrayList
基本实现
ArrayList是一个泛型容器,可以理解为动态数组。主要方法有添加元素、查询元素下标、通过下标查询元素、移除元素等。
其实现类似于StringBuilder,也是以一个数组存储元素,只不过这个数组并不是特定类型的元素,而是Object[]。
类似StringBuilder,ArrayList也有一个默认数组长度为10,且每次添加元素都要判断数组长度是否足以容纳要加入的元素,如果长度不足需要扩充数组长度。(ArrayList中是进行1.5倍长度增长,StringBuilder中是进行2倍长度增长,基本思路一致)
下面代码段摘录了成员变量和add、remove元素方法。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10; // 默认长度为10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //默认空数组数据
transient Object[] elementData; // 存储数据的结构
private int size; // 数组中真实保存的元素个数,初始态是0,且从构造方法看,没有默认0值填充
protected transient int modCount = 0; // 数组被修改的次数———增、删都会引起该值增1,变动了数组结构。修改元素值不会自增
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);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e; //加入到元素最后
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); //最小数组长度为DEFAULT_CAPACITY=10
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //数组元素修改次数增1
if (minCapacity - elementData.length > 0) //先减法再判断,防止越限。
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //目标数组长度增大到1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; //取
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); //数组扩充到newCapacity长度,由elementData填充前0~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;
}
public E remove(int index) {
rangeCheck(index); // 判断index是否越限,如果超过size的值,表示超出元素数抛出异常
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1; //要移动的元素个数
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
//将elementData的numMoved个元素从index+1开始复制到从index开始的位置,然后将最后一个元素置为null,使GCRoots不可达
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
}
迭代
对于容器而言,可以和数组类似使用其下标值进行迭代,也可以使用foreach进行迭代,其编译为class文件时,实际上是iterator迭代。
ForEach迭代的本质
.java代码块:
ArrayList<Integer> intList = new ArrayList();
intList.add(5); intList.add(50);
for (Integer integer : intList) {
System.out.println(integer);
}
.class代码块:
ArrayList<Integer> intList = new ArrayList();
intList.add(5);
intList.add(50);
Iterator var7 = intList.iterator();
while(var7.hasNext()) {
Integer integer = (Integer)var7.next();
System.out.println(integer);
}
迭代器接口
具备forEach迭代能力的类都实现了Iterable接口,该接口返回一个Iterator迭代器对象,通过该迭代器可以进行从首元素开始进行后向元素判断和遍历(hasNext和next),并且提供了安全的迭代内元素移除方法remove;而ListIterator接口继承了Iterator,对其进行了增强,可以从任意位置开始迭代,并且提供了前向元素判断和遍历能力(hasPrevious和previous),并且增强了添加add和修改set方法。
迭代的陷阱:
在迭代中,由于会维护一些索引位置相关的数据,要求在迭代过程中,容器不能有结构性变化(元素个数变化),否则索引位置就失效了,即不可以有add、remove、insert操作。
但是可以通过迭代器遍历,进行结构性调整。
看一下ArrayList的实现。它有一个内部类Itr implements Iterator,并且有一个内部类ListItr implements ListIterator。
private class Itr implements Iterator<E> {
int cursor; // 下一个要放回的元素位置,迭代游标,从0开始
int lastRet = -1; // 上一个被返回的元素位置,-1表示没有这个元素
int expectedModCount = modCount; //期望变更次数,该值必须与modCount一致才能继续迭代
Itr() {}
public boolean hasNext() {return cursor != size;}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet); //上一次被返回的位置被移除;调用的是ArrayList的remove(index)方法
cursor = lastRet; //游标上一到上次处理的位置
lastRet = -1; //
expectedModCount = modCount; //这里是迭代器可以进行数组结构变动的关键。在ArrayList的remove方法中,只会增加modCount。
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() { //如果modCount与期望值不相等则抛出异常。
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
迭代器的优势
- forEach语法更加简洁;
- 迭代器更加通用,所有的容器都可以实现。这是一种关注点分离的思想,将数据的实际组织方式与数据的迭代遍历分离。
- 迭代器的性能要好一些:因为提供Iterator接口的代码更了解数据的组织形式,可以提供更高效的实现。
ArrayList实现的其他接口
Collection——表示一个数据集合,数据间没有位置或顺序的概念。方法主要是针对元素的添加、移除;
List——表示有顺序或位置的数据集合,它扩展了Collection,增加了针对数据下标的相关方法。
RandomAccess——标记接口,表示可以随机访问。在一些通用算法代码中,通过判断该标记接口选取高效的实现。如Collections.binarySearch方法,就通过判断是否实现了RandomAccess选择合适的二分查找算法。
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的特点
其内部采用动态数组实现。非线程安全。
- 可以随机访问——通过索引位置访问,效率高;时间复杂度为O(1), 与长度无关;
- 除非已排序,否则,按内容查找效率比较低,复杂度为O(N);
- 添加元素会进行数组复制,可能还会重新分配内存长度,与添加的元素个数正相关,复杂度为O(N);
- 插入和删除元素效率比较低,同样会移动元素(也是数组赋值),复杂度为O(N);
LinkedList
LinkedList实现了List和Deque接口,其中Deque又继承了Queue接口。
Queue——队列接口,先进先出原则(在尾部添加元素,从头部删除元素),扩展了Collection。
public interface Queue<E> extends Collection<E> {
boolean add(E e);//向尾部添加元素e
boolean offer(E e);
E remove(); //删除头部元素——返回头部元素,并将头部元素删除
E poll();
E element();//查看头部元素——返回头部元素,但不改变队列
E peek();
}
Java中没有专门定义栈接口(先进后出),而是通过双端队列Deque实现的。Deque继承了Queue,并提供了操作头部元素的方法:push、pop、peek(与Queue一致):
- push:在头部添加元素,栈的空间是有限的,沾满了就会抛出异常IllegalStateException;
- pop:返回头部元素,并且将其删除,如果栈为空,则抛出异常NoSuchElementException;
- peek:查看头部元素,栈为空时返回null。
同时Deque由于是双端队列,还提供了offer、add、poll、remove等都提供了xxxFirst用来操作头部元素、xxxLast操作尾部元素。
因此,LinkedList可以用作队列和栈。
LinkedList<Integer> list = new LinkedList<>();
Queue<Integer> queue = list; //用作队列
queue.offer(1);queue.offer(2);queue.offer(3);//尾部添加
while(queue.peek() != null) {
System.out.println(queue.poll());//队列用法,出队列——头部,输出:1、2、3
}
Deque<Integer> deque = list; //用作双端队列
deque.push(1);deque.push(2);deque.push(3); //栈的用法,入栈——头部添加
deque.offer(4);//队列的用法,尾部添加
while(deque.peek() != null) {
System.out.println(deque.pop()); //栈的用法,出栈——头部,输出3、2、1、4
}
LinkedList用作List时与ArrayList基本类似,有add、get、remove、set、indexOf等方法。
实现原理
静态内部类Node用来存储链表中的元素,由于LinkedList实现了Deque双端队列,因此它是一个双向链表,Node中有前向pre、后向next元素索引,并且保存了本元素的值item。
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;
}
}
LinkedList中first和last分别记录链表的首尾,size记录链表中元素个数。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
public boolean add(E e) {
linkLast(e); //将元素e放入link的尾部
return true;
}
void linkLast(E e) {
final Node<E> l = last; //找到链表的尾元素
final Node<E> newNode = new Node<>(l, e, null); //创建新的Node,其pre=last,next=null
last = newNode; //将链表尾元素置为新Node
if (l == null)
first = newNode; //如果原来的尾元素是null,说明是一个空list,原first也是null,将first置为与last相等,此时list中只有一个元素
else
l.next = newNode;//否则,原尾元素的next由null设置为新的Node
size++;
modCount++;//list元素个数和修改次数都加1
}
public E get(int index) {//根据索引获取元素
checkElementIndex(index); //检查index是否超出size范围,超出则抛出异常
return node(index).item;
}
Node<E> node(int index) { //无法直接通过index获取到元素,需要前向或后向沿链表遍历
if (index < (size >> 1)) { //如果索引位置小于size的一般,则前向遍历
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { //否则后向遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
public int indexOf(Object o) {//通过元素值查找索引位置——LinkedList允许存入null
int index = 0;
if (o == null) { //如果要找的元素是null,则查找第一个为null的索引
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {//否则,判断元素值相等
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
public void add(int index, E element) {//插入到指定位置
checkPositionIndex(index);
if (index == size)
linkLast(element); //如果index就是size表示这是在尾部添加元素,等同于add(E e)
else
linkBefore(element, node(index));//将element插入到原node(index)前面,即将element插入到index位置
}
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev; //找到succ的前向元素pred
final Node<E> newNode = new Node<>(pred, e, succ);//新节点:连接pred和succ,元素值为e
succ.prev = newNode; //succ的prev指向新节点
if (pred == null)
first = newNode;
else
pred.next = newNode;//pred的next指向新节点
size++;
modCount++;
}
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));//将node(index)移除链表
}
E unlink(Node<E> x) {
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {//x就是首元素时
first = next;
} else {
prev.next = next;
x.prev = null; //一定要将x的前向和后向元素置为null,
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
}
对于迭代,LinkedList与ArrayList类似,都是通过内部类实现的。
LinkedList的特点
- 按需分配空间,维护了长度,首尾节点,但是没有对长度做限制;
- 不可以随机访问,按索引访问效率比较低,必须从头或尾进行遍历,效率为O(N/2);
- 不论是否已排序,按照内容查找元素效率低,O(N);
- 在两端添加、删除元素效率很高,为O(1);
- 在中间插入、删除元素,要先定位(即查找),效率为O(N);但是插入和删除本身效率很高,为O(1);
如果列表长度未知,添加、删除操作比较多,尤其经常从两端进行操作,而按照索引位置访问相对比较少,LinkedList比较理想。
对于列表长度已知,只添加、不插入和删除的场景ArrayList足以应对,尤其是ArrayList支持随机访问。
ArrayDeque
双端队列实现类,通过循环数组实现。
实现原理
transient Object[] elements; // non-private to simplify nested class access
transient int head;
transient int tail;
private static final int MIN_INITIAL_CAPACITY = 8;
public ArrayDeque() {
elements = new Object[16];
}
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
head与tail相同,则数组为空,数组长度为0;
tail大于head,则数组从head开始至tail-1结束;
tail小于head且tail=0,则数组从head开始至element.length-1结束。
tail小于head且tail>0,则数组从head开始至element.length-1,然后从0至tail-1结束。
elements的长度最小为8;默认16;且真实长度是比numElements大的2的整数次幂的最小值。(如numElements=12,真实长度是16)
为何要比numElements大?——循环数组需要至少留一个空位,tail变量指向下一个空位
添加元素:
以最简单的长度为8的数组为例,head=0,tail=7,在添加新元素后tail=8,如果和elements.length-1按位与,则值为0,与head相等。据此判断需要扩展数组容量。
public boolean add(E e) {
addLast(e);//默认向尾部添加元素
return true;
}
public void addLast(E e) {
if (e == null)//不允许向ArrayDeque中添加null,
throw new NullPointerException();
elements[tail] = e; //tail位置存储新元素
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity(); //将数组容量扩大一倍,
}
private void doubleCapacity() {
assert head == tail;//确认数组已满
int p = head;
int n = elements.length;
int r = n - p; // number of elements to the right of p
int newCapacity = n << 1; //数组长度翻倍
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
System.arraycopy(elements, p, a, 0, r);//将elements中p开始的r个元素,复制到a的0开始的位置
System.arraycopy(elements, 0, a, r, p);//将elements中0开始的p个元素,复制到a的r开始的位置
elements = a;
head = 0;
tail = n;
}
头部添加
举例:head=tail=0时,如果length=8;addFirst后,1111&0111=7,因此head=7。
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
出队列:
public E poll() {
return pollFirst();//默认头部出队列,先入先出
}
public E pollFirst() {
int h = head;
E result = (E) elements[h];//找到要出队列的元素
if (result == null)
return null;
elements[h] = null; // Must null out slot
head = (h + 1) & (elements.length - 1); //计算新的头部——后移一位,注意大量的位与运算
return result;
}
public E pollLast() {
int t = (tail - 1) & (elements.length - 1);//计算要出队列的位置,tail指向的是null的位置,tail-1才是队列尾
E result = (E) elements[t];
if (result == null)
return null;
elements[t] = null;
tail = t;
return result;
}
查看数组中元素长度:
注意在循环数组中,存在大量和(elements.length-1)进行的位与运算。
public int size() {
return (tail - head) & (elements.length - 1);
}
ArrayDeque的特点
- 在首尾添加、删除元素的效率很高,但是动态扩展需要内存分配以及数组复制,整体效率为O(N);
- 根据元素内容查找和删除的效率比较低,为O(N);
- 没有索引位置的概念,不能根据索引位置操作
如果只需要Deque接口,从两端进行操作,ArrayDeque效率要高于LinkedList。
但如果还要根据索引位置操作,或经常需要在中间进行插入、删除,则应该选择LinkedList。