ArrayList 的扩容问题和序列化问题

作者: Arvin Chen 分类: Java 来源: Break易站(www.breakyizhan.com)

一、前言

这里主要研究到以下问题,通过源码阅读分析探索以下问题的答案。本文不牵涉到更多问题,所以源码只贴出与这些问题直接联系的关键代码块。当然源码中必要的全局常量、方法会贴出。

  • ArrayList 的继承与实现关系;
  • ArrayList 关于数组和集合的讨论;
  • ArrayList 初始容量是0还是10问题的确认;
  • ArrayList 的扩容问题探索;
  • ArrayList 的序列化问题补充;

这里可以放出结论:ArrayList的扩容机制主要是通过计算初始容量,并用位移算法将容量扩容为原来的1.5倍。然后将原来的数组复制到扩容后的数组里面。

二、ArrayList 的继承与实现关系

2.1 ArrayList.java

ArrayList类通过extends关键字继承AbstractList抽象类,通过关键字implements实现List集合接口、RandomAccess标记接口、Cloneable克隆接口、Serializable序列化接口。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}

图2-1、ArrayList 继承与实现关系图:

2.2 抽象类AbstractList.java

抽象类AbstractList继承一个AbstractCollection集合同样实现了集合List接口。

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {}

2.3 接口List.java

List直接继承于底层集合Collection,List是一个集合,谁赞成?谁反对?

public interface List<E> extends Collection<E> {}

2.4 接口RandomAccess.java

此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。
接口RandomAccess 是一个标记接口,实现该接口的集合List 支持快速随机访问。List 集合尽量要实现RandomAccess 接口,如果集合类是RandomAccess 的实现,则尽量用for(int i = 0; i < size; i++) 来遍历效率高,而不要用Iterator迭代器来遍历(如果List是Sequence List,则最好用迭代器来进行迭代)。

2.5 接口Cloneable

实现接口的目的是重写java.lang.Object的clone()的方法,实现浅拷贝。深拷贝和浅拷贝针对像 Object, Array 这样的复杂对象的。浅拷贝只复制一层对象的属性,而深拷贝则递归复制了所有层级。

  • 浅拷贝
    被复制(拷贝)对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。浅拷贝即新建一个对象,复制原对象的基本属性,一级属性到新的存储空间,不拷贝原对象的对象引用元素,新对象的对象引用指向原来的存储空间,修改对象引用的元素,那么拷贝对象和原对象都会变化。
  • 深拷贝
    深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。深拷贝新建一个对象,不仅拷贝对象,还拷贝对象引用;深拷贝就是我们平常理解的复制,将对象的全部属性及对象引用复制到新的存储空间,不会指向原来的对象,修改新对象的任意元素都不会影响原对象。

2.6 接口Serializable

该接口无继承实现关系,实现该接口的类支持序列化。因此ArrayList 支持序列化。

  • 序列化:可以将一个对象的状态写入一个Byte 流里;
  • 反序列化:可以从其它地方把该Byte 流里的数据读出来。

三、ArrayList 关于数组和集合的讨论

3.1 ArrayList 是数组还是集合问题说明

ArrayList 是数组还是集合,这也算问题?
我们都知道List 是集合啊,ArrayList 继承于List 也是集合。不过你或许会在某些文章上见过ArrayList 是数组或者说ArrayList 是基于数组的说法。

3.2 从构造方法分析ArrayList

那我们首先看一下ArrayList 的构造方法;

    // 默认空数组,final 关键字修饰
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 空数据的共享空数组实例
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 存储ArrayList元素的数组缓冲区,即ArrayList 存放数据的地方
     * ArrayList的容量是这个数组缓冲区的长度
     * transient 关键字修饰,elementData 不支持序列化
     */
    transient Object[] elementData;

    /**
     * 默认无参构造方法
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 带整型参数的构造方法
     * @param 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);
        }
    }

    /**
     * 泛型集合参数构造方法
     * @param c 集合类型参数
     */
    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;
        }
    }

无参构造方法中直接初始化Object[] elementData = {}; 即ArrayList 的存储数据的容器是一个数组。带参构造方法也是new Object() 或者通过Arrays 的方法转换为数组对象。
ArrayList 实现了List 接口,重写了集合的add(),size(),get(),remove(),toArray()等方法,多个方法的内部代码块是基于数组来处理数据的。

3.1 确认ArrayList 是集合

因此ArrayList 是实现List 接口的集合,是基于数组的集合,数据的存储容器是数组,集合方法是通过数组实现的(比如泛型参数构造方法是将传入的集合c 先转化为数组在进行处理的)。包括其内部类Itr implements Iterator 中重新Iterator 的方法也是基于数组计算的。
搞这个问题有意义吗?

四、ArrayList 初始容量是0 还是10 问题的确认

4.1 从构造方法看初始容量

从第三部分中的构造方法可以看出
无参构造一个ArrayList 时存储数据的容器elementData = {};此时存储容器大小为0 ;
带整型参数的构造方法通过传入的整型数据的大小来确认初始化存储容器elementData 的大小,当initialCapacity == 0 时,还是赋值elementData = {};
泛型集合参数构造方法,根据集合的大小来初始化elementData 的大小,将集合转化为数组,数组的大小为0 的情况下,仍然赋值elementData = {};

4.2 从add() 方法看初始容量

这个初始化和10 又有什么关系???
在ArrayList 中定义了一个默认的存储容器的大小DEFAULT_CAPACITY 为10,用关键字final 修饰,注释是默认初始容器大小,通过构造方法创建ArrayList 对象并没有使用到这个常量,我们看看这个初始容器大小是怎么初始化容器大小的。

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;
    // 集合的逻辑大小,即存储真实数据的数量
    private int size;
    public int size() {return size;}
    public boolean isEmpty() {return size == 0;}
    /**
     * 添加元素
     * @param e
     * @return
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    /**
     * 确认集合内部容量大小
     * @param minCapacity
     */
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    /**
     * 计算集合的容量
     * @param elementData
     * @param minCapacity
     * @return
     */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

在集合add() 添加元素时,会将当前的size + 1 传入ensureCapacityInternal() 方法确认当前elementData 数组大小是否足够
足够的话size自增一,size = size + 1直接添加的元素赋值给elementData[size];
不足够的话进行扩容,扩容问题下面涉及,这里说扩容中特殊情况,对空集合的扩容,比如我们通过无参构造方法创建了集合对象,此时容器大小为0,然后调用add() 方法添加一个元素,此时elementData == {}即此时elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,满足该条件在计算集合的容量方法calculateCapacity 中会进行”容器初始化”,其实是扩容而已;

这里的”=” 是等于不是赋值
此时return Math.max(DEFAULT_CAPACITY, minCapacity);
minCapacity = size + 1 = 0 + 1 = 1
DEFAULT_CAPACITY = 10
minCapacity < DEFAULT_CAPACITY = 1 < 10
结果return 10;

此时容器elementData 扩容为Object[10]

4.3 确定ArrayList 的初始容量

从以上两方面分析,所以ArrayList 的初始容量根据传参确定,默认无参构造方法下新对象的容器初始大小为0。而10 是在空集合添加第一个元素时扩容时的默认容器大小。

五、ArrayList 的扩容问题探索

5.1 扩容问题说明

集合扩容就是集合容量大小不能满足需要存储数据的数量,而需要将elementData 容器大小增大,以存储更多的元素。

5.2 通过add() 方法探索扩容问题

集合存储容器elementData 的容量大小不小于真实存储元素数量size

elementData.length > size 为真true
elementData.length = size 为真true
elementData.length < size 为假false

集合在添加元素时会首先判断当前容器是否能装下第size + 1 个元素。不能的情况下会进行扩容,上面初始容量问题中谈到当空集合扩容时会给该集合对象一个默认的容器大小10,即扩容到elementData.length == 10
这是一种特殊情况,给了一个默认值,并没有真正涉及扩容核心算法。
下面看看ArrayList 是如何扩容的。

    // 集合最大容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    /**
     * 添加元素
     * @param e
     * @return
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    /**
     * 确认集合内部容量大小
     * @param minCapacity 添加元素后容器的最小容量
     */
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    /**
     * 计算集合的容量
     * @param elementData 存储数据的容器
     * @param minCapacity 添加元素后容器的最小容量
     * @return
     */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        return minCapacity;
    }
    /**
     * 确认明确的容量大小
     * @param minCapacity 添加元素后容器的最小容量
     */
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    /**
     * 扩容方法
     * @param minCapacity 添加元素后容器的最小容量
     */
    private void grow(int minCapacity) {
        // 扩容前容器大小
        int oldCapacity = elementData.length;
        // 扩容关键算法,newCapacity 扩容后容器大小
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);
        // 将扩容后的容器赋值给存储容器
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    /**
    * 溢出处理
    */
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) throw new OutOfMemoryError();
        // 超过最大值不合法,直接将容量大小定义为Intager 的最大值
        return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
    }

在添加元素的方法中依此调用容器大小判断相关的方法,当容器大小不够时,会进行扩容,调用grow() 方法进行扩容。扩容方法很简单,拿到扩容前容器大小oldCapacity,进行扩容,判断扩容后容量是否合法,是否溢出,然后进行处理为合理的大小。

5.3 扩容算法

扩容算法是首先获取到扩容前容器的大小。然后通过oldCapacity + (oldCapacity >> 1) 来计算扩容后的容器大小newCapacity。
这里的扩容算法用到了>> 右移运算。即将十进制转换为二进制,每一位右移后得到的结果。oldCapacity >> 1即oldCapacity 对2 求摩,oldCapacity/2;

oldCapacity + (oldCapacity >> 1)即oldCapacity + (oldCapacity / 2)

所以关键扩容算法就是当容量不够存储元素时,在原容器大小size 基础上再扩充size 的接近一半,即大约扩充原容器的一半。
相对直白的严谨的扩容算法如下:

扩容后容器大小newCapacity = size + size / 2

5.4 模拟扩容演示

举个栗子:原容器是10,elementData 已经存储10 个元素了,再次调用add() 方法会走grow() 方法进行扩容。运行中截图如下图

10 / 2 =5
新的容器大小为 10 + 5 = 15
另:运算 “/” 的结果是整数,15/2 =7;9/2 = 4; 8/2 = 4;

图5-1、扩容前容量大小图:

图5-2、扩容后容量大小图:

图5-3、扩容后elementData 容量大小图:

六、ArrayList 的序列化问题补充

集合的存储容器elementData 使用transient 关键字修饰不支持序列化,但是我们知道ArrayList 是支持序列化的,那我们是怎么序列化集合中的数据呢,这里不直接序列化elementData,而是遍历每个数据分别进行IO 流处理来实现存储容器中对象的序列化的。

    // ArrayList 列表结构被修改的次数。    
    protected transient int modCount = 0;

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // ArrayList 列表结构被修改的次数。
        int expectedModCount = modCount;
        s.defaultWriteObject();
        s.writeInt(size);
        // 对每一个对象进行IO 流的写处理
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

这里对存储容器Object[] elementData 用transient 关键字修饰,考虑到容器的存储空间在扩容后会产生很大闲置空间,扩容前容量越大这个问题越明显;序列化时会将空的对象空间也进行序列化,而真实存储的元素的数量为size,那样处理的话效率很低,所以这里不支持存储容器直接序列化,而写一个新的方法来只序列化size 个真实元素即可。

参考:
https://juejin.im/post/5b76357df265da27fd70c3ce

  •   本文标题:ArrayList 的扩容问题和序列化问题 - Break易站
    转载请保留页面地址:https://www.breakyizhan.com/java/9678.html
      微信返利机器人
      免费:淘宝,京东,拼多多优惠券
      腾讯,爱奇艺,优酷的VIP视频免费解析,免费看
      即刻扫描二维码,添加微信机器人!

    发表笔记

    电子邮件地址不会被公开。 必填项已用*标注