ArrayList
ArrayList中有两个方法可以返回数据
public Object[] toArray();
public <T> T[] toArray(T[] a);
ArrayList中有一个静态方法asList可以返回对应的List
Integer[] a = {1, 2, 3};
List<Integer> list = Arrays.asList(a);
这个方法返回的List,内部就是传入的数组,所以对数据的修改也会反映到List中,对List调用add、remove方法会抛出异常
要使用ArrayList完整的方法,应该新建一个ArrayList
List<Integer> list = new ArrayList<Integer>(Arrays.asList(a));
ArrayList特点总结:
1. 可以随机访问,按照索引位置进行访问效率很高。O(1)
2. 除非数组已排序,否则按照内容查找元素效率比较低。O(N)
3. 插入和删除元素效率比较低。O(N)
4. 非线程安全的
LinkedList
LinkedList还实现了队列接口Queue,队列的特点就是先进先出。
public interface Queue<E> extends Collection<E> {
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
}
Queue主要扩展了Collection接口,主要是三个操作:
- 在尾部添加元素(add、offer)
- 查看头部元素(element、peek),返回头部元素,但不改变队列
- 删除头部元素(remove、poll),返回头部元素,并且从队列中删除
每个操作都对应了两个方法。它们的区别在于,当队列为空的时候,element和remove会抛出异常,而peek和poll会返回null;当队列为满的时候,add会抛出异常,offer返回false
LinkedList其实是直接实现的Duque接口,该接口表示双端队列。它可以实现栈相关的操作,栈的特点是先进后出,后进先出。
public interface Deque<E> extends Queue<E> {
// 表示入栈,在头部添加元素,如果栈满了,会抛出异常
void push(E e);
// 出栈,返回头部元素,并且从栈中删除,如果栈为空,抛出异常
E pop();
// 查看栈头部元素,如果栈为空,返回null
E peek();
// 从后往前遍历
Iterator<E> descendingIterator();
}
LinkedList和ArrayList用法上类似,只是LinkedList增加了一个接口Deque,可以把它看作队列、栈、双端队列。
LinkedList实现原理
ArrayList内部是数组,元素在内存是连续存放的,但LinkedList不是。LinkedList直译就是链表,它的内部是双向链表,每个元素在内存都是单独存放的,元素之间通过链接连在一起。
为了表示链接关系,需要一个节点,节点包括实际的元素,两个链接,分别指向前一个节点(前驱)和后一个节点(后继)
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内部组成主要由如下三哥实例变量
int size = 0; // 表示链表长度
Node<E> first; // 指向头节点
Node<E> last; // 指向尾节点
添加元素 add
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
// 创建一个新的节点
final Node<E> newNode = new Node<>(l, e, null);
// 修改尾节点,指向最新创建的节点
last = newNode;
// 如果原来的尾节点为空,那么新节点也就变成头节点了
if(l==null) {
first = newNode;
} else {
// 否则让原来的尾节点的后继指向新的节点
l.next = newNode;
}
// 增加链表大小
size++;
// 记录修改次数
modCount++;
}
与ArrayList不同,LinkedList的内存是按需分配的,添加元素也很简单,直接在尾节点添加链接即可
获取元素 get
public E get(int index) {
// 检查index是否超出范围
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// sieze >> 1相当于size/2
// 如果index在size的前半部分,则从头节点开始找,否则从尾节点开始
if (index < (size >> 1)) {
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;
}
}
与ArrayList不同,ArrayList中数组元素连续存放,可以根据索引直接定位,而在LinkedList中,则必须从头或尾节点顺着链接查找
插入元素
add是在尾部添加元素,如果在头部或中间插入元素,可以使用如下的方法
public void add(int index, E element) {
// 检查index是否合法
checkPositionIndex(index);
// 如果index正好等于size,那么就直接调用添加方法
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
/**
* @succ 目标位置当前的节点 这里插入后,就变成后继了
*/
void linkBefore(E e, Node<E> succ) {
// 获取当前位置节点的前驱
final Node<E> pred = succ.prev;
// 新建节点,前驱是pred,后继是之前位置上的节点
final Node<E> newNode = new Node<>(pred, e, succ);
// 后驱的前驱重新给值
succ.prev = newNode;
// 前驱的后继重新给值
if (pred == null)
first = newNode;
else
pred.next = newNode;
// 增加长度
size++;
modCount++;
}
在中间插入元素,LinkedList只需要按需分配内存,修改前驱和后继节点的链接,而ArrayList可能需要分配很多额外的空间,并移动后续元素
remove 删除元素
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;
// 如果前驱为空,那么头节点就是后继
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;
}
删除x节点,基本思路就是让x的前驱和后继链接起来。
LinkedList特点
- 按需分配空间,不需要预先分配额外的空间
- 不可随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)
- 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)
- 在两端添加、删除元素效率很高,为O(1)
- 在中间插入、删除元素,要先定位,效率较低,为O(N),但修改本身的效率很高,为O(1)
如果列表长度未知,添加、删除操作比较多,尤其经常从两端进行操作,而按照索引位置访问相对比较少,则LinkedList是比较理想的选择
ArrayDeque
ArrayDeque是基于数组实现的双端队列,主要有以下几个属性
// 存储队列中节点的数组
transient Object[] elements;
// 代表头指针
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);
}
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
private void allocateElements(int numElements) {
// 如果numElements小于8,那么数组长度就分配8
intinitialCapacity = MIN_INITIAL_CAPACITY;
if (numElements >= initialCapacity) {
// 如果numElements大于8,分配的长度是严格大于numElements并且为2的整数次幂
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
elements = new Object[initialCapacity];
}
如果没有指定显式传入elements的长度,则默认16。如果显式传入一个代表elements的长度的变量,那么会调用allocateElements做一些简单的处理,主要的处理都在上面的代码注释中。 这里它计算严格大于numElements并且为2的整数次幂的方式,就是先将numElements二进制形式的所有位置1,然后+1就是了。
从尾部添加 add
public boolean add(E e) {
addLast(e);
return true;
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
// 将元素添加到尾指针的位置
elements[tail] = e;
// 将tail指向向一个位置,如果满了,就扩容
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
这里我们重点看一下判断当前队列是否满了的语句
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
我们之前在构造elements元素的时候,说过它的长度一定是2的指数级,所以对于任意一个2的指数级的值减去1之后它的二进制必然所有位全为1,例如:8-1之后为111,16-1之后1111。而对于tail来说,当tail+1小于等于elements.length - 1,两者与完之后的结果还是tail+1,但是如果tail+1大于elements.length - 1,两者与完之后就为0。就等于头指针的位置了。
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];
// 将head右边的元素赋值到新数组开头
System.arraycopy(elements, p, a, 0, r);
// 将head左边的元素赋值到新数组中
System.arraycopy(elements, 0, a, r, p);
elements = a;
// 重新设置head和tail
head = 0;
tail = n;
}
从头部添加 addFirst
public void addFirst(E e) {
if(e == null)
throw new NullPointerException();
// 让head指向前一个位置
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
出栈
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
if (result == null)
return null;
elements[h] = null;
head = (h + 1) & (elements.length - 1);
return result;
}
该方法很简单,直接获取数组头部元素即可,然后head往后移动一个位置。这是出队操作,其实删除操作也是一种出队,内部还是调用了pollFirst方法。
查看长度
public int size() {
return (tail - head) & (elements.length - 1);
}
ArrayDeque特点
- 在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组复制开销可以被平摊,具体来说,添加N个元素的效率为O(N)
- 根据元素内容查找和删除的效率比较低,为O(N)
- 与ArrayList和LinkedList不同,没有索引的概念
迭代器
Iterable Iterator
Iterable 表示可迭代的,它有一个方法iterator(),返回Iterator对象。
public interface Iterable<T> {
Iterator<T> iterator();
}
public interface Iterator<E> {
// 判断是否还有元素未访问
boolean hasNext();
// 返回下一个元素
E next();
// 删除最后返回的元素。如果没有调用过next(),直接调用remove()是会报错的
void remove();
}
如果对象实现了Iterable,就可以使用foreach语法。foreach语法,编译器会转换为调用Iterable和Iterator接口的方法。
ListIterator
ArrayList还提供了两个返回Iterator接口的方法:
// 返回的迭代器从0开始
public ListIterator<E> listIterator();
// 返回的迭代器从指定位置index开始
public ListIterator<E> listIterator(int index);
ListIterator扩展了Iterator接口
public interface ListIterator<E> extends Iterator<E> {
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void set(E e);
void add(E e);
}
迭代器常见问题
有一种常见的误用,就是在迭代的中间调用容器的删除方法:
public void remove(ArrayList<Integer> list) {
for(Integer a : list) {
if(a<=100) {
list.remove(a);
}
}
}
这样做是会抛出异常的。因为迭代器内部会维护一些索引位置相关的数据,要求在迭代过程中,容器不能发生结构性变化(添加、删除元素)
使用迭代器的remove方法可以避免错误
public static void remove(ArrayList<Integer> list) {
Iterator<Integer> it = list.iterator();
while(it.hasNext()) {
if(it.next() < 100) {
it.remove();
}
}
}
为什么使用迭代器的remove方法就可以呢?
迭代器基本原理
先看看ArrayList中的iterator方法
public Iterator<E> iterator() {
return new Itr();
}
Itr是ArrayList的成员内部类,
private class Itr implements Iterator<E> {
// 下一个要返回的元素的位置
int cursor;
// 最后一个返回的索引位置,如果没有为-1
int lastRet = -1;
// 期望的修改次数
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
public E next() {
// 检查是否发生了结构变化
checkForComodification();
// 更新cursor和lastRet
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的remove方法删除
ArrayList.this.remove(lastRet);
// 更新cursor和lastRet
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}