Java容器深度总结:ArrayList

没有一个冬天不可逾越,没有一个春天不会来临。

1.ArrayList概述

ArrayList内部是使用动态数组实现的,也就是说,ArrayList封装了对内部数组的操作,比如向数组中添加、删除、插入新的元素、扩容等。

ArrayList类图:
在这里插入图片描述

ArrayList定义:

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

从ArrayList的类图和定义,我们能获取到以下信息:

  • ArrayList< E > : 说明ArrayList支持泛型,所以只能存放引用类型数据。
  • extends AbstractList< E > : 继承了AbstractList,此类提供 List 接口的骨干实现,以最大限度地减少实现”随机访问”数据存储(如数组)支持的该接口所需的工作。
  • implements List< E >:实现了List接口,该接口包含了有关列表的基本操作,意味着ArrayList元素是有序的,可以重复的,可以有null元素的集合。
  • implements RandomAccess: 标识ArrayList支持快速随机访问,此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。
  • implements Cloneable:表明ArrayList可以通过调用clone()来克隆他的对象。
  • implements java.io.Serializable:表明ArrayList支持序列化。
2.ArrayList成员变量

ArrayList最主要的成员变量有以下两个:

  • transient Object[] elementData : 用于存放元素的数组。
  • private int size :数组实际元素的个数。

需要注意的是:elementData.length 为数组的容量。

在这里插入图片描述
如图:该数组的容量为10,size为9(实际只保存了9个元素)。

其余成员变量有:

  • private static final int DEFAULT_CAPACITY = 10:默认容量。
  • private static final Object[] EMPTY_ELEMENTDATA = {}:空数组。
  • private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}:默认空数组。
3.ArrayList构造方法和初始容量

ArrayList的构造方法有三个:

  • ArrayList()
  • ArrayList(int initialCapacity)
  • ArrayList( Collection<? extends E> c)
3.1 ArrayList()
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA  = {};

//elementData 用于存放元素的数组
transient Object[] elementData;

//since 1.7
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

当我们创建一个空ArrayList集合时,其数组为空数组,即初始化容量为0。DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是 ArrayList的成员变量,elementData是真正用于存放元素的数组。

Tip:

  • JDK1.6时,调用ArrayList无参构造会初始化一个容量为10的数组 。
  • JDK1.7开始,调用ArrayList无参构造会直接赋值一个空数组,第一次调用add元素时,会扩容到10(扩容机制)
// since 1.6 
public ArrayList() {
    this(10); // 调用ArrayList(int initialCapacity) 构造函数,初始化容量10
}
3.2 ArrayList(int initialCapacity)

initialCapacity用于指定ArrayList的初始数组容量

private static final Object[] EMPTY_ELEMENTDATA = {};

public ArrayList(int initialCapacity) {
        
    if (initialCapacity > 0) { // 初始容量大于0
          this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) { //初始容量为0
            this.elementData = EMPTY_ELEMENTDATA;
    } else { //初始容量小于0
          throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
    }
}
  • 如果指定初始容量大于0,则构建指定长度的空数组并赋值给elementData。
  • 如果指定初始容量等于0,则将已有的空数组赋值给elementData。
  • 如果指定初始容量小于0,则将抛出IllegalArgumentException异常。

Tip:
指定初始容量等于0时,会将空数组EMPTY_ELEMENTDATA赋值给elementData,EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA 都是空数组,但用途不同

  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA 用于空参构造赋值的默认空数组。
  • EMPTY_ELEMENTDATA用于初始化容量为0或使用Collection集合构造函数时Collection中没有元素。
3.3 ArrayList( Collection<? extends E> c)

构造一个包含指定Collection集合元素的列表,这些元素可以是E或E的子类(E为ArrayList的泛型类型)并且是按照该collection的迭代器返回它们的顺序排列的。其容量和指定集合容量相同

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;
    }
}

该构造函数执行过程:

  • 将Collection集合转为数组赋值给elementData。
  • 若elementData长度不为0,设置size,并且若elementData不是Object类型,则转换为Object[]类型的数组。
  • 若elementData长度为0,则将空数组EMPTY_ELEMENTDATA赋值给elementData。
4.ArrayList扩容机制(重点)

添加元素时使用 ensureCapacityInternal() 方法来保证容量足够。

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

数组扩容的执行流程可分为以下步骤:

  1. 计算最小容量;
  2. 判断是否需要扩容;
  3. 计算新的容量;
  4. 考虑数组长度溢出;
  5. 数组扩容;
4.1 计算最小容量

calculateCapacity()方法用于计算最小容量。传入参数(minCapacity)是添加元素后的元素总数。(例如调用add,则minCapacity = size + 1 ; 调用 addAll ,则minCapacity = size + 被添加集合的size)

 private static int calculateCapacity(Object[] elementData, int minCapacity) {
  
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
  • 若使用无参构造创建的ArrayList,在第一次扩容时,最小容量(minCapacity)的值为 默认容量10(DEFAULT_CAPACITY)和 元素总数的最大值。
  • 否则最小容量(minCapacity)的值就是元素总数。

Tip:
elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 如果为true,那么说明:该集合是使用的默认空构造器初始化的,并且是第一次添加数据。

4.2 判断是否需要扩容

ensureExplicitCapacity()方法用于判断是否要进行扩容,若需要就进行扩容。

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
  • modCount++ ,记录一次ArrayList结构变化。(用于快速失败[fail-fast])
  • 若计算出的最小容量大于当前elementData数组的容量,则调用grow()进行扩容。
4.3 计算新的容量

确定扩容后,首先计算数组elementData的新容量。

private void grow(int minCapacity) {

    // 计算新的容量
    int oldCapacity = elementData.length;
    
    int newCapacity = oldCapacity + (oldCapacity >> 1); 
    
    if (newCapacity - minCapacity < 0) 
        newCapacity = minCapacity;
    
    // 考虑数组长度溢出
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    // 扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}
  • newCapacity = oldCapacity + (oldCapacity >> 1);即新容量约为旧容量的1.5倍。
  • 若 1.5倍扩容后依然比计算的最小容量(minCapacity)还小,那么数组的新容量(newCapacity )就设置成最小容量(minCapacity)。
4.4 考虑数组长度溢出

扩容后需要考虑数组长度溢出问题。

// 考虑数组长度溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

不管是计算minCapacity还是计算出的newCapacity 都存在数值溢出的问题。

例如:当ArrayList的容量和size都为Integer.MAX_VALUE时,再次添加一个元素,那么总元素数为Integer.MAX_VALUE + 1,minCapacity为int类型的值,所以必然溢出。newCapacity按原容量扩容1.5倍,同样存在溢出情况。

// MAX_ARRAY_SIZE : 建议最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}
  • 当minCapacity溢出时抛出OutOfMemoryError异常;
  • minCapacity > MAX_ARRAY_SIZE 时 设置 newCapacity 等于Integer.MAX_VALUE。
  • minCapacity <= MAX_ARRAY_SIZE 时 设置 newCapacity 等于 MAX_ARRAY_SIZE。
4.5 扩容

最后扩容,重新构建一个数组,然后将原来数组的元素拷贝到新数组中,新数组的长度为newCapacity。并修改原数组的引用指向这个新建数组,原数组自动抛弃(Java垃圾回收机制会自动回收)。

// 扩容
elementData = Arrays.copyOf(elementData, newCapacity);

Tip:
ArrayList所谓的可变长度,实际上在底层也只是新建一个更长的数组,然后拷贝原数组的元素到新数组,并将引用指向新数组而已。

4.6 扩容机制总结

总来说:添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。

扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此应该尽量减少扩容次数。

  • 创建ArrayList对象时,可以指定大概容量。
  • add之前调用ensureCapacity()方法指定大概容量。
5. 常用方法
签名描述复杂度
E get(int index)获取指定位置的元素O(1)
boolean add(E e)在集合末尾新增一个元素O(1)
void add(int index, E element)在指定位置添加元素O(n)
E remove(int index)删除指定位置的元素O(n)
E remove(Object o)移除集合中第一次出现的指定元素O(n)
int indexOf(Object o)查询指定元素首次的位置O(n)
E set(int index, E element)设置指定位置的元素值O(1)
boolean contains(Object o)是否包含某个元素O(n)
int size()返回列表中的元素个数O(1)
boolean isEmpty()列表是否为空O(1)
void clear()从列表中移除所有元素O(n)
6.ArrayList迭代器

ArrayList通过内部类实现Iterator接口来实例化迭代器类,通过Iterator我们可以实现对elementData中的元素迭代遍历。

iterator()方法返回一个Itr类的实例对象。

public Iterator<E> iterator() {
    return new Itr();
}

Itr是ArrayList的一个内部类,实现了Iterator< E >接口。

private class Itr implements Iterator<E>

listIterator()方法返回一个ListItr类的实例对象。

public ListIterator<E> listIterator() {
    return new ListItr(0);
}

ListItr类也是ArrayList的一个内部类,继承自Itr ,实现了ListIterator< E >接口。

private class ListItr extends Itr implements ListIterator<E>
  • Iterator迭代器只能正向迭代。
  • ListIterator迭代器可以正向迭代,也可以反向迭代。
  • forEach(增强for)最终被编译器转换成对iterator.next()和iterator.hashNext()方法的调用,即forEach遍历方式等同于Iterator迭代器遍历。
public class ArrayListTest {

    public static void main(String[] args) {

       ArrayList list = new ArrayList(Arrays.asList(1, 2, 3, 4));
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
             //list.remove(1);//Error : ConcurrentModificationException
        }

        System.out.println("\n------------");

        ListIterator listIterator = list.listIterator();
        while (listIterator.hasNext()) {
            System.out.print(listIterator.next() + " ");
        }

        System.out.println("\n------------");

        while (listIterator.hasPrevious()) {
            System.out.print(listIterator.previous() + " ");
        }
    }
}
输出:
1 2 3 4 
------------
1 2 3 4 
------------
4 3 2 1 

注意:使用迭代器进行迭代的过程中,只能使用迭代器提供的add、remove进行结构修改,使用集合本身的add、remove等语句会引发ConcurrentModificationEeception 并发修改异常,这是因为迭代器是快速失败的(fail-fast)。

7.快速失败(fail-fast)与安全失败(fail-safe)
7.1 快速失败(fail-fast)

快速失败(fail-fast)是Java容器(Collection和Map都存在fail-fast)的一种错误检测机制。当方法检测到对象的并发修改,但不允许这种修改时就抛出ConcurrentModificationException 异常。

快速失败迭代器会尽最大努力抛出 ConcurrentModificationException,但不可能对是否出现不同步并发修改做出任何硬性保证。因此存在非同步的并发修改时,只是可能会抛出该异常。

快速失败(fail-fast)产生的场景

  • 单线程环境下,遍历集合时,集合结构被修改(使用迭代器修改则不会)。
  • 多线程环境下,一个线程遍历集合时,另一个线程修改了集合结构。

快速失败(fail-fast)原理:modCount用于记录ArrayList结构修改的次数,modCount继承自AbstractList。在序列化之前或初始化迭代器时,会在内部设置expectedModCount变量等于外部的modCount变量,序列化后或每次进行迭代时,就会检测expectedModCount和modCount是否一致,当这两个变量不一致时,就抛出并发修改异常(ConcurrentModificationEeception )。

如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

Tip:
结构修改是指:添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

快速失败(fail-fast)解决方案

  • 单线程遍历集合时,使用迭代器提供的结构修改方法,如:add、remove。
  • 多线程下,使用java.util.concurrent下的集合代替java.util下的集合类,如使用CopyOnWriteArrayList替代ArrayList。
7.2 安全失败(fail-safe)

安全失败(fail-safe):采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历(读写分离)。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

安全失败(fail-safe)缺点

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

Tip:
java.util.concurrent下的容器都是安全失败的(fail-safe)。

8.ArrayList序列化

ArrayList中的elementData被transient修饰,表明该成员变量不可被序列化。这是因为elementData中不一定全部被填充了元素,因此没有必要将elementData全部序列化。

transient Object[] elementData; // non-private to simplify nested class access

ArrayList通过实现 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。

public class ArrayListTest {
    
    public static void main(String[] args) throws Exception{

   	    ArrayList list = new ArrayList(Arrays.asList(1, 2, 3, 4));
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
        oos.writeObject(list);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
        Object o = ois.readObject();
        System.out.println(o);
    }
}

  • 在序列化过程中,ObjectOutputStream.writeObject() 和 ObjectInputStream .readObject() 方法会通过反射机制,尝试调用被序列化对象类里的 writeObject 和 readObject 方法,进行序列化和反序列化。
  • 如果被序列化对象类中没有这样的方法,则序列化时默认调用是 ObjectOutputStream.defaultWriteObject() 方法,反序列化时默认调用ObjectInputStream.defaultReadObject() 方法。

ArrayList通过提供writeObject()和readObject()方法,只序列化了数组中有元素填充的那部分数据。

9. clone机制

ArrayList实现了Cloneable接口,重写了clone()方法。

public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

ArrayList中的clone()方法返回的是一个全新的ArrayList实例对象,但是其elementData,也就是存储数据的数组,存储的对象还是指向了旧的ArrayList存储的那些对象。也就是ArrayList这个类实现了深拷贝,但是对于存储的对象还是浅拷贝。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值