【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异常。
相关推荐
TeleostNaCl1 分钟前
Android 应用开发 | 一种限制拷贝速率解决因 IO 过高导致系统卡顿的方法
android·经验分享
KiddoStone8 分钟前
多实例schedule job同步数据流的数据一致性设计和实现方案
java
用户20187928316714 分钟前
📜 童话:FileProvider之魔法快递公司的秘密
android
岁忧30 分钟前
(LeetCode 每日一题) 1865. 找出和为指定值的下标对 (哈希表)
java·c++·算法·leetcode·go·散列表
YuTaoShao33 分钟前
【LeetCode 热题 100】240. 搜索二维矩阵 II——排除法
java·算法·leetcode
考虑考虑1 小时前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干2 小时前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying2 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·2 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
Bug退退退1233 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq