ArrayList学习(基于JDK 1.8)

1. ArrayList概述

  • 回忆一下,遇到元素个数不确定时,就会使用ArrayList,而非数组

1.1 ArrayList的特性

  • 同样的,还是通过阅读类注释了解ArrayList的特性

ArrayList的类注释,提供了以下信息

  • ArrayList实现了List接口,允许元素为null

  • ArrayList是动态的list实现,提供了用于操作数组大小的方法

    • 容量,指ArrayList中存储元素的数组大小;size,指ArrayList中的元素个数
    • 向ArrayList中添加元素,其容量会自动增长(扩容)。为避免添加大量元素触发ArrayList多次扩容的情况,可以使用ensureCapacity()方法将其容量指定为一个合适的较大值
    • 注意: trimToSize()方法,可以将ArrayList的容量设置为当前size,使其不存在存储空间冗余
  • ArrayList支持随机访问,get和set操作时间复杂为 O ( 1 ) O(1) O(1);add操作大部分情况下时间复杂度为 O ( 1 ) O(1) O(1),除了触发扩容或从中间插入的情况

  • ArrayList使用fail-fast迭代器:

    • 一旦通过iterator()listIterator()创建迭代器,除非使用迭代器自身的remove或add方法,任何修改list结构的方法都将使迭代器抛出ConcurrentModificationException异常
    • 注意: 所谓的add方法,是listIterator()创建ListIterator迭代器所有特有的方法;而remove方法,是iterator() 创建的Iterator迭代器中的方法
      ArrayList<Integer> list = new ArrayList<>();
      list.add(1);
      list.add(2);
      Iterator<Integer> iterator = list.iterator();
      while (iterator.hasNext()) {
          if (iterator.next().equals(2)) {
              iterator.remove();
          }
      }
      
      ListIterator<Integer> listIterator = list.listIterator();
      while (iterator.hasNext()) {
          if (listIterator.next().equals(1)) {
              listIterator.add(21);
          }
      }
      
  • ArrayList与Vector是等价的,除了ArrayList是非线程安全的之外。

    • 多线程访问ArrayList,最好使用Collections.synchronizedList()方法将其转为线程安全的list
    • 或者使用线程安全的Vector类(使用synchronized实现线程安全),
    • 或者使用concurrent包下、读写分离的CopyOnWriteArrayList

总结

  1. ArrayList实现了List接口,允许null
  2. ArrayList基于动态数组,支持随机访问;插入元素会自动扩容,也可以使用ensureCapacity()方法方法手动扩容
  3. ArrayList是非线程安全的,Vector是线程安全的
  4. ArrayList使用fail-fast迭代器(两种迭代器、remove和add方法)

1.2 ArrayList类图

  • ArrayList类的声明如下

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
  • 类图如下

    • 直观地看,ArrayList继承了抽象类AbstractList,实现了List、RandomAccess、Cloneable和Serializable接口
      在这里插入图片描述
  • AbstractList抽象类:

    • 对List接口的骨架级实现,可以最小化实现一个支持随机访问的数据结构(如,数组)所需的工作量;
    • 与之对应的,AbstractSequentialLis抽象类,最小化实现一个支持顺序访问的数据结构(如,链表)所需的工作量。
  • List接口:不允许重复元素的集合,允许null元素

  • RandomAccess接口:

    • 实现List时的标记接口,以表明该list支持快速的随机访问。
    • 当应用于随机或顺序访问的list时,该接口的主要作用是允许一般的算法修改其行为以提供良好的性能
    • 说实话,自己不太理解,可能需要深入了解标记接口的作用 😂
  • Cloneable接口:表明ArrayList支持拷贝,其具体实现为浅拷贝

  • Serializable接口:表明ArrayList支持拷贝支持序列化

1.3 成员变量

  • ArrayList的类常量如下

    // 默认初始容量为10
    private static final int DEFAULT_CAPACITY = 10;
    
    // 一个共享的空数组实例
    // 若用户通过构造函数,指定一个大小为0的ArrayList,将使用EMPTY_ELEMENTDATA初始化elementData
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    // 一个共享的空数组实例,用于具有默认容量的ArrayList
    // 使用DEFAULTCAPACITY前缀与EMPTY_ELEMENTDATA相区别,以便知道在第一次添加元素时如何扩容
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // 最大的容量,如果超过该容量容易导致OOM
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
  • 实例变量

    // 存储元素的数组缓冲区,ArrayList的容量就是数组缓冲区的长度
    // 未指定初始化容量的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,在第一次添加元素时,将被扩容为10
    transient Object[] elementData; // non-private to simplify nested class access
    
    // ArrayList中的元素个数,size <= capacity
    private int size;
    
  • 存储元素的数组缓冲区,使用transient关键字修饰,即该成员变量无法序列化和反序列化

1.4 如何实现ArrayList中元素的序列化?

  • 存储元素的数组elementData被transient修饰,那岂不是就不能序列化和反序列化了?
  • 别着急,ArrayList提供了writeObject()readObject()方法,用于实现序列化和反序列化

writeObject()方法

  • 代码如下:
    • 除了序列化非static、非transient元素,还特意将size和数组中存储的元素序列化了
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        // 将当前类的非static、非transient字段写入输出流
        s.defaultWriteObject();
    
        // 将size作为与clone操作行为上兼容的capacity,实际上已经将size序列化过了
        s.writeInt(size);
    
        // 将元素顺序写入输出流
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
        // 序列化后,发现结构被修改,则抛出ConcurrentModificationException
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    

writeObject()方法

  • 代码如下:

    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
    
        // 这里的size是通过 s.defaultReadObject() 得到的
        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);
    
            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }
    

疑问一:两个方法都被定义为private,岂不是无法使用?

  • 仔细观察发现,序列化和反序列化的两个方法,都被定义为private

  • 那不就无法通过list.writeObject()进行调用了?

  • 通过debug发现调用ObjectOutputStream.writeObject(Object obj)时,实际会调用obj的writeObject() 方法
    在这里插入图片描述

  • readObject()方法也是如此,调用ObjectInputStream.readObject()方法,实际obj的readObject() 方法

  • 验证代码示例

    public static void main(String[] args) throws ParseException {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(3);
        list.add(5);
    
        // 序列化list
        try (FileOutputStream fileOutputStream = new FileOutputStream("list.txt" );
             ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream)
        ) {
            outputStream.writeObject(list);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException exception) {
            exception.printStackTrace();
        }
    
        ArrayList<Integer> list1 = new ArrayList<>();
        // 反序列化
        try (FileInputStream fileInputStream = new FileInputStream("list.txt");
             ObjectInputStream inputStream = new ObjectInputStream(fileInputStream)
        ) {
            list1 = (ArrayList<Integer>) inputStream.readObject();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException exception) {
            exception.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    
        list1.forEach(System.out::println);
    }
    

参考文档


1.5 构造函数

  • ArrayList具有三个构造函数

    // 指定初始化容量的构造函数,如果初始化容量为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;
    }
    
    // 从给定的集合类构造ArrayList
    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;
        }
    }
    
  • 后续的学习中,需要关注第一次添加元素时,DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA如何影响扩容的

2. 查找方法

2.1 根据索引查找元素

  • 作为一个支持随机访问的动态数组,虽然不能像数组一样,通过arr[i]直接访问某个元素

  • 但List接口定义的public E get(int index)方法,能达到同样的效果

  • ArrayList的get方法代码如下,就两行代码 😂

    • 先检查index是否越界,若越界,则直接抛出IndexOutOfBoundsException
    • 通过elementData(index) 返回elementData[index],该方法实现了类型转换
    public E get(int index) {
        rangeCheck(index); // 检查索引是否越界,若越界,抛出IndexOutOfBoundsException
    
        return elementData(index); // 等价于 elementData[index]
    }
    
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    E elementData(int index) {
        return (E) elementData[index];
    }
    

疑问:为何单独进行下标越界检查?越界检查的范围,好像不太对?

  • 数组下标为 0 ~ len - 1,通过arr[index]访问时,Java在运行时会自动抛出ArrayIndexOutOfBoundsException异常
  • 这样来看,根本不用单独进行下标越界检查的
  • 单独进行下标越界检查的原因:
    • 如果依靠数组自身的越界检查,则可能访问到还未存入元素的位置
    • size才是实际存储的元素个数,超过size的位置,元素值默认为null
  • 那这样分析的话,rangeCheck 方法应该检查 index < 0 || index >= size的情况,为何忽略了index < 0的检查?
  • 原因:作者偷懒了,想依靠数组自身的越界检查实现index < 0的check 😎

2.2 查找元素的索引

  • 能根据索引查找元素,就有需求:根据元素查找索引

  • ArrayList允许存在重复的元素,返回的索引应该是第一个匹配元素的位置;或者,需要直接返回最后一个匹配元素的位置

  • 对应的方法为indexOf()lastIndexOf()

  • indexOf()方法的代码如下:

    • ArrayList允许null值,首先处理查找null值索引的情况
    • 一旦查到匹配的元素,则立即返回对应的索引;未找到,则返回-1
    public int indexOf(Object o) {
        if (o == null) { // 首先处理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; // 未找到,返回-1
    }
    
  • lastIndexOf()方法的代码如下:

    • 要是按照我的菜鸟思路:从前往后查找匹配的元素,使用一个变量记录查找到的index,最后记录的就是lastIndex或-1 😂
    • too young, too simple啊,看看人家写的:直接从后往前找,第一个匹配元素的索引就是lastIndex
    • 同样的,还是按照null值和一般值两种情况进行查找;未找到,则返回-1
    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
    

2.3 判断是否包含元素

  • 之前,一直都未注意到list有关的类中提供了contains()方法,使用contains方法更多的在set和map类中

  • 其实,contains()方法是List接口、List接口的父接口Collection,均有的一个方法,用于判断集合中是否包含指定的元素

  • contains()方法的代码如下:只要元素的索引不为-1,即大于等于0,则说明ArrayList中包含该元素

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
    

3. 添加方法 —— 核心:扩容

3.1 add方法

  • 使用ArrayList时,经常出现类似的代码list.add(1);,这是最简单的添加方法,会直接在末尾添加一个元素

  • add方法的代码如下,看起来简单,实则暗含玄机😜

    • 先通过ensureCapacityInternal()方法,根据实际情况决定是否对内部的数组缓冲区进行扩容
    • 将元素添加到数组末尾,size++;返回true,表示成功添加元素
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!! 由ensureExplicitCapacity方法完成
        elementData[size++] = e;
        return true;
    }
    

重点来了,ensureCapacityInternal()方法是如何实现扩容的?

  • ensureCapacityInternal方法的代码如下:从方法名上理解,先计算所需容量,然后按需扩容以保证具有指定的容量
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
  • calculateCapacity 方法的代码如下:
    • 如果elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,即未指定容量的空数组,计算出的容量为max(DEFAULT_CAPACITY, minCapavity)
    • 否则,计算出的容量就是指定容量minCapacity
    • 总结: 这里有体现DEFAULTCAPACITY_EMPTY_ELEMENTDATA在第一次添加元素时的作用:决定初始化容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
  • 计算出所需的容量后,通过 ensureExplicitCapacity 方法保证其具有充足的容量
    • 无论最后是否需要扩容,添加元素都会改变ArrayList的结构,因此需要modCount++
    • 若所需容量超过当前容量,则调用grow()方法实现扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    

3.2 grow()方法实现扩容

  • 学习了HashMap,HashMap中初始容量要求为2的幂,扩容也是扩容两倍

    • 好处一: 通过(n - 1) & hash计算出来的index分布可以更加均匀,减少哈希冲突
    • 好处二: 按照2倍扩容,扩容后元素的位置更容易计算(e.hash & oldCap):原位index,或index + oldCap
  • 回到ArrayList的扩容,计算出来的minCapacity很可能就是size + 1

  • 一个坑一个坑的扩容,这也太费力了,肯定不能这样 🤔

  • grow 方法的代码如下

    • 首先,根据当前容量oldCapacity初始化newCapacity,newCapacity = oldCapacity * 1.5,扩容1.5倍
    • 使用指定容量修正newCapacity,兼容第一次初始化的情况
      • 若指定容量为size + 1,则不可能出现newCapacity - minCapacity < 0的情况
      • 通过公有的ensureCapacity()方法扩容,或者oldCapacity为0时,才需要使用指定容量修正
    • 通过MAX_ARRAY_SIZE修正newCapacity,避免过大而引发OOM
      • newCapacity - MAX_ARRAY_SIZE > 0的原因:(1)扩容1.5倍后,超过MAX_ARRAY_SIZE;(2)指定容量太大,使得newCapacity超过MAX_ARRAY_SIZE
    • 最后,创建一个容量为newCapacity的新数组;原数组中的元素通过System.arraycopy()实现拷贝
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); // 默认为oldCapacity的1.5倍
        if (newCapacity - minCapacity < 0) // 使用指定容量更新newCapacity,可以兼容第一次初始化的情况
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0) // 修正newCapacity,避免过大而引发oom
            newCapacity = hugeCapacity(minCapacity);
        // 创建一个容量为newCapacity的新数组,通过System.arraycopy实现元素的拷贝
        elementData = Arrays.copyOf(elementData, newCapacity);  
    }
    
  • hugeCapacity() 方法的代码如下

    • 若指定容量过大,导致newCapacity超过MAX_ARRAY_SIZE,则将其设置为Integer.MAX_VALUE;否则,直接截断设置为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;
    }
    

总结:

  • ArrayList的扩容,默认为1.5倍;若指定容量大于1.5倍,则使用指定容量修正
  • 为了避免newCapacity过大,还需要使用MAX_ARRAY_SIZE进行修正
  • 一般情况下,都是按照1.5被扩容的(也有可能使用指定容量进行扩容)

3.3 native方法System.arraycopy()

  • 扩容的最后一步操作:调用Arrays.copyOf()方法,基于现有数组创建一个指定容量的新数组

  • 如果新数组的长度小于原数组,则会发生数据截断

    public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }
    
    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength)); // 拷贝的数据长度为新旧长度的min,数据截断的关键
        return copy;
    }
    
  • Arrays.copyOf()方法通过调用native方法System.arraycopy()实现数组元素的拷贝

    • length指需要拷贝的数据长度
    • Arrays.copyOf()方法巧妙使用Math.min(original.length, newLength)实现数据的截断
    public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length);
    

3.4 指定index的add方法

  • List接口还贴心地提供了指定index的add方法,再也不用担心只能尾部插入的问题了
    • 注意: 与add(E element)方法不同的是,该方法无返回值;可以通过是否返回IndexOutOfBoundsException异常,判断是否成功添加元素
    public void add(int index, E element) {
        rangeCheckForAdd(index); // 检查指定的index是否越界;若越界,则抛出`IndexOutOfBoundsException`异常 
    
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index); // 在index插入元素,则index开始的元素都需要后移,空出对应的位置
        elementData[index] = element;
        size++;
    }
    
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    

疑问:为何单独定义rangeCheckForAdd() 方法?

  • 向ArrayList中添加元素,由于其存在扩容机制,直接向超过elementData.length的位置添加按理是允许的
  • 但是,实际存储的元素为size个,如果指定的index超过size,那就使得数组中的元素不连续了
  • 因此,需要限制index <= size;若 index == size,说明刚好在末尾添加元素
  • 同时,作者这里没有偷懒了,主动限制index >= 0 :stuck_out_tongue_winking_eye
  • 因此,angeCheckForAdd() 方法的check条件为index > size || index < 0,就抛出IndexOutOfBoundsException 异常
  • 同时,这与获取元素时(get/set 方法)的check方法就存在差异了,从 index >= size 变成了 index > size

3.5 多个元素的add

  • 有单个元素的add,就有多个元素的add

  • 同样,List接口也提供了两种addAll()方法:默认在尾部添加、在指定位置添加

  • 这两个方法均具有boolean类型的返回值,true表示成功添加

  • 默认在尾部添加多个元素,不再赘述该方法

    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
    
  • 在指定位置添加多个元素,不再赘述该方法

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
    
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
    
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved); // 空出numNew个位子
    
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }
    

4. 修改元素

  • 通过get()方法,可以获取指定index的元素;同样的,可以通过set()方法修改元素
  • set 方法的代码如下,十分简单:检查index是否越界、记录oldValue、更新value、返回oldValue
    public E set(int index, E element) {
        rangeCheck(index); // check下标是否越界
    
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
    

5. 删除元素

  • 删除元素,同样支持 “逆向工程”,支持通过index删除元素,或删除第一个匹配的元素

5.1 通过索引删除元素

  • 通过index删除元素的remove方法,代码如下
    • 应该从已有的元素中删除,因此index应该在 0 ~ index - 1的范围。所以,进行下标越界检查时,调用的是rangeCheck()方法,而非rangeCheckForAdd()方法
    • 通过右侧元素整体前移,并将最后一个元素置为null,来实现元素的删除
    • 该方法将返回被删除元素的值
    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; // 断开与元素的连接,以便该元素可以被gc回收
    
        return oldValue;
    }
    
  • 删除过程的示意图如下:
    在这里插入图片描述

5.2 删除指定的元素

  • 删除指定元素的remove方法代码如下:

    • 与indexOf()方法一样,考虑到ArrayList中可能存在重复元素,采用 “最左匹配原则” ,即删除从头到尾,第一个匹配的元素
    • 删除时,根据匹配元素的index进行删除;相比remove(int index)方法,多了确定元素索引的步骤
    • 该方法不再返回被删除元素或者其index,而是返回 truefalse,表示是否成功删除元素
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    
  • 其中,fastRemove(int index)方法就是remove(int index)方法的核心代码:右侧元素前移、删除最后一个元素

批量删除的方法:

  • removeAll(Collection<?> c),从ArrayList中删除包含在指定集合中的元素
  • removeIf(Predicate<? super E> filter),JDK 1.8的新增方法,删除ArrayList中满足条件的所有元素
  • clear(),非常暴力的方法,将数组清空,回归到无任何元素的状态

6. for-each与迭代器的关系

  • 学习容器类时,或多或少都会提到以下知识:
    • 通过for-each遍历,通过迭代器iterator遍历
    • 拥有fail-fast迭代器:一旦创建好迭代,除非使用迭代器的remove方法,其他任何修改结构的方法都将触发迭代器抛出ConcurrentModificationEXception 异常
  • 当时,学习时并未注意for-each和迭代器的关系:都是遍历,二者是不同的遍历机制;还是本质是同一种遍历机制
  • 集合类的学习中,发现:从JDK 1.5开始,实现了 Iterable 接口的对象,都可以使用for-each进行遍历
  • 那就是说,for-each遍历底层是通过迭代器实现的

6.1 for-each的底层实现验证

  • 基于ArrayList实现for-each遍历

    public static void main(String[] args) throws ParseException {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        for (Integer e : list) {
            System.out.print(e + " ");
        }
    }
    
  • 在idea中,双击对应的字节码文件,得到反编译后的代码:

    public static void main(String[] args) throws ParseException {
        ArrayList<Integer> list = new ArrayList();
        list.add(1);
        list.add(2);
        Iterator var2 = list.iterator();
    
        while(var2.hasNext()) {
            Integer e = (Integer)var2.next();
            System.out.print(e + " ");
        }
    
    }
    
  • 结论: for-each遍历底层是通过迭代器实现的

  • 若在遍历容器类时试图删除元素的原因:modCount会发生改变,可能会触发迭代器抛出ConcurrentModificationEXception 异常

6.3 ArrayList的两种迭代器

  • ArrayList支持 ListIterator 和 Iterator两种迭代器,分别叫做ListItrItr

  • 创建迭代器的方法为:

    public ListIterator<E> listIterator() {
        return new ListItr(0);
    }
    public ListIterator<E> listIterator(int index) {
        if (index < 0 || index > size)
            throw new IndexOutOfBoundsException("Index: "+index);
        return new ListItr(index);
    }
    public Iterator<E> iterator() {
        return new Itr();
    }
    

以Itr为例,在哪里触发fail-fst机制?

  • hasNext()通过判断当前索引cursor与size是否相等,从而确定是否存在next元素

    public boolean hasNext() {
        return cursor != size;
    }
    
  • next()方法:

    • 在访问元素前会进行modCount的检测,若modCount与expectedModCount不一致,说明list结构发生改变,将触发ConcurrentModificationEXception
    • 最后,不仅返回对应位置的元素,还更新了lastRet非常重要的一步操作
    public E next() {
        checkForComodification(); // modCount与expectedModCount是否一致
        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]; // 更新lastRet
    }
    

为何使用迭代器的remove方法不会触发fail-fast机制?

  • 迭代器的remove方法,代码如下

    • 在删除元素后,会更新expectedModCount
    • 使得expectedModCount和modCount保持一致,下一次访问时,不会触发ConcurrentModificationEXception异常
    public void remove() {
        if (lastRet < 0) // 初始时为-1,要想不为-1,必须先通过next()获取元素
            throw new IllegalStateException();
        checkForComodification();
    
        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1; // 重置为-1
            expectedModCount = modCount; // 更新expectedModCount
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    
  • lastRet的巧妙使用:

    • lastRet初始化为-1,只有通过next()方法获取元素后,才会更新为具体的index
    • 删除元素后,会将cursor置为lastRet,也就是新的下一个元素的索引;同时,还会将lastRet重置为-1
    • 这种巧妙的设计,使得想要删除某个元素,必须先通过next()方法获取该元素;否则,会抛出 IllegalStateException 异常

6.4 示例代码,为何不会抛出异常

  • 看下面的代码:

    • 按照之前的学习,删除元素2后,再次进入循环尝试获取元素3时,应该会触发异常
    public static void main(String[] args) throws ParseException {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer value = iterator.next(); // 根据分析,在访问元素3时,会触发异常
            System.out.println(value);
            if (value.equals(2)) {
                list.remove(value);
            }
        }
    }
    
  • 执行结果如下:既没有触发异常,又没有访问到元素3
    在这里插入图片描述

  • 原因: 巧合而已

    • 通过next()方法获取到元素2后, cursor变成2
    • 通过list.remove()移除元素2后,size变成2
    • 在while循环的条件判断时,hasNext()将返回false,不会进入循环
    • 因此,既不会触发异常,又没有访问到元素3
    • 一旦被删除的不是倒数第二个元素,就会触发异常
  • 不要抱有侥幸心理,随意地在迭代时添加或删除元素,除非使用迭代器的对应方法

7. 总结

ArrayList学习结束,总结一下:

  • ArrayList的特性:动态数组,支持随机访问;允许null值;使用fail-fast迭代器;非线程安全,对应的线程安全类Vector
  • ArrayList的重要属性:Object[] elementData数组,元素个数size(size != 容量,即数组长度)
  • ArrayList的查找、添加、删除、修改方法,添加操作会触发扩容
  • 扩容方法grow():默认扩容1.5倍,会根据指定容量minCapacity和最大容量MAX_ARRAY_SIZE进行修正,最终使用native方法System.arrayCopy()创建扩容后的新数组
  • 在添加、删除元素时,大量使用native方法System.arrayCopy()
  • 何时使用rangeCheck(),何时使用rangeCheckForAdd() ?二者的区别
  • for-each底层使用迭代器实现遍历,for-each遍历时修改结构,容易触发fail-fast机制(示例代码的教训

参考链接

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值