List属于单列集合,常用来替换数组使用。集合类中元素有序且可重复,每个元素都有其对应的顺序索引,可以根据指定的索引在集合中存取元素。List接口常用的实现类有:
关于List接口各个实现类的使用已经在浅析Java中的List接口和实现类中有过介绍,这里着重看一下它们底层的实现原理。下面以JDK 8为基础进行源码的解读, 其中,每个实现类的原理分析都从字段、构造方法和常用方法三个角度进行。
ArrayList是List接口一种典型的、重要的实现类,本质上它就是对象引用的一个变长数组,如何理解呢?
ArrayList中字段的定义如下:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 序列化ID,用于序列化和反序列化
private static final long serialVersionUID = 8683452581122892189L;
// 初始容量,默认为10
private static final int DEFAULT_CAPACITY = 10;
// 下面定义了两个空的数组
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 保存数据的数组,就是一个Object数组
transient Object[] elementData; // non-private to simplify nested class access
// 数组已保存数据的个数,默认就是0,这里并没有直接初始化一个固定大小的数组
private int size;
// 数组的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
从字段的定义中可以看出,ArrayList底层就是使用了一个Object数组来保存数据。同时采用了懒汉式的模式,一开始并没有初始化一个固定容量的数组,而只是创建了一个空数组。
ArrayList提供了三种构造函数供对象的实例化操作,源码如下:
// 无参构造,此时默认创建的是一个空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 带参构造,参数为指定的数组容量大小
public ArrayList(int initialCapacity) {
// 如果传入的参数大于0,则直接创建一个指定容量的数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
// 如果参数为0,那么同样只创建一个空数组
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 否则抛出异常,即必须保证参数大于等于0
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 使用另一个Collection接口的对象初始化
public ArrayList(Collection<? extends E> c) {
// 首先将传入的对象转换为数组形式
elementData = c.toArray();
// 如果数组中有数据
if ((size = elementData.length) != 0) {
// 如果此时并不是Object类型数组
if (elementData.getClass() != Object[].class)
// 将其转换为Object类型,再赋给elementData
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果已经是Object类型,直接赋给elementData即可
this.elementData = EMPTY_ELEMENTDATA;
}
}
下面通过一个例子看一下如何使用不同的构造函数进行对象实例化,如下所示:
@Test
public void test1(){
List<String> list = new ArrayList<>();
System.out.println(list.size());
System.out.println("------------------");
List<String> list1 = new ArrayList<>(10);
System.out.println(list.size());
System.out.println("------------------");
List<String> help = new ArrayList<>();
Collections.addAll(help, "Forlogen", "Kobe", "James");
List<String> list2 = new ArrayList<>(help);
System.out.println(list2.size());
System.out.println(list.toString());
}
输出为:
0
------------------
0
------------------
3
[]
下面我们主要看一下add()
、set()
、get()
这几个方法的具体实现。
add方法有两种重载形式,其中一种是只传入要添加的元素,如下所示:
public boolean add(E e) {
// 首先检查添加一个元素后是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 然后将其添加到数组的末尾,同时更新size
elementData[size++] = e;
return true;
}
其中ensureCapacityInternal()
的实现如下:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
它又首先调用calculateCapacity)()
来计算当前的elementData数组能否满足添加一个元素后所需的容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果elementData此时为默认的空数组,那么返回初始容量和添加后容量的最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 如果不为空,直接返回所需的容量
return minCapacity;
}
当得到了数组的容量后,再调用ensureExplicitCapacity()
进行检查:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 这里调用grow方法来创建所需的数组
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
其中grow()
方法的实现如下:
private void grow(int minCapacity) {
// 首先获取旧数组的长度
int oldCapacity = elementData.length;
// 以当前数组大小的1.5倍去扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容后仍不够,直接使用所需的容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果扩容后的容量大小超过数组的最大长度,使用Integer的最大值或是MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 确定了容量后才真正的创建数组elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
从上面的分析可知,ArrayList底层非空数组的真正创建是在第一次添加数据时才进行。
另外一种是在指定的位置添加元素的实现,源码实现如下:
public void add(int index, E element) {
// 首先检查指定的索引是否越界,如果越界直接抛异常
rangeCheckForAdd(index);
// 判断是否需要扩容
ensureCapacityInternal(size + 1);
// 这里调用的System中的arraycopy来进行数据的添加
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 添加数据,更新size
elementData[index] = element;
size++;
}
set()
用于将指定索引处的元素替换为指定的元素,它的实现如下:
public E set(int index, E element) {
// 首先检查传入的索引是否合法
rangeCheck(index);
E oldValue = elementData(index);
// 替换
elementData[index] = element;
return oldValue;
}
get()
用于获取指定位置的元素,实现如下:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
它同样是先检查传入的索引是否合法,如果不合法直接抛异常,合法则返回对应的元素。
ArrayList集合中元素允许重复,且元素之间是无序的。如果希望集合中的元素按照插入的顺序排列,以及对于频繁的插入或删除元素 的操作,LinkedList更为适合。LinkedList的实现形式为双向链表,它的内部实现采用的是内部类Node,将其作为存储数据的基本数据结构,而不是数组。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来指向下一个元素和上一个元素。
LinkedList中字段的定义如下,只有三个字段:
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;
LinkedList中只有两个构造方法,如下所示:
public LinkedList() {}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
一个无参构造和一个使用已有的Collection实现类对象进行构造的方法。
add()
同样有两种重载形式,其中不指定索引位置的实现如下:
public boolean add(E e) {
linkLast(e);
return true;
}
这里直接调用的是linkLast()
,将其默认添加到LinkedList的末尾。它的方法实现如下:
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++;
}
另一种指定位置的实现如下:
public void add(int index, E element) {
// 检查索引是否合法
checkPositionIndex(index);
// 如果指定的是size位置
if (index == size)
//和add(E e)的执行逻辑相同
linkLast(element);
else
// 否则插入到指定位置
linkBefore(element, node(index));
}
其中linkBefore()
的实现逻辑如下所示:
void linkBefore(E e, Node<E> succ) {
// 指定位置节点的前一个节点
final Node<E> pred = succ.prev;
// 使用要要添加的元素构造的Node节点
final Node<E> newNode = new Node<>(pred, e, succ);
// 将其作为当前位置的prev指向的节点
succ.prev = newNode;
// 如果为空,直接作为首个节点
if (pred == null)
first = newNode;
else
// 否则成功插入
pred.next = newNode;
size++;
modCount++;
}
整体的执行过程如下所示:
另外还有头插和尾插两种特殊的添加数据的实现,
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}
它们分别调用了linkFirst()
和linkLast()
,linkLast()
前面已经说完,它的执行过程如下:
重点看一下linkFirst()
的实现,如下所示:
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++;
}
实现方式大同小异,如果此时为空,直接作为头节点,否则将其添加到头部。具体的执行过程如下:
get()
的实现如下:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
先检查索引是否合法,再取出节点的元素值。
方法实现如下:
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
同样的是先检查索引是否合法,然后再取出指定位置的节点进行值的替换。
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;
}
具体的执行过程如下所示:
图片来自宋红康老师的笔记资料。
LinkedList的操作本质上就是底层双向链表的操作,如果理解链表的相关操作,理解LinkedList就很容易了。
Vector基本上和ArrayList是相同的,唯一区别在于Vector是强同步类,所谓强同步,就是方法上直接使用了synchronized进行同步。下面着重看一下它个ArrayList实现上的不同之处。
字段方面它有一个capacityIncrement,作用就是在扩容时指定扩容的大小。构造方法如下:
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 创建指定容量的Object数组
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector() {
this(10);
}
public Vector(Collection<? extends E> c) {
elementData = c.toArray();
elementCount = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
底层数组默认大小为10,但是这里在构造方法中就直接初始化了指定容量大小的数组。另外一个不同之处就是扩容的实现,如下所示:
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 如果指定了capacityIncrement,那么只增加相应的大小
// 否则扩容为当前容量的2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将原先的数据复制回去
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容实现要看capacityIncrement有没有设置初始值,如果没有,扩容为当前的2倍;如果有,增加capacityIncrement大小的容量。