写在前面
最近在学习java的底层代码,写博客是为了记录自己在学习过程中的一些笔记。这是我的第一篇博客,完成“写一篇博客”成就~~~
ArrayList
我们直接来看ArrayList的底层源码。其底层是使用Object数组实现的。会使用size来记录其容量。
transient Object[] elementData;
private int size;
由于其底层是使用数组实现的,因此插入和删除时,其时间复杂度受元素所在位置影响。
modCount是在使用迭代器遍历时的一个重要参数。如果modCount发生改变,那么迭代就有可能出现异常。
基本方法
首先我们先看基本的get,set,remove方法。
首先是add方法。我们看到,在调用add(int index, E element)方法时,首先我们会去检查是否越界,然后我们会将数组从index位置开始全部向后移动一位,然后将新的元素插入到index的位置。由于这里对index之后的元素都进行了移动, 因此我们说ArrayList的插入操作的时间复杂度为O(n)。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
我们注意到,在进行add操作时,都会调用ensureCapacityInternal方法。这个方法主要是进行扩容操作的。当ArrayList的数据存储的元素达到最大容量,这时我们想要继续向其中插入元素时,就需要对数据进行扩容。我们看一下这个方法的实现:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
首先调用的时calculateCapacity计算出所需最小容量:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
如果是默认设置的话,需要判断默认的容量和最小容量中大的数,否则就直接返回传入参数。
计算出最小容量后,我们就要调用ensureExplicitCapacity方法。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
可以看到这里对modCount进行了操作。至于操作有什么用处,后续讨论迭代器时我们再进行讨论。这里我们来判断数组的长度和最小容量比较,如果最小容量比数组长度大的话,我们就需要最底层数据进行扩容了。
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
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);
}
这里我们看到每次扩容都是增加原来容量的一半,如果最后最小的容量大于MAX_ARRAY_SIZE,就将容量设定为Integer.MAX_VALUE。
那么既然有了扩容的机制,有没有减少容量的方法呢?在阅读ArrayList源码过程中,我们发现有trimToSize这样一个方法。由于ArrayList的底层是使用数组实现的,因此数组会预留一定的空间插入新的元素。trimToSize就是将现在的容量减到现在数组中存储的数据个数,释放多余的存储空间。
/**
* Trims the capacity of this <tt>ArrayList</tt> instance to be the
* list's current size. An application can use this operation to minimize
* the storage of an <tt>ArrayList</tt> instance.
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
然后是set方法和remove方法。可以看到两个方法在调用前都会去检查是否越界。在调用remove时,我们发现这里也是将index之后的元素全部向前移动了一位。因此,ArrayList的删除操作的时间复杂度也是O(n)。
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
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;
}
如果我们需要查询某个元素是否在ArrayList中,就需要调用contains方法。我们来看一下contains的实现。这里看到,在调用indexOf方法查询list中是否存在元素时,需要从头到尾逐一遍历。因此查找的时间复杂度为O(n)。
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
ArrayList中的Iterator
我们在看ArrayList中的add和remove方法时,我们看到,这里每次都会对modCount进行++操作。那么这个modCount是干什么用的呢?另外,我们在对list进行遍历时,经常会有人提醒说不要在遍历的时候进行remove操作。若要在遍历时进行remove操作,一定要使用iterator中的remove方法进行操作,即Iterator.remove()。
我们通过一段实例代码来看一下删除的情况:
public static void main(String[] args) {
int n = 50;
List<Integer> ints = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
ints.add(i);
}
// 不会报错,但是删除的index不对
for (int i = 0; i < ints.size(); i++) {
if (30 < i && i < 40) {
ints.remove(i);
}
}
System.out.println(ints); // // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 49]
// 会报错
for (Integer anInt : ints) {
if (30 < anInt && anInt < 40) {
ints.remove(anInt);
}
}
System.out.println(ints);
// Exception in thread "main" java.util.ConcurrentModificationException
// at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
// at java.util.ArrayList$Itr.next(ArrayList.java:859)
// at com.fyk.collection.list.testListDelete.main(testListDelete.java:23)
ints.removeIf(anInt -> 30 < anInt && anInt < 40);
System.out.println(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
}
使用第一种fori的方式删除,虽然没有抛出异常,但是删除的元素并不是我们想要删除的30-40之间的数,结果不对。
使用第二种方式删除,直接就会抛出ConcurrentModificationException。我们知道,第二种方式实际上使用的就是Iterator进行遍历的,但是我们没有使用Iterator本身的remove方法,而是使用了ArrayList的remove方法,导致modCount发生了变化,抛出了异常。稍后会做解释。
而第三种调用的是Collection中的removeIf方法,我们看一下源码:
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
可以看到,其本身也是使用Iterator进行遍历,并使用iterator中的remove操作的,这里没有抛出异常,并且结果也是正确的。
那么为什么使用iterator的remove操作不会有这样的问题呢?我们从源码入手来进行分析。首先我们来看ArrayList中的Itr是怎么实现的。
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
我们看Itr中的三个属性,cursor是当前遍历的元素的位置,lastRet是上一次遍历的元素的位置。而这里又初始化了一个叫expectedModCount的属性,其初始值为modCount。那么这个属性有什么作用的?我们继续看Itr中的hasNext和next方法。
hasNex方法比较简单,就是判断一下当前的元素位置是否已经达到list的size。
public boolean hasNext() {
return cursor != size;
}
再来看一下next方法。这里首先会调用checkForComodification方法,然后将cursor加1,lastRet赋值为当前的cursor,然后返回数据。这里看到其中的位置指向已经发生了变化。
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
我们看一下checkForComodification方法,可以看到,这里判断了modCount和expectedModCount是否相等。如果不相等则会抛出异常。因此,如果我们在Iterator迭代的过程中,如果modCount发生了变化,即对数组进行了add,remove操作,这里迭代就会抛出异常。因此,使用Iterator进行遍历可以保证在遍历的过程中,数组的大小不会发生变化。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
我们再来看一下iterator中的remove操作,可以看到,我们在进行remove操作时,会将expectedModCount进行更新,另外会将cursor置为lastRet,将lastRet置为-1,这样就保证了在迭代的过程中,迭代顺序的正确性以及不会抛出ConcurrentModificationException这样的异常。这也是为什么要在遍历时使用Iterator来进行remove操作。同理,使用ListIterator进行遍历,可以在遍历的过程中进行add和set操作。原理与remove一样,因此不做赘述。
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();
}
}
ArrayList的其他源码就比较简单了,后面不再进行讨论。
总结一下:
- ArrayList的底层是使用数组实现的,因此随机访问快,时间复杂度为O(1);插入和删除慢,时间复杂度为O(n);
- ArrayList会自动进行扩容操作,每次扩容大小为当前容量的一半;
- modCount是保证在使用Iterator遍历时,可以对数组进行remove,add操作而不会出现异常。