这次我终于懂JAVA集合框架啦 - 详解List接口(一)

1.List是什么?

        List就是有序队列。

        List是一个接口,继承自Collection。

        其中主要进行实现基本的List接口功能的有AbstractList抽象类,AbstractSequentialList。

        其中AbstractList抽象类继承了AbstractionCollection并进行实现了List接口,AbstractList实现了List接口中除了size(),get(int location)之外的函数。

        AbstractSequentialList抽象类进行继承了AbstractList并且进行实现了链表中根据index操作链表的全部函数。

2.ArrayList

2.1概览

        1.ArrayList支持所有的List操作,继承了AbstractList并进行实现了List。

        2.ArrayList支持快速随机访问,实现了标志性接口RandomAccess,实现这个接口的都标志着可以进行随机快速访问(根据索引访问)。

        3.ArrayList支持浅拷贝,实现了Cloneable接口。

        4.ArrayList支持序列化,实现了Serializable接口。

        5.ArrayList并不是线程安全的。

2.2ArrayList的数据结构

        ArrayList底层通过一个动态的Object数组进行存储数据,默认ArrayList的动态数组长度是10,ArrayList存储长度的size。

private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

transient Object[] elementData; // non-private to simplify nested class access

private int size;

2.3ArrayList的序列化

2.3.1不采用默认JDK序列化方式

        ArrayList采用了一种十分巧妙的序列化方式,在JAVA中只有实现Serializable接口的类才可以被序列化,其中实现之后默认的序列化方式是JDK底层默认的二进制序列化方式,其中这种序列化方式进行使用的时候会将类中没有transient关键字标注的字段名全都序列化。

        可以看到ArrayList中的动态数组被transient进行标注了,代表不能被JDK底层默认的序列化方式进行序列化,必须进行自己编写序列化这个字段的逻辑,或者不进行去序列化这个字段。

transient Object[] elementData;

2.3.2ArrayList的序列化方式

        由于ArrayList底层是一个动态扩容的数组进行实现的,数组中所有位置不一定都被存储上数据了,所以如果贸然将整个数组进行序列化,就会特别浪费空间,所以ArrayList底层重写了序列化方式,仅仅对数组中已经有数据的部分进行序列化。

        ArrayList重写了Serializable接口中的readObject和writeObject方法。

        这段代码十分好读,writeObject是序列化的过程,先进行调用默认的序列化的方法,将对象中没有被transient进行标注的字段进行序列化,再进行将size进行读取到里面去,然后循环写入elementData中的数据,只遍历索引0-(size - 1)中的数据(也就是动态数组中有数据的方式)。

        readObject是反序列化的过程,先给elementData赋值控数组,先进行默认的反序列化过程,将对象中没有被transient进行标注1的字段进行反序列化,再进行反序列化,也是仅仅是反序列化给数组中有的数据。

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

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

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

2.4构造方法

        ArrayList中有三个构造方法,一个无参构造器,一个接收一个initialCapactity的构造函数(初始化ArrayList中动态数组的长度),一个接收一个集合,进行使用集合初始化。

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

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

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

2.4.1无参构造器

        DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个用于ArrayList底层初始化的空数组,长度为0,是一个类常量字段。当进行使用无参构造器创建ArrayList对象的时候,就会执行此方法,更改引用指向。

2.4.2疑问:为什么所有通过无参构造器新建ArrayList对象都是指向一个初始化数组,不会共享引用出错吗?

        不会的,因为在添加数据的时候,会进行判断是不是第一个添加数据并且elementData是共享的默认空数组,如果是就会进行新建一个新的初始化默认长度数组,不会进行使用这个初始化空数组,导致出现引用错误。

2.4.3指定initialCapacity初始化ArrayList

        如若initialCapactiy > 0则使用intialCapacity长度进行初始化一个Object数组。

        如若等于0进行使用EMPTY_ELEMENTDATA这个数组进行初始化Object数组。

        如果 < 0就直接抛出错误。

2.4.4使用集合作为参数初始化ArrayList

        会先将这个集合转换为一个数组,先进行判断集合是不是属于ArrayList类型,如果是就直接将转换为数组的集合赋值给elementData,如果不是ArrayList类型,就调用Array.copyOf将a中的数据赋值给elementData。如果a中的长度=0,就直接将EMPTY_ELEMENTDATA赋值给elementData。

2.5ArrayList访问数据

        ArrayList实现了RandomAccess接口,所以支持使用索引进行快速访问。

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

    return elementData(index);
}

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

        这是rangeCheck的源码,主要是判断index是否大于size了,如果大于了,直接进行抛出IndexOutOfBoundsException错误。为什么需要这样呢?因为主要是害怕动态数组的长度确实>index,数组访问支持,但是对于ArrayList集合就出错了,为了防止开发者进行访问越出集合有效数据边界的数据,进行以下校验。

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

2.6ArrayList添加数据

        添加数据分为两种,一种是进行在ArrayList尾部进行添加数据。

        一种是在ArrayList的任意位置进行添加数据。

2.6.1add函数的基本分析

        add(E e)默认是将数据插入到动态数组的末尾,这段代码非常简单,就是先调用ensureCapacity看一下动态数组是否需要进行扩容,然后将element[size++] = e进行赋值即可。

        add(int index, E element)就是先调用rangeCheckForAdd进行检查这个index正常吗,然后再进行调用ensureCapacityInternal看一下动态数组是否需要进行扩容,然后使用System.arraycopy配合elementData[index] = element实现数据的插入。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

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

        这个校验也很有意思,进行插入的时候索引为负或者索引>size也会报错,这个主要是为了确保ensureCapacity代码正常执行,做校验的。

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

2.6.2两种添加元素的不同点

        1.添加元素到任意位置的时候,会导致在该位置后的所有元素都需要进行重新排列。

        2.添加元素到数组末尾,在没有发生扩容的情况下,是不会出现元素的赋值排序过程的。所以向ArrayList后面进行添加元素的时候只要不出现扩容的情况,性能是十分优秀的,但是如果发生扩容现象,性能就不咋地了,会发生数组的复制扩容现象。

2.6.3两种添加元素的相同点

        添加元素时,都会调用ensureCapacity进行检查容量的大小,如果发现容量不足,会自动扩容为原来数组容量的1.5倍。

2.6.4ensureCapacityInternal方法探究

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

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

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

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

        我现在用较为通俗的语言进行介绍一下。

        首先ensureCapacity进行接收一个minCapacity最小容量值(size+1)=> 其实就是当前动态数组中已经有的数据的数目。

        先调用calculateCapacity(elementData, minCapacity),该函数进行接收原始elementData数据,minCapacity最小接收容量,在这个函数中先进性判断一下elementData是不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,其实就是判定这个elementData是不是初始化时的空数组,返回一个Math.max(DEFAULT_CAPACITY, minCapacity),判定是返回默认长度还是minCapacity,如果elementData已经不是初始化的默认空数组了,就直接进行返回minCapacity。

        然后调用ensureExplicitCapacity,接收的参数是minCapacity,也就是计算出的最小所需容量,由于结构发生了修改,先进行将modCount进行+1(这个变量主要是进行记录ArrayList对象的结构变化次数),然后最重要的语句来了,先进行判断minCapaciity是否大于动态数组的长度,如果大于,就直接调用grow,动态数组扩容函数,接收的参数是minCapacity。

        grow函数接收minCapacity,进行扩容。先使用oldCapacity进行接收elementData的长度,然后使用newCapacity进行接收扩容后数组的长度 => oldCapacity + (oldCapacity >> 1),就是扩容为oldCapacity的1.5倍。

        然后我们看到newCapacity - minCapacity < 0,这句话其实就是为了处理如果扩容后,长度还是装不下这些数据,那就直接将newCapacity赋值为minCapacity即可,需要多少我给你多少。

        为什么会出现扩容后长度还是不够的情况呢,其实这段代码的主要作用就是防止溢出。

        看下面图,如果发生了移除,就会这样进行循环,很有可能newCapacity算出来是一个负数。

        newCapacity - MAX_ARRAY_SIZE > 0主要是进行判断计算出来的值是否大于MAX_ARRAY_SIZE,如果大于了,就将minCapacity传入到hugeCapacity函数中去,进行重新计算newCapacity。

        这是MAX_ARRAY_SIZE的定义。

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

        hugeCapacity接收到minCapacity后,如果发现minCapacity小于0,直接抛出OutOfMemoryError错误。

        返回值主要是用双目运算符,进行判断minCapacity和MAX_ARRAY_SIZE的大小,如果minCapacity大,就赋值为Integer.MAX_VALUE,如果小就赋值为MAX_ARRAY_SIZE。

        最后使用Arrays.copyOf(elementData, newCapacity)进行赋值给elementData,自此,扩容结束。

        画一下扩容的整体流程。

2.7ArrayList删除数据

        删除数据的代码也很有意思,新增了结构改变的变量值,然后获取到了老elementData的数据,又进行获取到了需要移动的数据的值,我们看一下为什么要移动这么多变量。

        如果移动的变量>0,就进行使用System.arraycopy进行移动数组。

        然后将elementData[--size] = null => 因为移动数组的本质是一个一个向前复制占位,最后一个数据还是存在,需要置空。

public E remove(int 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;
}

2.8ArrayList的Fail-Fast机制

        前面我们讲了很多源码相关的知识,你会发现在新增/删除的时候都会出现modCount这个结构变化次数变量,那这个变量到底是干什么的呢?

2.8.1modCount到底是什么?

        结构变化次数变量,字面意思其实就是修改结构的次数的变量,结构变化是指什么呢?

        新增/删除元素,动态数组扩容,这些都是进行结构变化。

        但是更新元素的数据,这种修改元素内部的行为都不是结构变化。

        结构变化就是动态数组中元素个数/动态数组的长度的变化,这些才是结构变化,元素内部的变化不属于结构变化。

2.8.2从序列化的角度理解Fail-Fast机制

        这是ArrayList中序列化的代码,我们将最后几段关于modCount的代码提取出来。

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

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

        你会发现,他对比modCount是否和被期待的modCount相等,如果不相等就抛出ConcurrentModificationException错误。

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

        就是你在序列化/反序列化过程中就进行发现错误了,你结构和被期待的结构不一致,我就拦截你,以最快的速度发现错误,不会给你一错千里的机会,这就是Fail-Fast机制,可以防止黑客进行偷袭程序等,有效防止黑客控制ObjectOutputStream后进行错误的操作,提前发现错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值