Java-List详解
一,ArrayList
JDK源码中对ArrayList类的注释,大致翻译如下:
实现了List的接口的可调整大小的数组。实现了所有可选列表操作,并且允许所有类型的元素,包括null。除了实现了List接口,这个类还提供了去动态改变内部用于存储集合元素的数组尺寸的方法。(这个类与Vector类大致相同,除了ArrayList是非线程安全外)size,isEmpty,get,set,iterator,listIterator方法均为常数时间复杂度。add方法的时间复杂度为常数级别,这意味着,添加n个元素需要的时间为O(n)。所有其他方法的时间复杂度都是线性级别的。每个ArrayList实例都有一个capacity。capacity是用于存储ArrayList的元素的内部数组的大小。它通常至少和ArrayList的大小一样大。当元素被添加到ArrayList时,它的capacity会自动增长。在向一个ArrayList中添加大量元素前,可以使用ensureCapacity方法来增加ArrayList的容量。使用这个方法来一次性的使ArrayList内部数组的尺寸增长到我们需要的大小。
需要注意的是,这个ArrayList实现是未经同步的。若在多线程环境下并发访问一个ArrayList实例,并且至少一个线程对其做了结构型修改,那么必须在外部做同步。(结构性修改指的是任何添加或删除了一个或多个元素的操作,以及显示改变内部数组尺寸的操作。set操作不是结构性修改)。在外部做同步通常通过在一些自然的封装了ArrayList的对象上做同步来实现,如果不存在这样的对象,ArrayList应使用Collections.synchronizedList方法来包装。最好在创建的时候就这么做,以防止对ArrayList无意的未同步访问。
ArrayList的iterator方法以及listIterator方法返回的迭代器是fast-fail的:在iterator被创建后的任何时候,若对List进行了结构性的修改(以任何除了通过迭代器自己的remove方法或者add方法的方式),迭代器会抛出一个ConcurrentModificationException。因此,在遇到并发修改时,迭代器马上抛出异常,而不是冒着以后可能在不确定的时间发生不确定的行为的风险继续。
根据源码中的注释,我们了解了ArrayList是用来组织一系列同类型的数据对象,支持对数据对象的顺序迭代与随机访问。
1. 实现的接口
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
可以看到,实现了4个接口:List,RandomAccess,Cloneable,Serializable。
List是一个有序的集合类型(也被称作序列)。使用List接口可以精确控制每个元素被插入的位置,并且可以通过元素在列表中的索引来访问它。列表允许重复的元素,并且允许null元素,也允许多个null元素。
List接口定义了以下方法:
ListIterator<E> listIterator();
void add(int i,E element);
E remove(int i);
E get(int i);
E set(int i,E element);
int indexOf(Object element);
add,get方法都是我们在使用ArrayList时经常用到的。在ArrayList的源码注释中提到了,ArrayList使用Object数组来存储集合元素。
2. 成员变量
下面看一下源码中定义的几个字段:
//默认初始capacity
private static final int DEFAULT_CAPACITY = 10;
//供空的ArrayList使用的空的数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//供默认大小的空的ArrayList实例使用的空的数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放ArrayList中的元素的内部数组
//ArrayList的capacity就是这个内部数组的大小
//任何elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList在第一个元素被添加进来时,其capacity都会被扩大至DEFAULT_CAPACITY
transient Object[] elementData;
//ArrayList所包含的元素个数
private int size;
3. 构造器
首先看无参构造器的源码:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
无参构造器仅仅是吧ArrayList实例的elementData字段赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
其他构造器:
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(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
通过源码我们可以看到,第一个构造器指定了ArrayList的初始capacity,然后根据这个初始capacity创建一个相应大小的Object数组。若initialCapacity为0,则将elementData赋值为EMPTY_ELEMENTDATA;若initialCapacity为负数,则抛出一个IllegalArgumentException 异常。
第二个构造器则指定一个Collection对象作为参数,从而构造一个含有指定集合对象元素的ArrayList对象。这个构造器首先把elementData实例赋值为集合对象转化为的数组,然后再判断传入的集合对象是否不含有任何元素,若是的话,则将elementData赋值为EMPTY_ELEMENTDATA;若传入的集合对象至少包含一个元素,则进一步判断c.toArray方法是否正确返回了Object数组,若不是的话,则需要用Arrays.copyOf方法把elementData的元素类型改变为Object
4. 常用方法源码分析
4.1 add方法源码
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
可以看到,在add方法内部,首先调用了ensureCapacityInternal(size + 1),这个方法的作用有两个:
- 保证当前ArrayList实例的capacity足够大;
- 增加modCount,modCount的作用是判断在迭代时是否对rrayList进行了结构性的修改。
然后通过将内部数组下一个索引处的元素设置为给定参数来完成了向ArrayList中添加元素,返回true表示添加成功。
4.2 get方法源码
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
首先调用rangeCheck方法检查我们传入的index是否在合法范围之内,然后调用elementData方法,方法源码如下:
E elementData(int index) {
return (E) elementData[index];
}
4.3 set方法源码
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
首先检查给定的索引是否在合法范围内,若在,则先把索引处原来的元素存储在oldValue中,然后把新元素放到该索引处并返回oldValue即可。
二,LinkedList
LinkedList是对链表这种数据结构的实现,当我们需要一种支持高效删除/添加元素的数据结构时,可以考虑使用它,总的来说,链表具有以下两个优点:
- 插入及删除操作的时间复杂度为O(1)
- 可以动态改变大小
链表的主要缺点是:由于其链式存储的特性,链表不具备良好的空间局部性,也就是说,链表是一种缓存不友好的数据结构。
1. 支持的操作
void addFirst(E element);
void addLast(E element);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
boolean add(E e);
void add(int index,E element);
以上操作除了add(int index,E element)外,时间复杂度均为O(1),而它的时间复杂度为O(n)
2. 成员变量
transient int size = 0;
//指向头节点
transient Node<E> first;
//指向尾节点
transient Node<E> last;
LinkedList只保存了头尾节点的引用作为其实例域,下面是LinkedList的内部类Node的源码:
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;
}
}
每个Node对象的next域指向它的下一个节点,prev指向它的上一个节点,item为本节点所存储的数据对象。
3. 常用方法源码分析
3.1 addFirst方法源码
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
首先把头节点引用存于变量f中,然后创建一个新结点,这个新结点的数据为我们传入的参数e,prev指针为null,next指针为f。然后把头结点指针指向新创建的结点newNode。然后判断f是否为null,若为null,说明之前链表中没有结点,所以last也指向newNode;若f不为null,则把f的prev指针设为newNode。最后还需要把size和modCount都加一,modCount的作用与在ArrayList中的相同。
3.2 getFirst方法源码
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
若first为null,抛出异常;若first不为null,则直接返回item域。
3.3 removeFirst方法源码
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
首先用element存储待删除结点的item域,然后用next存储待删结点的下一个结点,将next赋值给first。
3.4 add(int index,E e)方法源码
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
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++;
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
Node<E> node(int index) {
// assert isElementIndex(index);
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;
}
}
这个方法首先调用checkPositionIndex方法检查给定index是否在合法范围之内。然后若index等于size,这说明要在链表尾插入元素,直接调用linkLast方法,这个方法的实现与之前介绍的linkFirst类似;若index小于size,则调用linkBefore方法,在index处的Node前插入一个新Node(node(index)会返回index处的Node)
node方法首先判断index位于链表的前半部分还是后半部分,若是前半部分,则从头结点开始遍历,否则从尾节点开始遍历,这样可以提升效率。