赞
踩
写在最前,本人也只是个大三的学生,如果你发现任何我写的不对的,请在评论中指出。
List, 在java里是一个接口,其常见的实现类有ArrayList
和linkedList
, 但是一般在编程中用的最多的还是ArrayList
,究其原因,ArrayList
的底层是数组, LinkedList
的底层是双向链表, 而我们的开发需求中遍历的需求比增删要多,即使是增删也一般是在List的尾部添加元素即可,像是尾部添加元素,ArrayList
的时间负责度也不过是O(1)。
对于ArrayList
我们可以关注的第一个点就是:
java本身就存在数组了, 为什么要采用ArrayList呢?
首先不说ArrayList
封装了多个遍历的方法便利了我们平日里的开发, 在使用原生数组的时候会有一个特点:在你创建它的时候必须为它规定数组的大小,而ArrayList
不用。
// 首先我们观察源码就会发现ArrayList实现数据存储的数据结构就是数组
transient Object[] elementData;
// 其次ArrayList在创建的时候不需要规定底层数组大小的原因是它的无参构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA是个默认大小的空实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
需要注意的是当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量会扩容到默认大小10,至于这个第一次添加才真正扩充的操作也很简单,现在就带你看一看:
// 当你第一次添加元素时 public boolean add(E e) { ensureCapacityInternal(size + 1); //这一步 点进去 elementData[size++] = e; // 扩容到10后追加到尾部 return true; } // 在ensureCapacityInternal()方法内部我们会看到该方法 /** @param elementData 存储数据的数据结构 * @param minCapacity 当我们第一插入元素时, size为0 传入时是1 **/ private static int calculateCapacity(Object[] elementData, int minCapacity) { // if判断当前数组是否是默认的空数组 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 获取默认的容量和传入参数的较大值 // 此时不用想都知道返回DEFAULT_CAPACITY(默认大小10) return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
其次,由于ArrayList底层实现也是数组,但它却不是规定死的,而是可以动态扩容的
ensureExplicitCapacity(int minCapacity)方法
如果你自己跟着add()方法的源码一步一步走进去, 肯定会遇到这个方法:
// 判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
// 调用grow方法进行扩容, 调用此方法表示已经开始扩容了
grow(minCapacity);
}
我们来分析一下:
ensureCapacityInternal()
方法,所以此时minCapacity - elementData.length > 0 成立,所以会进入grow(minCapacity)
方法grow(minCapacity)
方法grow(minCapacity)
方法,数组的容量都为10直到添加第11个元素, minCapacity大于elementData.length的时候才会进行扩容。
group()方法
private void grow(int minCapacity) { // 保存旧容量大小 int oldCapacity = elementData.length; // 将oldCapacity右移一位,相当于/2, 写成这样是因为位运算更快 // 整句的意思在扩容到原大小1.5倍, 如果是奇数的计算会丢掉小数点 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); } // 另外hugeCapacity()方法 会比较minCapacity和MAX_ARRAY_SIZE。 //如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。
也就是说:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
),所以此时数组容量为10疑难杂惑:
我们在阅读ArrayList
源码的时候会大量阅读到这两个函数, 比如一个出现在新增元素到指定下标的add()方法里
/**
*@param: elementData:源数组;
*@param: index:源数组中的起始位置;
*@param: elementData:目标数组;
*@param: index + 1:目标数组中的起始位置;
*@param: size - index:要复制的数组元素的数量;
**/
System.arraycopy(elementData, index, elementData, index + 1, size - index);
还有个就出现在上面的grow()方法内:
// arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组
elementData = Arrays.copyOf(elementData, newCapacity);
好的到了这里就剩下一个困惑了, 为什么保存数据的elementData要被transient修饰呢?
大家都知道被transient修饰后的变量不会被序列化,但是要知道ArrayList
是实现了Serializable这个标记接口的,难道我们在序列化一个ArrayList
之后会读不到它的数据嘛? 肯定是不会这么荒谬的。 要知道,ArrayList通常都会预留一些存储容量,就比如说你new ArrayList<~>(); 之后add()操作四次后, 我们都知道它底层elementData.length此时长度为10, 所以就会存在一些空间是没有存储空间的,那么你直接序列化它不就浪费了额外的空间和时间了么?,我们来看源码
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // 将Size写到ObjectOutputStream s.writeInt(size); // 因为ArrayList是可扩容的,所以可能会存在一些没被利用的空间,所以这里只存有值的空间 for (int i=0; i<size; i++) { //size是实际存储对象个数 s.writeObject(elementData[i]); //有序的将element中已使用的对象读出到流中 } 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) { // 就像clone()一样,根据大小而不是容量来分配数组 int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); ensureCapacityInternal(size); Object[] a = elementData; // 正确的顺序读入所有对象. for (int i=0; i<size; i++) { a[i] = s.readObject(); } } }
ArrayList讲完了, 可能还要看一眼Vector
Vector的底层数据结构也是数组(Object[] elementData;
),但现在基本上看不到这位了。 相较于ArrayList,它是线程安全的,扩容的时候它会直接扩容两倍。 在Vector中,方法基本都被synchronized
修饰,导致它的速度非常慢。
那还存在其他的线程安全的List么?
有,在java.util.concurrent包下还有一个类,叫做CopyOnWriteArrayList
, 整个类的行为我觉得和文件系统非常相似, 文件系统在修改数据的时候,不会直接在原来的数据上进行操作,而是重新找个位置修改。比如:要修改数据块A的内容,先把A读出来,写到B里面去。这时候如果电脑挂了,原来A的内容还在,并且只有在真正发生修改的时候,才去分配资源,简单来说就是懒加载。
再回来看看CopyOnWriteArrayList,它是一个线程安全的List,底层是通过复制数组来实现的。
直接看到它的add方法:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
在add()方法他会加上lock锁,然后会复制出一个新的数组,往新的数组里add真正的元素,最后把array的指向改变为新的数组。并且如果你去观察get()方法或者size()方法只是获取array所指向的数组的元素或者大小。读不加锁, 写锁。
但是这样大家应该明白它缺点是很耗费内容的,那次新增都复制一个数组。并且它还不能保证数据的一致性,假设A线程去读取数据,还没读完,现在B线程去清空了List,线程A照样能把剩余的数据读出来。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。