java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java ArrayList

关于Java中ArrayList的源码分析

作者:冷玩

这篇文章主要从源码角度带大家深入了解一下Java中ArrayList的构造方法和属性等知识,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下

ArrayList分析

    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

ArrayList里常用6个属性:

四个常量属性:

serialVersionUID:是用于在序列化和反序列化过程中进行核验的一个版本号。

DEFAULT_CAPACITY:默认的初始化容量10

EMPTY_ELEMENTDATA:空实例时的数组实例

DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默认大小的空实例时的数组实例

前两个解释的比较抽象

elementData:Object数组,集合真正用来存数据的容器。

size:集合大小

关于serialVersionUID可以参考文末补充内容

ArrayList的三个构造器(构造方法)

public ArrayList() //空参
public ArrayList(int initialCapacity) //带有初始化容量
public ArrayList(Collection<? extends E> c)//以一个集合来初始化

因为ArrayList集合底层就是采用elementData[]数组来存储数据,在创建集合时:

1、使用空参构造器时。

只是将DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个空数组赋值给了elementData数组来完成集合的初始化,因为DEFAULTCAPACITY_EMPTY_ELEMENTDATA是提前创建好的,所以在创建集合时的操作只是一个赋值操作,简化了创建数组的时间。

transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

2、使用有参(initialCapacity)构造方法时。不废话,先看源码:

public ArrayList( @Range(from = 0, to = java.lang.Integer.MAX_VALUE) 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);
        }
}

他会首先判断传进来的initialCapacity是否是大于0的:

如果不大于0他执行的操作和使用空参构造器执行的操作类似,只是这次赋值给elementData数组的是EMPTY_ELEMENTDATA数组,此次空数组赋值竟然和上次空参不一样,这是为什么呢?为什么要创建出两个空数组呢?暂且按下不表

如果大于0,他会新创建一个initialCapacity大小的数组赋值给elementData。至于传进来的是一个小于0的数,当然结果就是死路一条。

3、构造方法参数是集合时。

public ArrayList(Collection<? extends E> c) {
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
    }
}

他会首先将传进来的集合转化为一个Object数组,然后判断该数组是否为空。

如果为空,他仍会执行给elementData数组赋值EMPTY_ELEMENTDATA数组的操作,到这有没有发现奇妙的一点?无论集合第一次创建时采用的哪一个构造方法,如果传进来的是一个“空内容”,他都会使用底层已经创建好的数组来完成初始化

如果不为空,判断他们两个是否是同一个集合。如果是,将a数组直接赋值给elementData。如果不是,将a数组复制到elementData

问题

为什么ArrayList源码里要定义两个空数组呢?直接定义一个岂不是更节省空间?

可以看一下源码中对DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的注释:

We distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when first element is added.

大致意思就是:是为了在第一次添加元素时判断去给数组inflate(扩容)多少。 这就涉及到了集合在第一次添加元素时的操作和集合的扩容。

以下时第一次添加时的源码。

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 void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

在第一次添加元素时会先调用ensureCapacityInternal方法,同时将添加一个元素后的集合大小size传参过去,在ensureCapacityInternal方法中调用ensureExplicitCapacity()方法,在执行此方法之前,会先调用calculateCapacity方法。如果此时elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA相等,也就是采用的空参构造器的初始化的第一次添加,则返回默认容量和当前容量的较大者,当然第一次添加肯定是返回默认容量(DEFAULT_CAPACITY)。最后进入ensureExplicitCapacity方法,根据minCapacity - elementData.length > 0 判断容量是否足够,然而判断是否要执行扩容方法grow,;

走到这就会发现,ArrayList的初始化容量不会在创建集合时进行,而会在第一次添加元素时进行。而且只有采用的是空参构造方法时,才会在第一次添加元素时将最小容量设置为minCapacity = DEFAULT_CAPACITY = 10。

这还不是最终的扩容,最终的扩容在grow方法中:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);//让newCapacity等于原来容量的1.5倍
    if (newCapacity - minCapacity < 0)                 //如果newCapacity比minCapacity小
        newCapacity = minCapacity;                     //让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);
}

建议

因为ArrayList每扩容一次都要将原数组数据复制到新数组,所以建议给定一个预估计的初始化容量,减少数组扩容的次数,这是ArrayList集合比较重要的优化策略。

知识补充

serialVersionUID 是干什么的?

我们有时候在写代码的时候,对于一个需要序列化的类,如果不去写 serialVersionUID,编译器可能就会提示我们 The serializable class ClassName does not declare a static final serialVersionUID field of type long

有使用过 MyBatis-plus 框架的同学应该也发现,在使用反向代码生成时,所生成的实体类也都带有 static final 进行修饰的 long 类型 serialVersionUID 。那么到底 serialVersionUID 是什么呢?有什么用?

附官方文档连接:https://docs.oracle.com/javase/1.5.0/docs/api/java/io/Serializable.html

它是什么?

简单概括而言, serialVersionUID 是用于在序列化和反序列化过程中进行核验的一个版本号。

序列化运行时将一个版本号(称为serialVersionUID)与每个可序列化类相关联,该版本号在反序列化期间用于验证序列化对象的发送方和接收方是否为该对象加载了与序列化兼容的类。

如果接收方为对象加载的类与相应发送方类的serialVersionId不同,则反序列化将导致InvalidClassException

可序列化类可以通过声明名为 serialVersionUID 的字段显式声明自己的 serialVersionUID,且该字段必须是staticfinal的且类型为long

ANY-ACCESS-MODIFIER static final long serialVersionUID=42L;

不声明会怎样?

如Java(TM)对象序列化规范中所讲述的,如果可序列化类没有显式声明serialVersionUID,则序列化运行时将根据类的各个方面计算该类的默认serialVersionUID值。

但是,强烈建议所有可序列化类显式声明serialVersionUID值,因为默认的 serialVersionUID 计算对类详细信息高度敏感,这些详细信息可能因编译器实现而异,因此在反序列化过程中可能会导致意外的InvalidClassExceptions

因此,为了保证在不同的java编译器实现中SerialVersionId值是一致的,可序列化类必须声明一个显式的SerialVersionId值。还强烈建议显式 serialVersionUID 声明尽可能使用 private 修饰符,因为此类声明仅适用于立即声明的类——serialVersionUID字段不可用作继承成员。

其他问题

Q: 如果父类被序列化,默认情况下子类也被序列化,所以我们也需要为 child 声明 serialVersionUID 吗?

A: 建议对子类,或者说每一个存在序列化需求的类都进行 serialVersionUID 的指定,并且如上建议,采用 private 进行修饰,避免子类对父类的 protected 继承(我还没碰上炸毛的情况,所以也不好讲继承后会在什么情况下出现什么样的问题)

Q: 如果我不序列化,还需要指定吗?

A:如果不存在序列化需求,也就不存在序列化与反序列化中的比对,原则上不声明 serialVersionUID 也是可以的

到此这篇关于关于Java中ArrayList的源码分析的文章就介绍到这了,更多相关Java ArrayList内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文