ArrayList源码&扩容机制分析

ArrayList底层是数组队列,相当于动态数组。

  在增加大量元素前,应用程序可以使用ensureCapacity操作来增加ArrayList实例的容量。

  ArrayList继承于AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable接口。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
}

RandomAccess:一个标志接口,表明这个接口的List集合是支持快速随机访问的,可以通过元素序号快速获取元素对象。

Cloneable:覆盖了clone(),能被克隆。

java.io.Serializable:ArrayList支持序列化,能通过序列化去传输。

  序列化: 将数据结构或对象转换成二进制字节流的过程
  反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

 

ArrayList和Vector的区别:

  ArrayList 是 List 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 (ArrayList中任何方法都没有任何加锁操作,在多线程写时可能会出现值覆盖或者空指针异常等问题,有fail-fast iterable异常);

  Vector 是 List 的古老实现类,底层使用 Object[ ]存储,线程安全的(但Vector实现线程安全问题是通过给add()方法加上了synchronized,让其add方法变成一个同步方法。这样做虽然保证了线程安全,但牺牲了不小的性能)。

ArrayList线程安全吗?怎么解决?

  创建一个ArrayList,然后创建两个线程,每个线程for循环1000次向公共的List里面添加数据,在一个线程读取List当前的大小之后,另一个线程可能已经对List进行了修改。这样就可能导致数据的不一致性,例如一个线程读取到的List大小已经被另一个线程修改了,因此,在这个案例中,最终的列表大小可能不是期望值2000,而是一个小于或大于2000的随机值。这正是ArrayList线程不安全的一个典型表现。

解决1:Vector,底层也是数组,但它是线程安全的,通过给add()方法加上了synchronized,让其add方法变成一个同步方法。

解决2:使用CopyOnWriteArrayList:CopyOnWriteArrayList是Java提供的一个线程安全的并发集合类,它通过每次添加、修改或删除元素时创建一个新的底层数组来实现线程安全,这种机制叫做写时复制

  写时复制思想, 比如add()方法中,是将原件复制出来一份,然后在复印件上写,之后通过setArray()方法让原件地址指向复印件,这样可以让所有人读原件,而我只修改复印件,所以读和写不会出现冲突,因此通过加锁和写时复制思想可以很好保证了多线程情况下所有线程都可以读,但是只有一个线程在写,因此不会出现并发修改异常,源码:

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(); // 解锁
    }
}

 

ArrayList与LinkedList的区别:

  1.是否保证线程安全:两个都是不同步的,不保证线程安全。

  2.底层数据结构:Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别)

  3.插入和删除是否受元素位置影响:

     ① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。

    ② LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。

  4.是否支持快速随机访问:ArrayList底层基于数组,支持快速随机访问。LinkedList不支持随机访问,访问非链表首尾得元素比较低效。

  5.内存空间占用:ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

ArrayList扩容机制:

  从ArrayList的构造函数说起,ArrayList有三种方式初始化:

//1.默认构造函数,使用初始容量10构造一个空列表(无参数构造)
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
//2.带初始容量参数的构造函数。(用户自己指定容量)
    public ArrayList(int initialCapacity) {}
//3.构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
    public ArrayList(Collection<? extends E> c) {}

  以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。

//ArrayList的扩容机制
//ArrayList的扩容机制提高了性能,
//如果每次只扩充一个,那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。
    /**
     * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量.
     * @param   minCapacity   所需的最小容量
     */
  //最好在向 ArrayList 添加大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数.
public void ensureCapacity(int minCapacity) { //如果是true,minExpand的值为0,如果是false,minExpand的值为10 int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; //如果最小容量大于已有的最大容量 //申请容量比当前数组元素的实际长度大 则进入方法 grow if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } } //1.得到最小扩容量 //2.通过最小容量扩容 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方法进行扩容,调用此方法代表已经开始扩容了 grow(minCapacity); } /** * 要分配的最大数组大小 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * ArrayList扩容的核心方法。 */ private void grow(int minCapacity) { // oldCapacity为旧容量,newCapacity为新容量 int oldCapacity = elementData.length; //将oldCapacity 右移一位,其效果相当于oldCapacity /2, //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, int newCapacity = oldCapacity + (oldCapacity >> 1); //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //再检查新容量是否超出了ArrayList所定义的最大容量, //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 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); } //比较minCapacity和 MAX_ARRAY_SIZE private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } //Arrays.copyOf(),主要是为了给原有数组扩容 public static int[] copyOf(int[] original, int newLength) { // 申请一个新的数组 int[] copy = new int[newLength]; // 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }

 

 总结:

  ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。(不是原数组,而是新数组然后给予数组对象地址)。

  默认情况下,新的容量会是原容量的1.5倍。 新容量=旧容量右移一位(相当于除于2)在加上旧容量。

  ArrayList 的底层是用动态数组来实现的。我们初始化一个ArrayList 集合还没有添加元素时,其实它是个空数组,只有当我们添加第一个元素时,内部会调用扩容方法并返回最小容量10,也就是说ArrayList 初始化容量为10。

  ArrayList扩容主要是调用ensureCapacity()方法,当当前数组长度小于最小容量minCapacity的长度时(前期容量是10,当添加第11个元素时就就扩容),便开始可以扩容了,ArrayList 扩容的真正计算是在一个grow()里面,新数组大小是旧数组的1.5倍,如果扩容后的新数组大小还是小于最小容量,那新数组的大小就是最小容量的大小,再检查新容量是否超出了ArrayList所定义的最大容量,如果超出了就调用hugeCapacity()来比较最小容量和 MAX_ARRAY_SIZE,如果最小容量大,新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。后面会调用一个Arrays.copyof方法,这个方法是真正实现扩容的步骤,在此方法中会调用arraycopy方法,将原数组拷贝到新数组后返回。

posted @ 2023-04-17 22:56  壹索007  阅读(30)  评论(0编辑  收藏  举报