集合源码学习-ArrayList

基础知识:

	1,ArrayList 是 java 集合框架中比较常用的数据结构,**继承自 AbstractList,实现了 List 接口**。
    2,底层采用数组实现。
	3,ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
	特点:
		1,值可重复;
		2,插入元素有序;
		3,允许存在null值;
		4,查询效率高。

源码分析

初始化时定义的部分参数:

//初始化默认集合个数
private static final int DEFAULT_CAPACITY = 10;
//集合元素个数
private int size;

add():(初始化)

public boolean add(E e) {
	//新创建的ArrayList在添加第一个值时,会先进行数组初始化操作,初始化完成后,会将第一个值添加到数组。
    //此时size为0,所以size+1 = 1;
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    //初始化时elementData集合为空,minCapacity =1;
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //此时elementData为空,minCapacity =1,下列if判断成立
    //DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为新建的数组,为空
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //此时返回最大的数;
            //DEFAULT_CAPACITY 为默认值10 ,minCapacity =1;
            //最终返回10
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
private void ensureCapacityInternal(int minCapacity) {
    //此时 calculateCapacity(elementData, minCapacity) 这个方法最后返回的值为10
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
    //add操作次数+1
    modCount++;
   //此时 minCapacity =10 ,elementData.length =0,下列if判断成立
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
   //oldCapacity = 0
    int oldCapacity = elementData.length;
    // 右移1位 4 >> 1 = 4/2 = 2
    // 左移1位 4 << 1 = 4*2 = 8
    //newCapacity = oldCapacity + oldCapacity/2
    //此时 newCapacity  = 0,minCapacity = 10
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
       //此时 newCapacity = 10
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //数组拷贝,此处为数组的初始化,新数组容量为10
    elementData = Arrays.copyOf(elementData, newCapacity);
}
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    // 数组初始化完成后,此时数组长度为10,开始将第一个值插入
    //此时size=0,所以 elementData[0] =e, size++
    elementData[size++] = e;
    //此时ArrayList添加第一个值的操作完成。
    return true;
}

add() (扩容)

public boolean add(E e) {
    //此时,假设size长度为10,size+1 = 11;
    ensureCapacityInternal(size + 1);  
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    //elementData.length = 10,minCapacity =11
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
  	//此处elementData不为空,if条件判断不成立,返回minCapacity =11
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
	//此时 minCapacity =11,elementData.length = 10,条件成立
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
    //此时minCapacity = 11
    //oldCapacity = 10
    int oldCapacity = elementData.length;
    //newCapacity = oldCapacity + oldCapacity/2 = 15
    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);
}

get(index):

public E get(int index) {
    //判断是否越界
    rangeCheck(index);
	//然后返回下标
    return elementData(index);
}
private void rangeCheck(int index) {
    if (index >= size)
    //如果越界抛出异常
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

remove(index):

public E remove(int index) {
    //判断是否越界
    rangeCheck(index);
    //删除次数+1
    modCount++;
    //获取到要删除下标的值
    E oldValue = elementData(index);
    //计算要移动的个数(比如有5个元素,要删除第4个,则 numMoved = 5 - 3 -1 = 1,要移动一格 )
    int numMoved = size - index - 1if (numMoved > 0)
        //此次为浅拷贝,将index+1的值拷贝到index上,此时,原先最后一个元素依然存在,数组只做了单纯的复制操作
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //此处将最后一个值置位null,下次垃圾回收时将会被回收
    // clear to let GC do its work
    elementData[--size] = null; 
	//返回删除的值
    return oldValue;
}

ArrayList的一个坑

边遍历边删除元素(错误)

 public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("2");
        list.add("3");
        // for循环遍历删除
        for (int i =0;i<list.size();i++){
            if("2".equals(list.get(i))){
                list.remove(i);
            }
        }
        System.out.println(list);
    }
    //打印结果
    1
    2
    3

第二个2没有被删除,原因是for循环时,指针会向下走,而ArrayList删除元素后,之后元素位置会向前移动,结果会导致部分元素无法被删除。
解决方案:

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("2");
        list.add("3");
        for (int i =0;i<list.size();i++){
            if("2".equals(list.get(i))){
                list.remove(i);
                //每次删除后,将指针向前拨一位即可解决问题
                i--;
            }
        }
        System.out.println(list);
    }
    打印结果:
    1
    3

第二种方式为迭代器遍历,这种方式比较推荐:

   ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("2");
        list.add("3");
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            if ("2".equals(iterator.next())){
                iterator.remove();
            }
        }
        System.out.println(list);

思考题:

1,ArrayList增删是怎么做的?为什么会慢?

答:在新增时需要先进行数组长度的校验,如果长度不够,会进行扩容。扩容时才用了位运算,右移一位。

​ 扩容完成后,会进行数组的拷贝。如果数组大的话,拷贝的速度是很慢的。

​ 所以慢的原因主要有两点:1,数组扩容;2,数组拷贝;

2,ArralList(int initialCaacity)会不会初始化数组大小?

答:会初始化数组大小,但是List大小没有变化,因为List的大小是返回size的,只有当执行add()方法的时候才会改变List的大小。

ArrayList<String> list = new ArrayList<>(10);
System.out.println(list.size());
list.add("q");
System.out.println(list.size());

//打印
0
1
Process finished with exit code 0

还有一个点:

如果只是初始化了数组,没有改变list的大小,那么此时执行list,set()方法会抛出数组越界异常,因为list.size()=0.

ArrayList<String> list = new ArrayList<>(10);
System.out.println(list.size());
list.set(1,"1");

//打印
0
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 1, Size: 0
3,ArrayList增删速度一定慢吗?

不一定,增删速度主要取决于增删元素与数组末尾的位置,位置越近,速度越快。

如果元素增删是在数组最后一个元素,此时不涉及到其它元素的拷贝,那么速度就很快。

如果元素增删是在头部,那么要拷贝的元素会很多,速度就会变慢,特别是数组特别大的时候。

4,ArrayList的遍历和LinkedList的遍历性能比较?

答:ArrayList的遍历速度要快于LinkedList的遍历速度。

​ 主要原因是ArrayList内存的连续性。它存储时占用一片连续的存储空间,可以大幅度降低读取内存的性能开销。

5,ArrayList如何优化?

答:性能损耗的点主要有两方面:1,数组扩容;2,数组拷贝;

​ 优化时可以从这两方面入手。扩容方面,尽量给一个合理的初始化值,减少扩容次数。数组拷贝方面:可以重写add方法,使用尾插的方式,减少数组拷贝次数。


版权声明:本文为m0_37688047原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。