JDK8 ArrayList源码解析P2

110 阅读3分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

  上篇文章介绍了ArrayList底层的数据结构以及常见的增删改查方法,详见# JDK8 ArrayList源码解析P1。今天来看一下最常用但却没怎么听说过的部分,迭代器Iterator

迭代器 Iterator

  在使用ArrayList时我们经常需要遍历集合来完成某些操作,通常有两种方式,一种是直接使用foreach,一种是使用Iterator接口,但两种本质上是一样的,foreach底层实现还是Iterator接口。

Itr

  ItrArryList中的一个私有内部类,实现了Iterator接口用以实现遍历。

private class Itr implements Iterator<E> {
        ...
      }

成员变量

      
      // 迭代过程中,下一个元素的位置,默认从 0 开始
      int cursor;       
      // 新增场景:表示上一次迭代过程中,索引的位置;删除时置为 -1
      int lastRet = -1; 
      
      
      // protected transient int modCount = 0;
      // modCount是ArrayList的一个属性字段,表示数组实际的版本号
      // expectedModCount 表示迭代过程中,期望的版本号;
      int expectedModCount = modCount;
      

主要方法

迭代器主要就三个方法:

  • hasNext:是否还有下一个元素;
  • next:下一个元素值;
  • remove:删除当前迭代的值;

hasNext()

       public boolean hasNext() {
        // cursor 表示下一个元素的位置,size 表示实际大小
        // 如果两者相等,说明已经没有元素可以迭代了,
        // 如果不等,说明还可以进行迭代
        return cursor != size;
      }

next()

      @SuppressWarnings("unchecked")
      public E next() {
      
        // 迭代过程中,判断版本号有无被修改
        // 如果被修改,抛 ConcurrentModificationException 异常
        checkForComodification();
        
        //本次迭代过程中,元素的索引位置
        int i = cursor;
        
        //参数校验
        if (i >= size)
          throw new NoSuchElementException();
          
          
        //获取存储元素的数组
        Object[] elementData = ArrayList.this.elementData;
        
        //参数校验
        if (i >= elementData.length)
          throw new ConcurrentModificationException();
          
          
        // 下一次迭代时,元素的位置,为下一次迭代做准备
        cursor = i + 1;
        
        // 返回元素值 同时将lastRet设置为 i
        return (E) elementData[lastRet = i];
      }

next方法做了两件事:

  • 是否还能继续迭代;
  • 定位到当前迭代的值,并为下一次迭代做好准备;

checkForComodification()


      // 检测 ArrayList 中的 modCount 和当前迭代器对象的 expectedModCount 是否一致
      // 不等的话直接抛出异常
      final void checkForComodification() {
        if (modCount != expectedModCount)
          throw new ConcurrentModificationException();
      }

这个方法没啥好说的,就是检查ArrayList中的modCount和当前迭代器对象的 expectedModCount是否一致,不一样就抛出ConcurrentModificationException

remove()

      public void remove() {
      
        // 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了
        if (lastRet < 0)
          throw new IllegalStateException();
          
          
        // 迭代过程中,判断版本号有无被修改,
        // 如果被修改,抛 ConcurrentModificationException 异常
        checkForComodification();

        try {
        
          // 此处调用的是ArrayList的 remove(int index)
          ArrayList.this.remove(lastRet);
          
          // 设置下一个位置
          cursor = lastRet;
          
          // -1 表示元素已经被删除,这里也防止重复删除
          lastRet = -1;
          
          // 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
          // 这样下次迭代时,两者的值是一致的了
          expectedModCount = modCount;
          
        } catch (IndexOutOfBoundsException ex) {
          throw new ConcurrentModificationException();
        }
      }

需要注意的点是:

  • lastRet= -1 的操作目的,是防止重复删除操作;
  • 删除元素成功,数组当前modCount 就会发生变化,这里会把 expectedModCount 更新为modCount的值,下次迭代时两者的值就会一致了;

ListItr

  ListItrItr的子类,除了基本的三个方法之外,还额外实现了一些其他的方法。我们都知道,在遍历集合时,若是对集合结构进行修改则会触发fast-fail机制,但若是我们的确有这个需求,一边遍历一边修改集合的结构,那怎么办呢?答案就在ListItr中的add方法中。

add(E e)

      public void add(E e) {
        //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
        checkForComodification();

        try {
          //获取当前遍历到的位置
          int i = cursor;
          //插入到数组中
          ArrayList.this.add(i, e);
          //再向下移动一个位置 因而本次遍历过程中访问不到这个元素 只能在下一次遍历中才能访问
          cursor = i + 1;
          //更新lastRet
          lastRet = -1;
          // 添加元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
          // 这样下次迭代时,两者的值是一致的了
          expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
          throw new ConcurrentModificationException();
        }
      }
      }

  使用add这个方法在遍历时添加时,本次遍历是访问不到添加的那个元素的,只能在下一次遍历时才能访问,保证了本次遍历的正确性,从而防止出现fast-fail

其他方法

  ListItr还实现了ListIterator这个接口,因而还可以前向遍历,这里只给出源码,简单看一下就行。

       //判断是否还有前一个元素
        public boolean hasPrevious() {
            return cursor != 0;
        }
        //下个元素的索引
        public int nextIndex() {
            return cursor;
        }

       //前一个元素的索引
        public int previousIndex() {
            return cursor - 1;
        }

      //迭代时,前一个元素的值
        @SuppressWarnings("unchecked")
        public E previous() {
            //校验modCount
            checkForComodification();
            //前一个位置索引
            int i = cursor - 1;
            if (i < 0)//参数校验
                throw new NoSuchElementException();
            //实际存储数据的数组
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)//参数校验
                throw new ConcurrentModificationException();
            //更新cursor
            cursor = i;
            //返回当前迭代的值,并设置 lastRet
            return (E) elementData[lastRet = i];
        }

总结

  结合JDK8 ArrayList源码解析P1和本篇文章一起总结一下ArrayList的特点,如下:

  • 底层是动态数组,默认大小为10,扩容时每次都是当前数组长度的1.5倍;
  • 使用无参构造或者capacity=0的有参构造时,ELEMENTDATA都是空数组,只有在第一次添加元素时才去扩容;
  • 扩容主要方法为 grow(int minCapacity)不支持缩容
  • 允许元素为null
  • 实现 RandomAccess接口,支持随机访问,平均时间复杂度为O(1)
  • 底层是数组,所以增加和删除元素过程时,效率低下,平均时间复杂度为O(n)