你对ArrayList了解多少?

1.前言

ArrayList 是我们常用的数据结构,但是你对ArrayList了解有多少了?不妨来考一考自己。

2.ArrayList每日一问(答案见末尾)

(1)jdk1.8ArrayList默认容量是多大?

(2)ArrayList 扩容多大?

(3)ArrayList 是线程安全的吗?为什么?

(4)ArrayList 中 elementData 为什么使用 transient 修饰?

(5)ArrayList list = new ArrayList(20); 中的list扩充几次?

如果你对以上的问题全部都能答对,嗯,确认过眼神,大佬喝茶~

如果没有答对,也不要灰心,我们来一起学一学ArrayList

3.ArrayList底层分析(基于jdk1.8)

面试造火箭,开发拧螺丝,实际我们大多数都是在CRUD,ArrayList也不例外,我们就从ArrayList的CRUD展开

3.1 ArrayList的成员变量


//默认的容量
private static final int DEFAULT_CAPACITY = 10;

//空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};

//默认大小的空数组实例,这里有的同学问了,和上面的那个有什么区别吗?
//官方解释的是:与上面的实例分开,以至于我们知道何时会添加第一个元素
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//Object数组,也就是ArrayList的底层
transient Object[] elementData

//大小
private int size;

3.2 ArrayList的构造方法

无参的构造方法,这里我们看到无参的构造函数,也就是当我们不给ArrayList的大小时,ArrayList会是一个空数组

    //这里我们看到初始了一个空数组
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  }

带初始化容量的构造方法

    
    //1.如果初始化的值大于0,直接new一个initialCapacity的Object数组
    //2.如果等于0,会是一个上面成员变量中的第一种,EMPTY_ELEMENTDATA
    //3.小于0,会跑出IllegalArgumentException异常
    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);
        }
    }

Collection类型的构造器

    
    //1.将Collection的集合转换成数组,ArrayList底层
    //2.如果c.toArray()不是一个Object数组,使用Arrays.copyOf方法进行转换
    //3.如果c.toArray()的长度等于0,使用自己的成员变量
    
   public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

3.3 ArrayList add方法(分为两种)

直接添加

(1)判断是否个空数组,如果是空数组,就给默认值

(2)判断是否需要扩容

(3)正式扩容

(4)添加元素


    //是否需要扩容
    //添加元素
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
    }
    
    
     private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    //如果elementData,是个空数组的话,minCapacity也就是为1
    //这里会取默认的大小:10,类似懒加载,默认是个空数组,当我们
    //第一次添加时就会给个默认大小10,
      private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    //确定是否需要扩容
       private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    //真正的扩容代码
    //旧数组容量+旧数组容量右移一位=1.5倍
    // 检查新容量的大小是否小于最小需要容量,如果是的,新容量等于最小需要容量
    //如果新容量大于MAX_ARRAY_SIZE,使用hugeCapacity比较二者
    //使用Arrays.copyOf 进行拷贝
        private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        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);
    }
    
    //对minCapacity和MAX_ARRAY_SIZE进行比较
    //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
    //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
    //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;
    }
    

指定位置添加,就好比我们插队一样,需要在中间挪出一个位置才能进去,上面如果理解了,这里就比较好理解,就多了一个判断,判断有没有超过最大值,然后调用System.arraycopy进行拷贝


   public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    
   private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UOEAi0zc-1597248035484)(http://52jdk.com/ArrayListAdd.png)]

3.4 get方法(两行代码)

第一步判断下表是否越界,第二步直接通过数组下表获取元素


  public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }
    
    
      E elementData(int index) {
        return (E) elementData[index];
    }


3.5 remove方法(两种方法)

按照下标remove
(1) 判断下标是否越界,如果越界就会直接抛出异常

(2) 在数组中查询该元素

(3)计算需要移动的数目

(4)如果numMoved大于0,说明要删除的元素后面的都需要左移,如果是最后一个元素,则不需要移动

(5)使原数组大小减一,并赋值为空,有利于GC

(6)返回旧的元素

    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; // clear to let GC do its work

        return oldValue;
    }
    
    //src:源数组   
    //srcPos:从源数组的srcPos位置处开始移动
    //dest:目标数组
    //desPos:源数组的srcPos位置处开始移动的元素,这些元素从目标数组的desPos处开始填充
    //length:移动源数组的长度
    public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);


按照元素值来删除,这里也比较好理解,就是先遍历,获取元素的下标,然后利用上面的删除代码进行删除

      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;
        }
        
        
     private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TTmIOap9-1597248035486)(http://52jdk.com/ArrayList%20remove.png)]

3.6 ensureCapacity 方法

这个方法可以在 add 大量元素之前用 ensureCapacity 方法,以减少增量从新分配的次数


    public void ensureCapacity(int minCapacity) {
            int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                // any size if not default element table
                ? 0
                // larger than default for default empty table. It's already
                // supposed to be at default size.
                : DEFAULT_CAPACITY;
    
            if (minCapacity > minExpand) {
                ensureExplicitCapacity(minCapacity);
            }
        }
    
     private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

3.7 writeObject和readObject

ArrayList 为什么要定义这两个读写流的方法呢,这就要回到我们第四个问题了,
ArrayList 中 elementData 为什么使用 transient 修饰?

由于 ArrayList 是基于动态数组实现的,所以并不是所有的空间都被使用。因此使用了 transient 修饰,可以防止被自动序列化。
因此 ArrayList 自定义了序列化与反序列化,具体可以看 writeObject 和 readObject 两个方法。
需要注意的一点是,当对象中自定义了 writeObject 和 readObject 方法时,JVM 会调用这两个自定义方法来实现序列化与反序列化。

writeObject 官方解释(意思是:通过流来序列化ArrayList)

/**
* Save the state of the ArrayList instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the ArrayList
* instance is emitted (int), followed by all of its elements
* (each an Object) in the proper order.
*/

readObject 官方解释 (意思是:通过流来反序列化ArrayList)

/**
* Reconstitute the ArrayList instance from a stream (that is,
* deserialize it).
*/

4.答案

jdk1.8如果不给初始值容量的话,是默认给了一个空数组,也就是0,当在第一次添加时,会重新比较大小,给10的容量

ArrayList 扩容1.5倍,记住右移一位:newCapacity = oldCapacity + (oldCapacity >> 1)

ArrayList不是线程安全的,当我们add添加或者删除时,elementData[size++] = e; elementData[–size] = e; 这两个操作,并不是原子操作,都是分为两步操作

3.7咋们已经分析到了

默认ArrayList的长度是10个,所以如果你要往list里添加20个元素肯定要扩充一次(newCapacity 扩充为原来的1.5倍,但和输入的minCapacity相比发现小于minCapacity,于是 newCapacity = minCapacity,所以只扩容一次,具体见扩容里的grow方法),但是这里显示指明了需要多少空间,所以就一次性为你分配这么多空间,也就是不需要扩充了!

5.提问

为什么添加删除操作都有 modCount++ ,有什么作用,相信聪明的你已经知道了答案,欢迎在评论区留言

6.关于

程序员小R:非专业咖啡调制师,公众号“程序员小R”,回复“666”获取1000页的最新面经题

原文地址:https://www.cnblogs.com/robotsh/p/14131001.html