List
List是Collection的一个子接口,其特点是元素有序且可重复。其主要的实现类有ArrayList、LinkedList和Vector。
首先来看看ArrayList:
从源码中可以看出,ArrayList实现了序列化接口,以及其底层是一个Object数组,并设置了一个默认容量大小为10。
接下来看下其构造器:
public ArrayList() {
this.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;
}
}
·空参构造器,即List list = new ArrayList<>();时会底层会创建一个空数组;
·int型参数构造器,会对参数的取值进行非法性判断,若合理则会创建一个对应的初始值大小数组;
·集合类型构造器;
创建好了集合之后我们肯定要对其操作,先看看其添加元素的方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, 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);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
·调用add方法添加一个元素,会先进行判断,调用ensureCapacityInternal(size+1);
·ensureCapacityInternal先调用calculateCapacity(eleData,minCapacity),然后调用ensureExplicitCapacity(int mincapacity)方法;
·calculateCapacity会得到数组的最小大小,若数组为空,则初始化为10;
·ensureExplicitCapacity会对需要的大小和数组大小进行比较,若超出了数组大小,则调用grow(minCapacity)方法
·grow方法会对数组容量进行扩展,容量扩展为原来的1.5倍,若仍比所需的容量小,则数组大小就为所需的容量大小,然后对此容量进行非法性判断,最后创建一个新数组,将原数组的内容复制到新的数组中。
添加完之后看看删除操作:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
因为底层是数组,所以我们可以根据索引进行操作。
·首先调用rangeCheck方法,检测索引是否越界;
·然后获取数组在这个索引上的值;
·然后把这个索引后面的内容都往前覆盖一个,将最后多出来的一个空位设置为null,返回移除的元素。
因此,我们会发现删除集合中间的元素速率较低,因为会频繁进行规模性的元素移动。
继续查看api,我们会发现这样的代码:
public boolean remove(Object o) {
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;
}
ArrayList也能根据内容移除元素,那么问题来了:
上面List中存储的是整型元素,移除3是根据索引还是内容呢?运行下:
索引越界异常,说明是根据索引进行的remove操作,接下来用 Integer(3)试试:
操作成功,说明了在remove操作时不会进行自动装箱和拆箱操作。
接下来看看ArrayList的查询操作:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
·首先检查索引是否越界;
·通过索引到数组中取;
我们知道,数组通过索引取值是非常迅速的,因此ArrayList的查询某个元素是非常快的。
总结:当使用ArrayList时,默认创建一个空数组,第一次进行add操作时会进行扩容,初始容量为10,后来add时若容量不够再次扩容,扩容大小为原数组的1.5倍,若仍然不足,则创建一个minCapacity大小的数组,然后将旧数组的内容复制到新的数组中。ArrayList的增删操作性能较低,会频繁的移动元素;查询效率较高,通过索引的方式就能取得。
LinkedList
先看看字段,我们会发现其内部维护了一个Node的节点类,并设置了头尾节点。
查看Node会发现是一个双向链表。
还是先看看其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的操作实际和链表的操作没什么区别,添加和删除某个元素的操作时间复杂度为O(1),查找某个元素为O(n);
Vector
我们发现vector和ArrayList很像,且是jdk1.0就存在的类。区别在于Vector是线程安全的类,其方法都是synchronized修饰的,也因此效率会低于ArrayList。
看下Vector的构造方法:
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
默认会创建一个初始容量为10的数组,这里就看出和ArrayList的差别,ArrayList是第一次add时才会创建初始容量10的数组,一种懒汉式、一种饿汉式。
接下来看看Vector的扩容机制:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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是在使用构造器时我们可以选择传入的参数,即自定义的扩容大小,若不进行传入,默认为0,每次扩容的新大小为原来数组的2倍。然后将旧数组的内容复制到新数组中。
然后就是size方法:
public synchronized int size() {
return elementCount;
}
返回的是成员变量中的elementCount,ArrayList中就是size变量,记录了当前使用到的数组索引,而非数组的大小。
总结:当我们需要频繁的查找操作时,使用ArrayList更为合适,若需要线程安全可以用Vector或者Collections工具类的synchronizedList方法;当要频繁的在集合中插入或删除操作时使用LinkedList更为合适。