之前的一篇博客讲了List的主要实现类,这次我们来详细的讲一下实现类中的ArrayList和LinkedList。这两个实现类是在日常开发中经常使用到的,因此我会深入一点去讲一些底层的东西,以便能更好的理解掌握着两个实现类。
ArrayList
ArrayList与数组基本一致,但是ArrayList解决了数组长度不可变的问题。数组一旦创建,就无法再进行长度的扩展。并且数组只能存储基本数据类型,当我们需要存储对象时,数组就无法满足我们的需求。
ArrayList实现了基于动态数组的数据结构,首先我们来看看ArrayList的属性。
//可序列化版本号
private static final long serialVersionUID = 8683452581122892189L;
//默认的初始化数组大小 为10 .
private static final int DEFAULT_CAPACITY = 10;
//实例化一个空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//存放List元素的数组
private transient Object[] elementData;
//List中元素的数量,和存放List元素的数组长度可能相等,也可能不相等
private int size;
可以看出,ArrayList的主要底层结构是一个Object类型的数组,这也就解释了为什么ArrayList可以存放对象的原因。
这里有一个知识点,ArrayList实现了Serializable接口,因此它是可以序列化的,为什么elementData是用transient修饰的。原因在于ArrayList扩容时通常会预留一些容量(扩容的方式下边再讲),如果直接将elementData进行序列化,可能会将一些空值也序列化进去。为了节省序列化的空间和时间,只序列化elementData里不为空的值以及size。我们可以通过writeObject和readObject两个方法来理解。
//序列化
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
//反序列化
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
使用ArrayList肯定离不开构造函数,它的创建方法有三种:
ArrayList() 构造一个初始容量为十的空列表。 |
ArrayList(Collection<? extends E> c) 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。 |
ArrayList(int initialCapacity) 构造具有指定初始容量的空列表 |
我们通过它的源码来看看它是如何具体运行的:
/**
* 根据指定的容量初始化空的列表,注意当容量为 0 时,使用的是 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);
}
}
/**
* 初始化容量为 10 的空列表
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*/
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一些常用的方法。
返回类型 | 方法名(参数) |
---|---|
boolean | add(E e) 将指定的元素追加到此列表的末尾。 |
boolean | addAll(Collection<? extends E> c) 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。 |
void | clear() 从列表中删除所有元素。 |
E | get(int index) 返回此列表中指定位置的元素。 |
int | indexOf(Object o) 返回此列 |
Object | clone() 返回此 ArrayList实例的浅拷贝。 |
boolean | contains(Object o) 如果此列表包含指定的元素,则返回 true 。 |
boolean | isEmpty() 如果此列表不包含元素,则返回 true 。 |
E | remove(int index) 删除该列表中指定位置的元素。 |
boolean | remove(Object o) 从列表中删除指定元素的第一个出现(如果存在)。 |
int | size() 返回此列表中的元素数。 |
void | sort(Comparator<? super E> c) 使用提供的 |
List<E> | subList(int fromIndex, int toIndex) 返回此列表中指定的 |
Object[] | toArray() 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组 |
以上是ArrayList中常用的方法,其他全部的方法可以通过Java8 API查看。
在上边中,我们需要关注两个方法,分别是add和remove。首先我们来看看add方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
可以看到,在添加元素之前,add方法先调用了一个ensureCapacityInternal方法,这就跟我接下来要说的ArrayList的存储与扩容有关了。
ArrayList的扩容与存储
上边说过ArrayList与数组最大的区别就在于ArrayList是长度可变的,这个长度可变就是通过扩容实现的。我们先来看ensureCapacityInternal方法以及后续调用方法的源码。
// 内部数组的默认容量
private static final int DEFAULT_CAPACITY = 10;
// 空的内部数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//内部数组最大的大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void ensureCapacityInternal(int minCapacity) {
//判断当前ArrayList内部数组是否为空
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//内部数组的容量判断
ensureExplicitCapacity(minCapacity);
}
//内部数组的容量判断
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//数组扩容
grow(minCapacity);
}
//数组扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//容量扩充公式
int newCapacity = oldCapacity + (oldCapacity >> 1);
//针对新容量的一系列判断
if (newCapacity - minCapacity < 0){
newCapacity = minCapacity;
}
if (newCapacity - MAX_ARRAY_SIZE > 0){
newCapacity = hugeCapacity(minCapacity);
}
//复制旧数组元素到新数组中去
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0){
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
从上边代码我们可以知道ArrayList的扩容过程:
- 先判断ArrayList数组的内部数组是否为空,如果为空则将最小容量设置为10。
- 如果数组不为空,判断添加元素后是否会导致数组溢出。
- 如果数组容量不足,则进行扩容操作。
在扩容操作中,我们可以看到首先扩容的方式是,即新扩展的容量是就扩展的容量的1.5倍。然后用这个扩容后的容量去进行判断是否能够存储需要添加的所有元素。从代码中可以看到进行了两次判断。
- 如果新的容量仍不满足需要添加的元素的数量,则直接将数组扩容至恰好能存下所有元素的长度。
- 如果扩展后的容量超过了ArrayList初始定义的最大容量,则将容量扩容为Integer.MAX_VALUE。
以上就是add操作的过程,接下来我们来看看remove操作。首先我们来看看remove的源码:
public boolean remove(Object o) {
//若o为null,没有equals方法,因此需要先判断是否为空
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
//简单来说就是将删除元素后边的元素前移1位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
remove操作相对简单,需要注意的知识点在于删除时会先进行判空,因为如果对象为null,是没有equals方法,无法通过equals比较对象。从代码中也可以看到,删除的实质其实就是元素往前覆盖,当删除完毕后,会将最后一个元素置空,方便GC(垃圾回收器)回收。
LinkedList
LinkedList的底层是双向链表(实现了Deque接口),Deque接口继承自Queue接口,因此在java中如果想要实现队列,通常使用LinkedList来实现。接下来我们先来看看LinkedList的属性:
//链表的长度
transient int size = 0;
//指向链表头部的结点
transient Node<E> first;
//指向链表尾部的结点
transient Node<E> last;
//结点类
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有两个结点,分别指向首尾,这也就是双向链表的两个指针,size则存放着链表的长度。我们可以通过下图了解LinkedList内部具体的连接结构
接着是LinkedList的构造函数,它的构造方法有两个:
LinkedList() 构造一个空列表。 |
LinkedList(Collection<? extends E> c) 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。 |
分别是一个无参构造和一个通过Collection类进行初始化的构造方法,源码如下:
public LinkedList() {}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
下边我们来了解一下LinkedL常用的方法
我们主要通过add,remove三个方法来了解LinkedList如何进行增删改。首先是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++;
}
从源码中看一看出,在添加前先读取LinkedList对象中的最后一个结点,如果非空,则在最后一个结点后插入,反之证明链表为空,因此添加的结点作为第一个结点。
然后是remove操作,话不多说上源码:
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
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的删除就是结点中的next结点以及prev结点的操作。如下图所示,要删除B结点,将A结点的next指向C结点,将C结点的prev指向A结点,然后将B结点的值置空。这里的置空是为了方便GC回收。
总结
ArrayList和LinkedList的异同
- 从继承结构来看,LinkedList继承自AbstractSequentialList,而ArrayList继承自AbstractList。
- 从底层结构来看,LinkedList是基于双向链表,而ArrayList是基于动态数组。
- 两者都是线程不安全的,如果要在多线程环境下使用,需要使用Collections.synchronizedList()进行包装
ArrayList和LinkedList的使用环境
对于随机访问,因为ArrayList是基于数组的,ArrayList比LinkedList的性能更优。因为ArrayList可以通过数组下标访问,而LinkedList必须通过遍历。
对于增删数据,LinkedList采用链表的数据结果,增加和删除元素都是通过更改指针的指向,而ArrayList是通过元素的前移,因此LinkedList在增删数据方面比ArrayList更优