【08期】ArrayList常见面试题

简介

ArrayList是我们开发中非常常用的数据存储容器之一,其底层是数组实现的,我们可以在集合中存储任意类型的数据,ArrayList是线程不安全的,非常适合用于对元素进行查找,效率非常高。

线程安全性

对ArrayList的操作一般分为两个步骤,改变位置(size)和操作元素(e)。所以这个过程在多线程的环境下是不能保证具有原子性的,因此ArrayList在多线程的环境下是线程不安全的。

源码分析

1. 属性分析

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

/**
 * 如果自定义容量为0,则会默认用它来初始化ArrayList。或者用于空数组替换。
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 如果没有自定义容量,则会使用它来初始化ArrayList。或者用于空数组比对。
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 这就是ArrayList底层用到的数组

 * 非私有,以简化嵌套类访问
 * transient 在已经实现序列化的类中,不允许某变量序列化
 */
transient Object[] elementData;

/**
 * 实际ArrayList集合大小
 */
private int size;

/**
 * 可分配的最大容量
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

扩展:什么是序列化

序列化是指:将对象转换成以字节序列的形式来表示,以便用于持久化和传输。

实现方法:实现Serializable接口。

然后用的时候拿出来进行反序列化即可又变成Java对象。


transient关键字解析

Java中transient关键字的作用,简单地说,就是让某些被修饰的成员属性变量不被序列化。

有了transient关键字声明,则这个变量不会参与序列化操作,即使所在类实现了Serializable接口,反序列化后该变量为空值。

那么问题来了:ArrayList中数组声明:transient Object[] elementData;,事实上我们使用ArrayList在网络传输用的很正常,并没有出现空值。

原来:ArrayList在序列化的时候会调用writeObject()方法,将sizeelement写入ObjectOutputStream;反序列化时调用readObject(),从ObjectInputStream获取sizeelement,再恢复到elementData

那为什么不直接用elementData来序列化,而采用上诉的方式来实现序列化呢?

原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间

2. 构造方法分析

根据initialCapacity 初始化一个空数组,如果值为0,则初始化一个空数组:

复制代码
/**
 * 根据initialCapacity 初始化一个空数组
 */
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);
    }
}

不带参数初始化,默认容量为10:

复制代码
/**
 * 不带参数初始化,默认容量为10
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

通过集合做参数的形式初始化:如果集合为空,则初始化为空数组:

复制代码
/**
 * 通过集合做参数的形式初始化
 */
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

3. 主干方法

trimToSize()方法:

用来最小化实例存储,将容器大小调整为当前元素所占用的容量大小。

复制代码
/**
 * 这个方法用来最小化实例存储。
 */
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

clone()方法

用来克隆出一个新数组。

复制代码
public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

通过调用Objectclone()方法来得到一个新的ArrayList对象,然后将elementData复制给该对象并返回。

add(E e)方法

在数组末尾添加元素

复制代码
/**
 * 在数组末尾添加元素
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

看到它首先调用了ensureCapacityInternal()方法.注意参数是size+1,这是个面试考点。

复制代码
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

这个方法里又嵌套调用了两个方法:计算容量+确保容量

计算容量:如果elementData是空,则返回默认容量10和size+1的最大值,否则返回size+1

复制代码
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

计算完容量后,进行确保容量可用:(modCount不用理它,它用来计算修改次数)

如果size+1 > elementData.length证明数组已经放满,则增加容量,调用grow()

复制代码
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

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

增加容量:默认1.5倍扩容。

  1. 获取当前数组长度=>oldCapacity

  2. oldCapacity>>1 表示将oldCapacity右移一位(位运算),相当于除2。再加上1,相当于新容量扩容1.5倍。

  3. 如果 newCapacity>1=1, 1<2所以如果不处理该情况,扩容将不能正确完成。

  4. 如果新容量比最大值还要大,则将新容量赋值为VM要求最大值。

  5. 将elementData拷贝到一个新的容量中。

    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);
    }


size+1的问题

好了,那到这里可以说一下为什么要size+1。

size+1代表的含义是:

  1. 如果集合添加元素成功后,集合中的实际元素个数。
  2. 为了确保扩容不会出现错误。

假如不加一处理,如果默认size是0,则0+0>>1还是0。 如果size是1,则1+1>>1还是1。有人问:不是默认容量大小是10吗?事实上,jdk1.8版本以后,ArrayList的扩容放在add()方法中。之前放在构造方法中。我用的是1.8版本,所以默认ArrayList arrayList = new ArrayList();后,size应该是0.所以,size+1对扩容来讲很必要.

复制代码
public static void main(String[] args) {
    ArrayList arrayList = new ArrayList();
    System.out.println(arrayList.size());
}

输出:0

事实上上面的代码是证明不了容量大小的,因为size只会在调用add()方法时才会自增。有办法的小伙伴可以在评论区大显神通。


add(int index, E element)方法

复制代码
public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

rangeCheckForAdd()是越界异常检测方法。ensureCapacityInternal()之前有讲,着重说一下System.arrayCopy方法:

复制代码
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

代码解释:

  • Object src : 原数组
  • int srcPos : 从元数据的起始位置开始
  • Object dest : 目标数组
  • int destPos : 目标数组的开始起始位置
  • int length : 要copy的数组的长度

示例:size为6,我们调用add(2,element)方法,则会从index=2+1=3的位置开始,将数组元素替换为从index起始位置为index=2,长度为6-2=4的数据。

异常处理:

复制代码
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

set(int index,E element)方法

复制代码
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

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

逻辑很简单,覆盖旧值并返回。

indexOf(Object o)方法

根据Object对象获取数组中的索引值。

复制代码
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

如果o为空,则返回数组中第一个为空的索引;不为空也类似。

注意:通过源码可以看到,该方法是允许传空值进来的。

get(int index)方法

返回指定下标处的元素的值。

复制代码
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

rangeCheck(index)会检测index值是否合法,如果合法则返回索引对应的值。

remove(int index)方法

删除指定下标的元素。

复制代码
public E remove(int index) {
    // 检测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;
}

这里又碰到了System.arraycopy()方法,详情请查阅上文。

大概思路:将该元素后面的元素前移,最后一个元素置空。

ArrayList优缺点

优点:

  1. 因为其底层是数组,所以修改和查询效率高。
  2. 可自动扩容(1.5倍)。

缺点:

  1. 插入和删除效率不高。
  2. 线程不安全。

手写ArrayList

那面试手写ArrayList应该就不是问题了。

下面贴出我手写的一个简单阉割版的ArrayList:

复制代码
public class MyArrayList {

    // 非私有,以简化嵌套类访问
    // transient 在已经实现序列化的类中,不允许某变量序列化
    transient Object[] elementData;

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

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

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 实际ArrayList集合大小
    private int size;
    /**
     * 构造方法
     */
    public MyArrayList(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);
        }
    }
    public MyArrayList(){
        this(DEFAULT_CAPACITY);
    }

    public void add(Object o){
        //1. 判断数据容量是否大于 elementData
        ensureExplicitCapacity(size+1);
        //2. 使用下标进行赋值
        elementData[size++] = o;
    }
    private void ensureExplicitCapacity(int minCapacity){
        if (size == elementData.length){
            // 需要扩容,扩容1.5倍(ArrayList默认扩容1.5倍)
            // 注意:如果oldCapacity值为1
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            // 如果新容量 < 最小容量, 则将最小容量赋值给新容量
            // 如果 oldCapacity=1, 则 minCapacity=1+1=2   newCapacity=1+(1>>1)=1
            if (newCapacity - minCapacity < 0){
                newCapacity = minCapacity;
            }
            // 创建新数组
            Object[] objects = new Object[newCapacity];
            // 将数据复制给新数组
            System.arraycopy(elementData, 0, objects, 0, elementData.length);
            // 修改引用
            elementData = objects;
        }
    }
    public Object get(int index) {
        rangeCheck(index);
        return elementData[index];
    }
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException("下标越界");
    }

    /**
     * 通过下标删除
     * @param index
     * @return
     */
    public Object remove(int index) {
        rangeCheck(index);

//        modCount++;
        // 先查出元素
        Object oldValue = elementData[index];
        // 找出置换结束位置
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 从 index+1 开始 将值覆盖为 index-numMoved 的值
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
    public boolean remove(Object o) {
            for (int index = 0; index < size; index++){
                if (o.equals(elementData[index])) {
                    remove(index);
                    return true;
                }
            }
        return false;
    }
}

本文由mdnice多平台发布

相关推荐
神奇小汤圆4 小时前
浅析二叉树、B树、B+树和MySQL索引底层原理
后端
文艺理科生4 小时前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling4 小时前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅4 小时前
springBoot项目有几个端口
java·spring boot·后端
Luke君607974 小时前
Spring Flux方法总结
后端
define95274 小时前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li5 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶6 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring
Coder_Boy_6 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例中相关概念
java·人工智能·spring boot·后端·spring
Java后端的Ai之路6 小时前
【Spring全家桶】-一文弄懂Spring Cloud Gateway
java·后端·spring cloud·gateway