java源码解读——ArrayList

ArrayList的结构

继承:AbstractList

实现:List、RandomAccess、Clonable、Serializable

(文末附集合类的UML类图)

属性: 

    //数组初始容量大小

    private static final int default_capacity=10; 

    //初始化空数组

    private static final Object[] empty_elementdata={};

    //默认共享数组空间

    private static final Object[] defaultcapacity_empty_elementdata={};

    //存储元素的数组缓冲区

   transient Object[] elementData;

    //包含的元素数量

    private int size;
    
    //数组最大长度,为什么要减8呢?数组作为一个对象,需要一定的内存存储对象头信息,对象头信息最大占用内存不可超过8字节
    
    private static final int max_array_size=Integer.MAX_VALUE-8;

构造方法:   

    //默认构造方法,会初始化一个空对象数组

    public ArrayList(){

        this.elementData = defaultcapacity_empty_elementdata;

    }

    //指定对象数组的初始容量,不能为负数

    public ArrayList(int initialCapacity){

        if(initialCapacity>0){

            this.elementData = new Object[initialCapacity];

        }else if(initialCapacity==0){

            this.elementData = empty_elementdata;

        }else{

            throw new IllegalArgumentException("非法容量");

        }

    }

jdk1.8中,ArrayList初始化时,如果没有指定初始容量,那么默认会创建一个空对象数组(Object[]),此时size=0

添加元素add:

1、直接调用add(E e)添加元素到末尾:

1.1、首先调用ensureCapacityInternal(int)方法检测是否需要扩容,该方法接收一个参数(size+1),也就是当前数组长度+1

1.2、如果是第一次添加元素(集合还是空的),那么会默认创建长度为10的对象数组(minCapacity=Math.max(default_capacity,size+1))

1.3、modCount++,修改标识会+1,表示结构被修改的次数

1.4、如果当前所需容量minCapacity超过对象数组的长度,就会调用grow(int)函数进行扩容

1.5、扩容时,newCapacity = oldCapacity + (oldCapacity >> 1);也就是说会扩容到原来长度的1.5倍,然后使用Arrays.copyOf(elementData, newCapacity)进行扩容。【重点】

1.6、完成扩容验证后,调用elementData[size++] = e,添加新元素到对象数组成功

2、添加元素到指定位置add(int index,E e):

2.1、首先通过rangeCheckForAdd(index)方法进行下标校验,如果超过当前size或者为负数,则报IndexOutOfBoundsException错误

2.2、调用ensureCapacityInternal()检测扩容(见1.1~1.5),刚刚2.1已经判断index<size,为什么这里还要判断是否需要扩容呢?上面的判断条件是:index>size || index<0就会报越界错误,转换后就是0<=index=<size不会报错,相信大家看出来了,没错,index=0的时候是不报错的,因为size的初始值就是0,也就是说刚创建ArrayList时,调用add(0,Object)添加第一个元素是可以成功的,那既然如此,就需要进行扩容,因为只要添加了元素,ArrayList就会自动扩容为10。【重点】

2.3、调用System.arraycopy()进行数组拷贝,将index及后面的元素全部后移1位

2.4、elementData[index] = e  将元素e添加到index位置,并且size++,长度自加

为什么1.5使用Arrays.copyOf()拷贝数组,而2.3中使用System.arraycopy()进行拷贝呢?

首先两者的区别是,System.arraycopy调用本地方法(C、C++)对原有数组拷贝,而Arrays.copyOf底层调用了System.arraycopy(),并且数组后面补0,最后返回了一个新数组

【在这里,两个地方使用不同的拷贝方法,是有原因的,但目前我未深入研究】

add扩容的证明:

List<String> list = new ArrayList<String>();
for(int i=0;i<20;i++){
    if(i==10){
        list.add("元素"+i);
    }else{
        list.add("元素"+i);
    }
}

初始化时,arrayList的对象数组elementData为空,size为0:

添加第一个元素时,初始化elementData对象数组的长度为10:

添加第11个元素时,minCapacity(11)>DEFAULT_CAPACITY(10),那么就会进行自动扩容,扩容为原来的1.5倍:

删除元素remove:

1、按下标删除元素remove(int):

1.1、调用rangeCheck(index)判断下标是越界,注意,这里只判断index>=size就抛出异常,没有检测index<0的情况,而上面添加元素时,调用rangeCheckForAdd(index)判断了0<=index<=size,为什么两者不同呢?请看下面的1.3【重点】

1.2、modCount++增加结构修改次数

1.3、E oldValue = elementData(index)在这里将从对象数组里取出下标为index的值,在这里面已经做了index<0的判断,如果index为负数则会报ArrayIndexOutOfBoundsException错误。也就是说如果上面rangeCheck也判断了index<0的情况,那判断了两次index<0,这就造成:方法职责不单一、重复逻辑,而且降低了效率

1.4、还是利用System.arraycopy()将index后面的元素前移一位,直接将index所在元素覆盖,完成元素删除操作

1.5、这时候有同学就会想到,System.arraycopy操作是操作的本数组,那么元素前移后,数组的最后一个元素和倒数第二个元素是相等的!所以此时还需要调用elementData[--size]=null,将最后一个元素置空。这时候同学们又会想到,那arrayList的容量capacity有没有-1呢?答案是没有,在arrayList中完全没有元素后,也就是size=0时,GC会自动对它进行回收【重点】

2、删除对象remove(Object):

2.1、首先判断object是否为null,在list中是允许元素为null的(其实capacity>size的部分,元素值都null),如果为空的话,就循环查找list的第一个null元素,删除并返回true(循环的条件是index<size,所以不会删除capacity>size那部分的null)。但是值得注意的是,这里删除元素没有用remove(index),而是使用一个private的方法fastRemove(index),看名字就知道他的意思是快速删除,那么就会引发一个思考,他快在哪里?

首先fastRemove和remove的区别是少了一个rangeCheck下标检查和E oldValue = elementData(index)旧值的取值,因为fastRemove不用返回删除的对象,而remove(index)要返回删除对象。那难道就因为这两个操作造成它快很多吗?答案是的,他确实比remove快一点,但仅仅是一点,他使用fastRemove的真正原因可能是这里的index是已经确认存在的,也就是index是合法的,不会出现越界的情况。(我自己的猜想)

2.2、如果object不为null,还是会循环下标,然后通过equal()函数去找出object对应的下标,再通过fastRemove删除元素

注意:需要循环删除list元素时,不建议使用remove,因为删除一个元素,其他元素会前移,而循环中的index却+1,导致错过了一个元素,甚至会造成越界,在此强烈建议使用Iterator

获取get:

arrayList内部是使用对象数组进行存储的,所以获取元素就是直接从数组里取出元素

1、调用rangeCheck(index)判断index>=size的情况(这里不使用rangeCheckForAdd的原因和上面一样)

2、return elementData(index)直接返回下标对应的元素

 

总结:

相信大家从上面的增删查就可以看出来,ArrayList适合每次增加、删除元素时都需要复制、移动后面的元素,这个就需要一定的代价,而查询是通过下标直接从数组里面取值,所以ArrayList适合查询,不适合大量的增删,也就是说它不适合结构性的改变

优化:我们知道添加元素时,每次都要去判断是否需要开辟空间,需要的话就调用Arrays.copyOf或 System.arraycopy去扩容,如果大量增加元素,其效率必然降低,所以在预先知道需要多少空间时,最好在实例化ArrayList时就调用构造方法public ArrayList(int)开辟好所需空间

内部类SubList:

SubList是父级list(这里就是arrayList)的一个视图,从fromIndex(包含),到toIndex(不包含)

这里先介绍集合类的结构性变化:

前面一直说modCount,他有什么作用呢?它代表的就是list的结构是否发生改变

非结构性修改:指的是不涉及list大小的修改,也就是modCount没变,举个例子,修改集合中某个元素,这个元素是一个对象,我只修改了对象中的属性age的值,这个对list来说毫无影响

结构性变化:我删除了list的一个元素或者我增加了一个元素,都会修改modCount,而且size也会相应改变,这就是list的结构性变化

修改:1、subList和arrayList的非结构性改变都会影响到彼此,也就是同时改变。2、但是对于结构性变化,subList的操作会映射到父级arrayList中,也就是subList改变——>arrayList跟着改变。而arrayList的改变不会映射到subList中,反而会使subList失效,产生ConcurrentModificationException异常

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值