【Android面试基础】ArrayList的随机访问和顺序访问的区别?

面试官问:ArrayList的随机访问和顺序访问的区别?

:嗯...,什么是随机访问和顺序访问?

OOS:(只要自己不尴尬,尴尬的就是别人)😁

我们知道Java中的ArrayList是一个可调整大小的数组实现,是一种可以动态改变数组大小的数据结构。ArrayList增加删除比较低效,查找很高效。随机访问效率高,随机插入、随机删除效率低。

1.随机访问&顺序访问

对于ArrayList来说,因为它是基于动态数组实现的,所以随机访问和顺序访问的效率都很高。

  • 随机访问

    若你需要访问ArrayList中特定位置的元素,例如,list.get(5)会立即返回数组中第六个元素(索引从0开始),这种直接根据下标获取元素的方式为随机访问,ArrayList的随机访问是非常效率的,它允许你直接通过数组索引以常数时间复杂度O(1)访问任何位置的元素。

  • 顺序访问

    当你需要遍历整个ArrayList时,创建一个迭代器Iterator对象,并通过调用hasNext()和next()方法进行遍历,这种顺序遍历访问的方式为顺序访问。ArrayList顺序访问也效率很高的方法。 ArrayList使用随机访问和顺序访问原理上都是通过数组索引获取元素,但是由于迭代器的使用,每次迭代都会调用Iterator的next()方法,而这些方法调用会引入一定的开销。实际使用会总体上较慢的,我们来看一个例子。

java 复制代码
 public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList();
        for(int i =0;i < 100000000;i++){
            list.add(new Integer(i));
        }
        long time = System.currentTimeMillis();
        for(int i =0;i < 100000000;i++){
            list.get(i);
        }
        System.out.println("cost 随机访问 time:"+(System.currentTimeMillis()-time));

        //顺序访问
        long time1 = System.currentTimeMillis();
        for(Integer i:list){
            //list.get(i);
        }
        System.out.println("cost  顺序访问2 time:"+(System.currentTimeMillis()-time1));
        Iterator iterator = list.iterator();
        long time2 = System.currentTimeMillis();
        //顺序访问
        while (iterator.hasNext()){
            iterator.next();
        }
        System.out.println("cost 顺序访问2 time:"+(System.currentTimeMillis()-time2));
    }

运行结果:

我们发现第一种for循环的随机访问更高效,使用iterator比增强型for(Object i:list)高效。 增强型for循环的方式为啥是最慢的,原因是增强型for循环遍历ArrayList时,底层会创建一个Iterator对象,并通过调用hasNext()和next()方法进行遍历。而普通for循环是直接通过索引来访问元素,没有额外创建Iterator对象和调用方法next()的开销。

当然这是ArrayList集合数据量很大的时候对比。当数据集合在比较小的时候,比如小于1000,基本上ArrayList的上面三种遍历方式就没有区别了。

此外ArrayList中的数组都是Object对象。如果是存放基本类型数据,会有自动装箱和拆箱的开销,获取元素的效率就很较低。

java 复制代码
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList();
        for(int i =0;i < 100000000;i++){
            list.add(i); //直接存放int 基本类型,自动装箱
        }
        long time = System.currentTimeMillis();
        for(int i =0;i < 100000000 ;i++){
            int o = list.get(i);// 自动拆箱
        }
        System.out.println("cost 随机访问 time:"+(System.currentTimeMillis()-time));

        //顺序访问
        long time1 = System.currentTimeMillis();
        for(int i:list){// 自动拆箱
            list.get(i);
        }
        System.out.println("cost  顺序访问2 time:"+(System.currentTimeMillis()-time1));
        Iterator iterator = list.iterator();
        long time2 = System.currentTimeMillis();
        //顺序访问
        while (iterator.hasNext()){
            int o = (int) iterator.next();//拆箱
        }
        System.out.println("cost 顺序访问2 time:"+(System.currentTimeMillis()-time2));
    }

运行结果如下:

ArrayList的随机访问和顺序访问搞明白了,随机访问和顺序访问效率都很高,由于ArrayList顺序访问额外创建Iterator对象和调用方法next()的开销,在数据量很大的时候,ArrayList的随机访问效率高于顺序访问。

我们再来仔细看看这个简单却又不简单的ArrayList类的其他细节,学习java封装的优雅。下回如果遇到面试官说"就是做了一个封装,这也没什么技术突出点"的时候,拿这个Josh Bloch大佬的Collection的封装举例怼他。

2.标记性接口

我们先看看ArrayList的声明,如图所示,ArrayList除了实现List接口,还实现了RandomAccess,Cloneable,Serializable等标记性接口。

List接口提供了集合Collection和Iterable操作方法和定义,比如LinkedList,Vector都是List的具体实现。RandomAccess,Cloneable,Serializable等标记性接口。

  • RandomAccess

    RandomAccess就是随机访问的意思,用以标记实现的List集合具备快速随机访问的能力。源码中注释说明也说了这个标记接口,表示List的随机访问快于顺序访问的能力

所有的List实现都支持随机访问的,只是基于基本结构的不同,实现的速度不同罢了,这里的快速随机访问,那么就不是所有List集合都支持了。 比如,ArrayList和Vector基于数组实现,天然带下标,可以实现常量级的随机访问,而LinkedList基于链表实现,随机访问需要依靠遍历实现,复杂度为O(n) ,所以ArrayList和Vector具备快速随机访问功能,而LinkedList没有。

  • Cloneable

    Cloneable接口没有包含clone()方法,只是标记类是否支持克隆,如果不实现此接口,调用对象的clone()就会抛出异常。即使重写Object的clone(),但是没有加上Cloneable接口也会报错。

ArrayList中对于Object的clone()。我们看到是基于Arrays.copyOf(elementData, size)给元素复制。注释说这是一个a shallow copy of this ArrayList instance,就是一个浅拷贝。原因是Arrays.copyOf()是基于System.arraycopy()实现的,所以ArrayList中的其他元素也都是copy原型对象的地址。都是指向一个对象。

  • Serializable

    Serializable同样属于标记接口,他表示类的对象可以实现序列化的能力。关于序列化先关知识原理可能大家都明白,我就不赘述了。我们只要看看ArrayList他是怎么实现的。看源码有实现writeObjectreadObject方法,for循环将每个元素写入和读出。而其中每个元素对象也需要实现序列化。否则就会报NotSerializableException异常

我们看到截图代码中有一个if (modCount != expectedModCount)的判断,不相等就会出现ConcurrentModificationException。modCount是啥?这个就是非线程安全的原因。

3.非线程安全

ArrayList非线程安全的,modCount为列表在结构上被修改的次数。

注释的意思是,此字段由迭代器和list Iterator方法返回的迭代器与列表迭代器实现使用。如果此字段的值意外更改,迭代器(或列表迭代器)将抛出ConcurrentModificationException,也就是我们每次增删操作或者改变列表大小的操作,modCount就会加1。如下源码:

java 复制代码
    public boolean add(E e) {
        //最终在ensureExplicitCapacity方法修改modCount
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    private void ensureExplicitCapacity(int minCapacity) {
         //修改
        modCount++;
        //省略
        .......
    }

    private void fastRemove(int index) {
         //修改
        modCount++;
        //省略
        .......
    }

    public void clear() {
        //修改
        modCount++;
         //省略
        .......
    }
    //每次new Itr(),初始化iterator的expectedModCount为当前modCount
    public Iterator<E> iterator() {
        return new Itr();
    }

    /**
     * An optimized version of AbstractList.Itr
     */
    private class Itr implements Iterator<E> {
         //省略
        .......
        
        int expectedModCount = modCount;
 
        @SuppressWarnings("unchecked")
        public E next() {
            //判断修改版本
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //省略
            .......
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            //省略
            .......
            //断修改版本
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;//修改expectedModCount
                limit--;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        //省略
        .......
    }

所以在iterator进行遍历的时候,对list进行增加或者删除都会报ConcurrentModificationException异常。如果想要在遍历过程中删除元素,可以使用迭代器本身iteratorremove()方法,因为他有对本身的expectedModCount重新赋值。

可以思考怎么优化ArrayList的新增删除的优化。新增上优化方向就是减少扩容的次数。对于删除的优化方向是减少每次removeSystem.arraycopy操作.

4.扩容方案

ArrayList是一个动态数组,它是怎么实现扩容的呢。上面我们发现 ensureExplicitCapacity 在每次添加元素的时候就会调用。我们先看看ArrayList的初始大小

java 复制代码
    /**
   * Default initial capacity.
   */
  private static final int DEFAULT_CAPACITY = 10;

  /**
   * Shared empty array instance used for empty instances.
   */
  private static final Object[] EMPTY_ELEMENTDATA = {};

  /** 
   * first element is added.
   */
  private static final Object[] DEFAULTCAPACITY_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);
      }
  }

  /**
   * Constructs an empty list with an initial capacity of ten.
   */
  public ArrayList() {
      this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  }

发现有一个DEFAULT_CAPACITY常量。但是初始化时候,并没有使用,默认在不设置initialCapacity的情况下,都是空数组{}。当我们开始添加元素的时候,才开始初始化数组的初始容量。

java 复制代码
public boolean add(E e) {
      ensureCapacityInternal(size + 1);  // Increments modCount!!
      elementData[size++] = e;
      return true;
}
private void ensureCapacityInternal(int minCapacity) {
      //如果为空数组,初始化数组的初始容量DEFAULT_CAPACITY,当minCapacity大于DEFAULT_CAPACITY的时候,就开始新的扩容
      if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
          minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
      }
    
      ensureExplicitCapacity(minCapacity);
  }

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


      // 判断是需要扩容
      if (minCapacity - elementData.length > 0)
          grow(minCapacity);
  }

  private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

  
  private void grow(int minCapacity) {
      // overflow-conscious code
      int oldCapacity = elementData.length;
      //在原来的oldCapacity基础上 扩大1.5倍
      int newCapacity = oldCapacity + (oldCapacity >> 1);
      if (newCapacity - minCapacity < 0)
          newCapacity = minCapacity;
      //判断是否超过最大值  MAX_ARRAY_SIZE,如果超过,扩容到最大的Integer.MAX_VALUE
      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);
  }

  private static int hugeCapacity(int minCapacity) {
      //如果minCapacity已经开始小于0了,已经超出int的最大字节数,比Integer.MAX_VALUE还大,那么抛出OutOfMemoryError异常。
      if (minCapacity < 0) // overflow
          throw new OutOfMemoryError();
          //否则直接给到Integer.MAX_VALUE最大的大小
      return (minCapacity > MAX_ARRAY_SIZE) ?
          Integer.MAX_VALUE :
          MAX_ARRAY_SIZE;
  }

从源码中,我们总结ArrayList的扩容方案,ArrayList没有指定初始容量的情况下,默认elementData为空数组,再添加元素的时候,判断是否需要扩容,每次扩容为当前容量的1.5倍,当数组长度大于Integer.MAX_VALUE - 8的时候,开始设置超大容量Integer.MAX_VALUE,当容量超过Integer.MAX_VALUE的时候,就抛出OutOfMemoryError异常。

和ArrayList不同的是Vector是线程安全的,是因为Vector对于操作使用synchronized加锁了。

5.总结

  • ArrayList的随机访问和顺序访问效率都很高,由于ArrayList顺序访问额外创建Iterator对象和调用方法next()的开销,在数据量很大的时候,ArrayList的随机访问效率高于顺序访问。
  • ArrayList实现RandomAccess,Cloneable,Serializable等标记接口,具有快速访问的能力,又具有克隆和序列化的能力。其中克隆是浅拷贝,序列化过程中元素也需要对应实现序列化能力。
  • ArrayList是非线程安全的,主要在迭代器对于modCount在列表在结构上被修改的次数的预期判断。
  • ArrayList没有指定初始容量的情况下,默认elementData为空数组,再添加元素的时候,判断是否需要扩容,每次扩容为当前容量的1.5倍,当数组长度大于Integer.MAX_VALUE - 8的时候,开始设置超大容量Integer.MAX_VALUE,当容量超过Integer.MAX_VALUE的时候,就抛出OutOfMemoryError异常。
相关推荐
L.S.V.3 分钟前
Java 溯本求源之基础(三十)——封装,继承与多态
java·开发语言
码农爱java4 分钟前
设计模式--装饰器模式【结构型模式】
java·设计模式·面试·装饰器模式·原理·23 中设计模式
liangmou212120 分钟前
解释小部分分WPI函数(由贪吃蛇游戏拓展)
android·游戏·c#
星就前端叭1 小时前
【开源】一款基于SpringBoot的智慧小区物业管理系统
java·前端·spring boot·后端·开源
带刺的坐椅1 小时前
RxSqlUtils(base R2dbc)
java·reactor·solon·r2dbc
silence2501 小时前
深入了解 Reactor:响应式编程的利器
java·spring
亚瑟-灰太狼1 小时前
memory泄露分析方法(Binder,Window,View篇)
android
weixin_SAG1 小时前
21天掌握javaweb-->第19天:Spring Boot后端优化与部署
java·spring boot·后端
m0_748247551 小时前
SpringMVC跨域问题解决方案
java
Elcker1 小时前
KOI技术-事件驱动编程(Sping后端)
java·spring·架构