集合是java基础中非常重要的一部分,集合底层的实现也是值得我们细细的品味,集合也是面试中常见的问题。我们从ArrayList开始探究,这也是该系列的第一个内容。将对集合的理解分为三个级别:
第一级别:入门,即刚学java仅知道他的一些特性,存储特性,查询特性等
第二级别:进阶,对底层数据结构有一些了解,扩容机制,增删机制等
第三级别:品味,知道实现之后就去翻读源码,了解底层的真正实现,看源码时常常有恍然大悟之感,哦~原来是这么实现的,妙啊!
(提前申明:源码来源于jdk1.9)
入门:
①ArrayList是一个有序可重复的集合,他的有序即是取出来的顺序和存进去的顺序是一致的。
②list可以通过iterator()方法获得一个该list的迭代器,遍历时可以调用迭代器的remove()方法,不能调用该list本身的remove()方法不然会报异常ConcurrentModificationException。
ArrayList list= new ArrayList();
list.add("a");
list.add("b");
list.add("c");
Iterator iterator=list.iterator();
while(iterator.hasNext()) {
//在迭代器中调用集合的remove方法就会报异常
list.remove(iterator.next());
}
但是我们修改一下,将循环遍历中的方法改为如下
while(iterator.hasNext()) {
//这样就可以成功的将集合元素全部删除
iterator.remove();
}
补充:在foreach代码中也不可以调用list的remove方法,因为foreach在底层遍历集合时使用的也是迭代器iterator,遍历数组时使用的又是普通的fori循环。
③相比较于LinkedList,ArrayList查询快,增删慢。LinkedList增删快,查询慢。
ArrayList 进阶:
①ArrayList底层数据结构是数组,所以读取快
②ArrayList的默认数组大小是10,每次扩容为原大小的1.5倍(使用了位运算符)
③既然ArrayList底层使用的是数组,那么他是怎么实现集合的添加删除的操作的呢?
1.添加,添加中又分两种添加,
(1)直接在数组末尾添加:ArrayList底层有一个属性size表示集合元素的多少,所以直接在size++的位置添加元素即可。
(2)指定数组下标的的添加:使用System.arrayCopy方法进行操作
如下一个length为6,size为4的数组拥有四个初始值
这时候需要在3和5之间添加一个一个元素4,你会怎么添加呢?首先需要将5和6向后移动一个位置,如下图
再将4添加到空出来的位置。
有了上述的思路后,再想怎么删除就比较简单了。 还是对这个数组进行操作,这时候需要将3删除。
只需要将5,6向前移动一个位置,直接将3覆盖了就可以了。变成如下
④fast-fail机制,当使用迭代器遍历集合时不能修改集合,否则会抛出ConcurrentModificationException,底层通过维持一个修改次数的属性modCount,每次修改modCount++,当获取迭代器时,将当前的modCount记录,赋值给一个迭代器内部维护的属性expectedModCount(期望的修改次数),每次迭代先判断,保证没有进行过修改。
ArrayList 终极进阶:看源码
//默认的数组大小
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放数据的数组
transient Object[] elementData; // non-private to simplify nested class access
表示当前list中元素的个数
private int size;
上面有两个空数组,是不是很懵逼,没关系,我们再看一段源码
//如果是调用含参构造器且参数为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);
}
}
//如果是调用空参构造器,则使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
在不同的构造器中分别将两个不同的空数组引用赋值给了elementData,通过阅读属性上的注解可以知道他们的用途是判断扩容是要扩充到多少。
抱着刨根问底的心态,我们去寻找扩容的代码。既然涉及到了扩容那么肯定是从add()方法出发了。
//public的add()方法调用了private的add()方法将当前的elementData和size已经需要添加的传参
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
//满了就扩容,走grow()方法
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
//每次添加size++
size = s + 1;
}
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//计算扩容后数组的大小,扩容为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果计算出来的newCapacity比size+1还小,那肯定不能用newCapacity
if (newCapacity - minCapacity <= 0) {
//就是这儿,如果elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//直接返回DEFAULT_CAPACITY和size+1中的最小值
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
//判断是否超过最大值
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
奇怪的知识增加了
我们现在来看一下关于集合的增加和删除的源代码,就能更加深刻的理解上面所说的理论了。在这之前我们先了解一下System.arraycopy()方法。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
该方法是一个本地方法,我们不用关注其实现的细节,只需要了解他的参数和结果即可。
总共有五个参数,见名知意
1. src 需要拷贝的原数组
2. 拷贝的原数组的起始位置
3. 目标数组(复制的目的地)
4. 目标数组的起始位置
5. 复制的长度
这里不做过多的介绍,还是不了解的可以先去IDE敲一敲,这种自己想两个案例敲两遍就记住了,看再多遍也还是会忘。
public E remove(int index) {
//检查传入的index是不是比size-1大
Objects.checkIndex(index, size);
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;
}
public boolean remove(Object o) {
if (o == null) {
//移除null值的方法
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//移除非null值的方法
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
//见名知意,是一个快速根据下标删除的方法,但是方法移除的方法都是一样的(覆盖)
//他快就快在不需要index范围的检查,与remove(int index)最大的区别就是这个
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
}
最后我们来看一看ArrayList的迭代器
平时我们都是通过list对象的iterator()方法来获得对应的迭代器
public Iterator<E> iterator() {
return new Itr();
}
看看这个Itr();是个什么类?点进去后发现是ArrayList的一个private修饰的内部类实现了iterator接口
private class Itr implements Iterator<E> {
//遍历到的元素下标
int cursor; // index of next element to return
//上一个元素的下标
int lastRet = -1; // index of last element returned; -1 if no such
//期望的修改次数,用于判断集合是否在迭代的时候进行了修改
int expectedModCount = modCount;
看了这三个属性,其实就很清楚Itr的实现原理了,通过记录下一个访问元素的下标来进行遍历,通过expectedModCount来保证集合在遍历过程中不会进行修改,每次遍历时只需要将expectedModCount与ArrayList中的modCount进行比较。我们再去看源码,就会发现源码其实很简单。
public boolean hasNext() {
//当前位置不等于size就有下一个
return cursor != size;
}
//检查是否进行过修改,下面的好多方法都会调用到这个方法
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public E next() {
checkForComodification(); //fast-fail
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];
}
//迭代器内部的移除元素方法,仅能移除当前元素
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); //fast-fail
try {
//调用这个方法会使list中的modCount++
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
//所以每次调用都修改expectedModCount为modCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
看了iterator中的remove方法就知道
ArrayList还可以通过listIterator()和listIterator(int index) 方法来获取一个可以向前遍历的迭代器,具体源码就不做展开,内容也很简单有兴趣的可以自己去看看,该方法返回的又是另一个类ListIterator,可以指定下标开始
public ListIterator<E> listIterator(int index) {
rangeCheckForAdd(index);
return new ListItr(index);
}
public ListIterator<E> listIterator() {
//有趣的是该类没有空参构造器,所以必须传参数
return new ListItr(0);
}
//继承了Itr实现了ListIterator
private class ListItr extends Itr implements ListIterator<E> {}
//ListIterator继承了Iterator,不过是添加了向前遍历的方法规范
public interface ListIterator<E> extends Iterator<E> {}
希望本文对大家有所帮助,都是自己手撸的,也算是对集合有了更深的理解,有什么错误大家在评论区批评指正一下哈,感激不尽。