源码分析:逐行分析ArrayList

概述:底层数据结构是数组 ArrayList是一个动态数组 它允许任何符合规则的元素插入甚至包括null

特性:查改快增删慢、非线程安全、效率高

容量:ArrayList 的初始容量是(10) ArrayList 存在扩容机制
ArrayList 在1.8之前都是直接创建一个长度10的数组 1.8是等到第一次赋值的时候才会创建

  • 扩容机制:第一次赋值或超过数组容量时才触发扩容 扩容为1.5倍左右
    扩容机制是将原来的数组对象复制到新的数组对象中 新的数组长度是原来的1.5倍

优势:由于底层实现是数组 查找效率为O(1)
因为数组内的对象都是同一类型 所以在查询的时候只需要根据单个对象内存和索引下标就可以找到对应的元素

缺点:增删操作慢 因为增加删除会涉及到元素的复制移动以及数组整体的扩容
另外数组的长度是固定的 相对来说 会造成一定的空间浪费

性能问题:ArrayList 存在两个问题
因为底层的数据结构是数组 数组的长度是固定的 所以在ArrayList数据存储达到最大值时 会触发扩容机制 而扩容机制相对来说比较消耗性能
第二个问题是ArrayList在进行头插入和中间插入时 会将所有受到影响的元素 重新排序 而重新排序相对来说比较消耗性能

解决:针对ArrayList存在的两个问题 也有对应的解决方案
第一个 针对扩容机制 推荐在数组创建的时候 就指定好数组的长度
第二个 针对头插入中间插入需要重新排序 推荐使用尾插入

画图解析:

关系结构:

增删操作:

源码解析:

三个构造函数:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
    // 在JDK8中无参构造 得到的是一个空数组引用 并没创建数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

private static final Object[] EMPTY_ELEMENTDATA = {};
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {// 指定长度大于0 那么创建指定长度的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {// 长度为0 那么也是指向一个预定义的空数组 而不是去创建一个空数组
        this.elementData = EMPTY_ELEMENTDATA;
    } else {// 负数抛出异常
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}

public ArrayList(Collection<!--? extends E--> c) {
    Object[] a = c.toArray();// 将继承了Collection接口的参数 转换成数组
    if ((size = a.length) != 0) {// 先将数组的长度赋值为arraylist的长度 并判断是否不为0
        if (c.getClass() == ArrayList.class) {// 判断继承collection对象的是不是arraylist
            elementData = a;// 如果是直接赋值替换
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);// 如果不是那就将数组的元素复制进arraylist
        }
    } else {// 如果传进来的collection为空 那么也指向预定义的空数组
        elementData = EMPTY_ELEMENTDATA;
    }
}

长度方法:为什么要把这个简单的方法单独提出来呢 因为方便下面的理解
ArrayList中的size并不是数组的长度 他只是一个逻辑长度 只是元素的长度 数组的长度永远都是固定 比如长度为10的数组里面只维护了8个元素 size是8

// 获取数组的长度
public int size() {
    return size;
}

添加方法:

//元素添加方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 判断是否能存储进
    elementData[size++] = e;// 把需要添加的元素放到指定索引位置
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {// 参数minCapacity是当前数组应该有的最小长度
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 判断当前数组对象是否是预定义的空数组
        return Math.max(DEFAULT_CAPACITY, minCapacity);// 返回默认的数组长度10 如果传进来的参数大于默认长度 使用传进来的长度
    }
    return minCapacity;// 如果不是直接返回
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;// 记录修改次数
    if (minCapacity - elementData.length > 0)// 判断数组添加后的最小长度 是否比当前数组长度更长
        grow(minCapacity);// 扩容
}
private void grow(int minCapacity) {// 扩容
    // overflow-conscious code
    int oldCapacity = elementData.length;// 取当前数组的长度
    int newCapacity = oldCapacity + (oldCapacity >> 1);// 取当前数组位移1后的长度 也就是1.5倍
    if (newCapacity - minCapacity < 0)// 如果新长度小于现在长度
        newCapacity = minCapacity;// 那么取现有的长度
    if (newCapacity - MAX_ARRAY_SIZE > 0)// 如果新长度大于数组的最大值
        newCapacity = hugeCapacity(minCapacity);// 大于数组的最大值 那么取Integer.MAX_VALUE
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);// 使用数组的复制
}

//指定索引位置的添加
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++;
}

删除方法:

// 通过索引删除数组元素
public E remove(int index) {
    rangeCheck(index);
    modCount++;// 记录操作次数
    E oldValue = elementData(index);// 记录需要删除的元素
    int numMoved = size - index - 1;
    if (numMoved > 0)// 逻辑长度减索引再减1 如果大于0 说明不是最后一个元素
        System.arraycopy(elementData, index+1, elementData, index, numMoved);// 删除的元素不是最后一位的话 需要移动受影响的元素 从而保证数组的正常访问
    elementData[--size] = null; // clear to let GC do its work  源代码中这里也写了注解 把数组最后一位的引用置空 让GC去回收
    return oldValue;// 返回前面被删除的对象
}

// 通过对象删除数组元素
public boolean remove(Object o) {
    if (o == null) {// 判断需要删除的元素是否为null
        for (int index = 0; index < size; index++)// 如果是null 那就循环数组进行删除
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)// 如果要删除的元素不是null 那么就通过元素的equals方法进行对比 判断是相同的就删除
            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
}

总结一下:

ArrayList 本质上就是一个操作数组的容器 在数组的基础上封装了一系列的方法:扩容、复制、序列化、插入、删除、修改
这里只列举了ArrayList的初始化、添加删除的源码 修改(set)和查询(get)都没有列举 因为逻辑都比较简单没必要在这里浪费时间!

根据上面源码的分析 得出的结论是 当我们需要处理大量数据的时候 频繁插入/删除元素 会使底层数组频繁拷贝 效率不高内存空间浪费
当然 也有例外 如果我们能在一开始就知道 数据的长度 以及只做尾操作 也是能保证ArrayList的时间复杂度为O(1)

posted @ 2021-08-04 15:47  熏晴微穗  阅读(40)  评论(0编辑  收藏  举报