前言:本篇文章的分析是基于jdk1.8
ArrayList分析
对于ArrayList 网上对于其分析也比较常见,其源码也比较简单,我这里只是记录下一些开发中容易遗漏和采坑的点
ArrayList 主要包括的其实就是一个Object 类型的数组,一些参数信息
1、关于构造方法,ArrayList 提供的构造方法,
我们比较常用,无参构造方法
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这个构造方法很简单,创建一个长度为10 的空数组
指定初始化长度的构造方法
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;
}
}
这个也比较容易理解,就是把集合中的数组拷贝一份到当前数组中去
2、对于ArrayList我们主要看下他的扩容方式
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_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);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity); //⑤
}
① 判断当前list 中数组是否为空数组,如果是空数组,就判断新的长度和10 哪个大,取两者中较大值,作为新的容量
②判断新的容量和数组实际存储的数据长度哪个大,如果新的容量大于数组长度,进行扩容
③ 在进行扩容操作前,获取到之前的数据长度
④ 数组长度1.5倍增加,如果1.5倍后还是小于新扩容的长度,那么将新的容量作为数组长度,如果新的容量比Integer.MAX_VALUE-8 还要大,那么将数组的长度制定为Integer.MAX_VALUE
⑤ 将原来的数组拷贝到新的制定长度的数组当中。
可以看到ArrayList 对使用者是变长的,但是其底层是通过数组拷贝的方式来进行扩容,如果我们在开发中,不停地追加数据到ArrayList 中,会造成性能的极大损耗,建议在初始化时,给定一个长度。
3、ArrayList 查询方式
ArrayList 继承了RandomAccess 接口,我们看下这个接口
public interface RandomAccess {
}
这个接口是个markerInterface ,javadoc 中表明,实现这个接口,支持快速的随机访问,主要目的是允许通用算法在应用于随机或顺序访问列表时改变其行为以提供良好的性能。
通俗点说就是使用for循环遍历会比Iterator要快
for (int i=0, n=list.size(); i < n; i++)
list.get(i);
runs faster than this loop:
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();
4、同为ArrayList为什么这个就不可以修改?
通过Arrays.asList 创建的List 往上面添加元素时,会报错,我们来看下Arrays.asList 方法
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
没有问题啊,就是创建了一个ArrayList ,但是此List非彼List
这个ArrayList 是Arrays 的一个私有的静态内部类,其中并没有提供add方法,当我们调用add方法时,实际上是调用了父类AbstractList的add方法
public boolean add(E e) {
add(size(), e);
return true;
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
直接抛出异常,所以Arrays.asList 构建出来的ArrayList ,其长度是固定的,不能添加,也不能删除新的元素。
5、iterator 内删除集合中的元素为什么给我报错?
public static void main(String[] args) throws Exception{
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
for(Iterator<Integer> it = list.iterator();it.hasNext();){
Integer i = it.next();
list.remove(i);
}
}
list 是可以删除元素的,为什么在这里会报出ConcurrentModificationException
我们看下ArrayList中的Iterator
ArrayList 内部类
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这个是Iterator的remove方法。check方法中来判断modCount 和 exceptedModCount, modCount 这个字段基本在集合中都存在,用来记录对集合操作的次数(增加和删除,不包括查询),这里的modCount 是ArrayList 中的,exceptedModCount 在创建Itr 对象时,拿到ArrayList 中的modCount 进行赋值 。
我们这个里面,在iterator 循环中调用list 的remove方法
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;
}
private void fastRemove(int index) {
modCount++;
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
}
可以看到fastRemove 会把modCount++,
也就是说,在第一个元素进来的时候,我们执行it.next(), list.remove() 是没有问题的,此时modCount和exceptedModCount是相同的,
第二个元素进来之后,执行next() 方法是,会再次校验,此时list中的modCount 已经加1 和 exceptedModCount值不相同,因此抛出异常。
正确的方式应该是使用iterator 的remove方法。
remove(Object)采用for循环的方式遍历到这个对象的索引,然后将数组中该索引对象置为null, 等待gc 清除数据
最后,如果想要清空list中的数据建议直接使用list.clear() 方法,而不是for 循环使用索引删除。
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;
}
}
其内部维护的就是Node对象,包含前节点,后节点,因此LinkedList 要比ArrayList 更加占用内存,
LinkedList 是循环双向链表,当我们根据索引去查找某个具体的数据时,是非常耗时的
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
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;
}
}
如果索引小于长度的一半,从头部开始进行遍历,一直遍历到索引处,如果索引大于长度的一半,从尾部开始遍历,直到当前索引处,可以想象如果LinkedList 长度非常大时,我们又想获取到居于中间某个索引,或者说是for循环通过索引方式去遍历,那么查询速度是非常慢的,对于ArrayList ,直接根据索引,作为数组下标查找到具体元素,可以说用LinkedList 进行查找会非常慢。对于LinkedList的遍历建议使用Iterator方式,要比for循环索引下标的方式快。
对于添加操作:
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 的添加操作非常简单,就是创建好新的节点,然后将前后节点的关联关系创建好,但是对比于ArrayList 的创建,需要先判断数组长度是否足够,不够的话,进行扩容,然后再将原来的数据进行一份拷贝,相比较于LinkedList,插入操作是更加损耗性能。
对于删除操作:
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中的某个元素,LinkedList 采用遍历的方式,找到当前元素在LinkedList, 获取到前后节点,重新修改前后节点所关联的节点即可,而ArrayList 在remove 一个对象之后,还需要再进行数据的拷贝移动,这一操作会造成性能下降。
总结:
1、ArrayList 方便进行元素的查找,对于添加元素和删除元素会有性能上的损耗
2、LinkedList 方便进行元素的增加和删除,不易于查找。
关于Vector ,它的基本实现和ArrayList相同,唯一不同的就是在大部分方法上添加了synchronized 关键字,来保证了线程安全,但是当数组元素很多的时候,在方法上添加synchronized关键字,会降低vector的使用效率,所以不建议使用。