一文详解ArrayList

274 阅读12分钟

一文详解ArrayList

概念

  • 数组是线性表数据结构。它用一组连续性的内存空间,来存储一组相同类型的数据。
  • 可以简单理解数组就是一个存储数据容器。

特征

因为数组连续内存空间的相同类型数据,具备:读取速度快、随机读取、尾部新增元素快等特点

image.png

例子

有一个长度为5的Long类型的数组,需要分配连续内存空间则为1000-1039,内存块首地址1000。
image.png
如果需要访问数组中的数据的时候,需要先通过一个寻址公式来找到内存地址,然后再读取其中的数据。
寻址公式为 a[i]_address = first_address + i * data_type_size
释:first_address为内存块的首地址1000(在很多语言中,都将数组的第一个下标定为0,因为在寻址的时候,可以直接得到内存地址,而不需要再将数值减一后再进行计算),data_type_size为数据类型的大小。
数组中元素为long类型,所以datatype_size为8个字节。
当我们需要读取a[0]时候,
a[0]_address = first_address + 0 * data_type_size = 1000 + 0 * 8 =1000;
所以数组适合查询,支持随机访问,在根据下标进行随机访问时时间复杂度为O(1)(注意:按照下标进行随机访问的时候时间复杂度O(1);如果使用二分查找的话,即便已经进行过排序的数组,时间复杂度也是O(logn))。
但是数组存储空间是连续的,在对数组进行增加和删除的时候(尾部操作除外),是比较低效的。
插入操作:如果插入的数据在最后一个,数组就不需要进行移动,最好时间复杂度为O(1);
如果数组数据是有序,要在第y位插入数据,则后面的每一个数据都需要往后挪一个,最坏时间复杂度为O(n);因为在每一个位置插入的概率都是一样的,所以平均时间复杂度为(1+2+…+n)/n=O(n)()。
如果数据是无序的话,要在第y位插入数据,将第y位的数据移动到整个数组的最后面,然后再将需要插入的数据插入即可,时间复杂度就降为了O(1)。
接下来说删除操作,类似插入操作。

java ArrayList

ArrayList 是jdk基于数组[]封装的一个类,具备数组的特性并且增加了动态扩展的特性。

类图

image.png
ArrayList 实现接口
java.util.List
java.util.RandomAccess
java.io.Serializable
java.lang.Cloneable
List:jdk抽象数组具备特征通用行为,定义了jdk中数组的基本操作。
主要操作:查找、新增、移除、替代、迭代遍历等
RandomAccess:空接口,作为支持快速随机访问的标志;在这里表示ArrayList支持快速随机访问。
详细说明:juejin.cn/post/684490…
Serializable:空接口,作为支持序列化的标志;在这里表示ArrayList支持序列化。
详细说明:www.jianshu.com/p/e554c787c…
Cloneable:空接口,作为支持克隆的标志(Object.clone()),在这里表示ArrayList支持克隆。
详细说明:www.zhihu.com/question/52…

ArrayList继承抽象类
java.util.AbstractList
AbstractList 提供了 List 接口的通用方法实现,大幅度的减少了实现迭代遍历相关操作的代码。例如说 #iterator()、#indexOf(Object o) 等方法。(但是ArrayList重写大部分AbstractList的方法)

属性

Object[] elementData;
元素数组。
int size;
数组大小;(大小指的是已经使用元素的数组大小)
image.png

方法

数组方法比较简单,下面主要抽取数组常用方法进行解析

构造方法

ArrayList(int initialCapacity);
ArrayList();
ArrayList(Collection<? extends E> c);

/**
 * 默认初始化容量
 */
private static final int DEFAULT_CAPACITY = 10;


/**
 * 共享的空数组对象
 *
 * 如果传入的初始化大小或者集合大小为 0 时,将 {@link #elementData} 指向它。
 *
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 共享的空数组对象,用于 {@link #ArrayList()} 构造方法。
 *
 * 通过使用该静态变量,和 {@link #EMPTY_ELEMENTDATA} 区分开来,在第一次添加元素时。
 *
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    this.elementData = 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);
        }
    }

/**
 * 省略ArrayList(Collection<? extends E> c)
 */

注意:如果有参构造方法的数组大小为零,则创建一个空的数组,默认无参构造方法创建也是一个空的数组(不是大小为10数组),但是有参构造方法初始化为EMPTY_ELEMENTDATA这个空数组,而无参构造方法初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个空数组。
问题:EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA都是空数组,为什么声明两个呢?
答案:DEFAULTCAPACITY_EMPTY_ELEMENTDATA 首次扩容为 10 ,而 EMPTY_ELEMENTDATA 按照 1.5 倍扩容从 0 开始而不是 10

新增方法

add(E e);
add(int index, E element)

/**
 * 将指定的元素追加到此列表的末尾
 */
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
	
/**
 * 计算容量
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

/**
 *  modCount++ 增加数组修改次数
 */	
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
	
/**
 *  默然按照1.5背进行扩容
 */	
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);
    }

grow(int minCapacity)流程
image.png

删除方法

remove(int index);按照下标进行删除;
remove(Object o);按照元素进行删除;

 public E remove(int index) {
        // 校验 index 不要超过 size
        rangeCheck(index);
        // 增加数组修改次数
        modCount++;
        //获取下标元素
        E oldValue = elementData(index);

        //记录需要移动下标
        int numMoved = size - index - 1;
        if (numMoved > 0)
            //数组拷贝
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //数组末尾null gc就可以清楚回收
        elementData[--size] = null; // clear to let GC do its work

        // 返回该位置的原值
        return oldValue;
    }

 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);
        //数组末尾null gc就可以清楚回收
        elementData[--size] = null; // clear to let GC do its work
    }

获得指定位置的元素

E get(int index);

 public E get(int index) {
     // 校验 index 不要超过 size
        rangeCheck(index);
     // 获得 index 位置的原元素
        return elementData(index);
    }

private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

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

设置指定位置的元素

E set(int index, E element);

public E set(int index, E element) {
    // 校验 index 不要超过 size
    rangeCheck(index);
    // 获得 index 位置的原元素
    E oldValue = elementData(index);
    // 修改 index 位置为新元素
    elementData[index] = element;
    return oldValue;
}

迭代器

Iterator iterator();

 public Iterator<E> iterator() {
        return new Itr();
    }

 //下次访问元素所在数组位置的下标
 int cursor;       // index of next element to return
 /**
 *上一次访问元素的位置
 * 1. 初始化为 -1 ,表示无上一个访问的元素
 * 2. 遍历到下一个元素时,lastRet 会指向当前元素,而 cursor 会指向下一个元素。这样,如果我们要实现 remove 方法,移除当前元素,就可以实现了。
 * 3. 移除元素时,设置为 -1 ,表示最后访问的元素不存在了,都被移除咧。
 */
 int lastRet = -1; // index of last element returned; -1 if no such
 //创建迭代器时,数组修改次数。
 int expectedModCount = modCount;

//判断是否存在下一个元素
public boolean hasNext() {
            return cursor != size;
        }

//获取下一个元素
 public E next() {
            //校验是否数组发生了变化
            checkForComodification();
            //i 记录当前 cursor 的位置
            int i = cursor;
            //判断下标是否超过数组元素范围
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            //判断是否超出数组长度,判断是否存在被修改
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            //cursor指向下一个
            cursor = i + 1;
            //lastRet指向当前元素下标,并返回元素
            return (E) elementData[lastRet = i];
        }

public void remove() {
            //如果lastRet小于0,代码当前没有指向任何元素,故抛出错误
            if (lastRet < 0)
                throw new IllegalStateException();
            //校验数组是否发生变化
            checkForComodification();

            try {
                //移除元素
                ArrayList.this.remove(lastRet);
                //cursor 指向 lastRet 位置,因为被移了,所以需要后退下
                cursor = lastRet;
                //lastRet 标记为 -1 ,因为当前元素被移除了
                lastRet = -1;
                //记录新的数组的修改次数
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

常见面试题

1、ArrayList、LinkedList、Vector的区别是什么?

考察点分析:

ArrayList、LinkedList、Vector 3个类都继承了List接口,主要考察对基础知识的了解程度,方便在合适的场景使用不同的容器。

答案:
ArrayListLinkedListVector
底层结构数组双向链表数组
是否线程安全
是否可以存null
特点随机访问、查询快;在尾部添加效率高,其他地方插入慢插入、删除快;随机访问查询慢;用synchronized关键字,只能单线程进行查询等
延伸

为什么ArrayList线程不安全,而Vector线程安全,如果实现ArrayList线程安全该怎么实现。
举例图解ArrayList线程不安全
image.png
原数组:{a} ,length=1,size=1
线程1需要插入{a,b,c,d,e,f,g} ,线程2需要插入{a,b,c}
假设线程1执行到ensureCapacityInternal时,此时的size=1,进行扩容,因为要插入7个元素,数组元素=7+1,数组按1.5倍扩容 = 3,所以数组扩容到8;此时线程2执行到ensureCapacityInternal(size+1)时,因为数组长度8还未到达扩容的条件,无需扩容;线程1进行添加元素,size+7,这时候size=8,这时线程1执行结束;线程2继续执行,由于刚才已经判断过不需要扩容,所以直接添加元素,但问题是线程1执行后size=8了,线程2再往里添加元素自然就报数组下标越界了.

Vector线程安全
image.png
vector之所以是线程安全的,是因为官方在可能涉及到线程不安全的操作都进行了synchronized操作,相当于官方帮你加了一把同步锁。
re: addElement(E obj)
image.png
ArrayList 实现线程安全:
List list = Collections.synchronizedList(new ArrayList());
CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
特点:
在线程数目增加时CopyOnWriteArrayList的写操作性能下降非常严重,而Collections.synchronizedList虽然有性能的降低,但下降并不明显。
在多线程进行读时,Collections.synchronizedList和CopyOnWriteArrayList均有性能的降低,但是Collections.synchronizedList的性能降低更加显著。

2、ArrayList为什么要进行扩容?它的扩容机制是什么样的?

考察点分析:

扩容是ArrayList一个非常重要的功能,考察对ArrayList源码熟悉程度

答案:

(1)如果数组是默认DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空数组,且插入元素数小于默认首次扩容长度10,按照默认长度10进行扩容;如果插入元素数大于默认长度10,且小于最大整数,则按照元素数进行扩容;
如果插入元素大于最大整数,则按照最大整数进行扩容。
(2)如果数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空数组,且插入元素数+数组原来的元素小于数组1.5倍扩容长度,按照数组1.5倍进行扩容;如果插入元素数+数组原来的元素大于数组1.5倍扩容长度,且小于最大整数,则按照插入元素数+数组原来的元素进行扩容;如果插入元素数+数组原来的元素大于最大整数,则按照最大整数进行扩容。
源码及流程查看前面add方法

3、Iterator 和 ListIterator 有什么区别?

考察点分析:

Iterator及ListIterator 特性理解(ArrayList 的 Iterator 源码前面已经解析)

答案:

Iterator只可以向前遍历,而LIstIterator可以双向遍历。
ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

4、List如何一边遍历,一边删除?

考察点分析:

数组实战中使用注意事项及熟练程度。

答案:

(1)数组遍历方式
使用foreach循环遍历;
使用for循环遍历;
使用Iterator循环遍历;
(2)数组元素删除方式
ArrayList的remove()方法;
Iterator的remove()方法;
removeIf()方法;
错误示例:

List<String> list = new ArrayList<>();
    list.add("1");
    list.add("2");
    list.add("3");

    for (String str : list) {
        if (str.equals("1")) {
            list.remove(str);
        }
    }

直接跑异常java.util.ConcurrentModificationException,然后一脸懵;
image.png
通过查看代码块的字节码
image.png
foreach循环在实际执行时,其实使用的是Iterator,使用的核心方法是hasnext()和next()。
image.png
image.png
就沿着Iterator分析问题:
next()每次都会校验modCount != expectedModCount,
而remove()则会为modCount+1
如果使用foreach方法去一边遍历一遍删除,则会报错误ConcurrentModificationException

常用方式:
(1)iterator遍历,通过iterator.remove()方法移除

 List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            if (str.equals("1")) {
                iterator.remove();
            }
        }

不报错原因:iterator.remove()方法中回去主动修正expectedModCount
image.png
(2)for循环删除

 List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        for (int i = 0; i < list.size(); i++) {
            String item = list.get(i);
            if (item.equals("1")) {
                list.remove(i);
                //这里一定要修正下标
                i = i - 1;
            }
        }

(3)使用removeIf()方法

  List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.removeIf(f -> "1".equals(f));

removeIf()方法源码,实质运用iterator遍历和删除
image.png

5、储元素的数组elementData为什么是transient修饰的

考察点分析:

ArrayList源码一些细节的了解程度,transient系列化中作用。

答案:

(1)对序列化、反序列化有了解。(blog.csdn.net/qq_62414755…
(2)transient关键字作用:(blog.csdn.net/w139074301/…
一旦变量被transient修饰,变量不再是对象持久化的一部分,该变量在反序列化后也无法获得;
transient关键字只能修饰变量,不能修饰方法和类;
一个静态变量不管是否被transient修饰,均不能被序列化;
(3)数组已经实现实现Serializable接口,可以序列化,为什么用transient修饰elementData,难道elementData不需要序列化?
ArrayList在序列化的时候通过调用writeObject()方法,将size和element写入ObjectOutputStream;反序列化时通过调用readObject(),从ObjectInputStream获取size和element,再恢复到elementData。而不是通过elementData来序列化。elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。

6、你知道集合中的Fail-fast机制吗?为什么要有这样的机制?

考察点分析:

考察你对集合中的一些核心设计理念的了解。

答案:

fail-fast概念:快速失败系统,通常设计用于停止有缺陷的过程,这是一种理念,在进行系统设计时优先考虑异常情况,一旦发生异常,直接停止并上报。
我们通常说的Java中的fail-fast机制,默认指的是Java集合中的一种错误检测机制。
例子:ArrayList中的get方法

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

        return elementData(index);
    }

private void rangeCheck(int index) {
        //这里就是数组采取的fail-fast的体现
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 
    }

数组通过下标获取元素,当下标大于等于size时,就直接抛出异常,并明确提示异常原因,这就是fail-fast的应用。为了避免执行接下来复杂的代码,另一方面可以根据错误原因进行针对性处理。
blog.csdn.net/OYMNCHR/art…