ArrayList8源码分析

·  阅读 218

ArrayList 8 源码分析

1、简介

- 集合的底层是Object[] 的数组,内部实现动态扩容
- 在内存中是连续的内存空间、并且按照顺序进行存储的,所以在查找某个位置数据是很快的,往中间插入数据是很慢的。
- 允许有空值存在,值也是完全可以重复的,但是必须是同一类型的值。
- 初始化大小为10,但后续扩容时,涉及的数组的复制,所以对系能是一种消耗,如果已知数据量多大的情况下,最好指定集合的大小。
复制代码

2、实现的接口

(1)RandomAcess接口

允许通用算法在应用于随机或顺序访问列表时改变其行为以提供良好的性能。
复制代码

(2)Cloneable接口

集合可以实现接口和数据的克隆。
复制代码

(3)Serializable接口

集合支持被序列化,可以用于网络传输。
复制代码

3、属性

1、private static final int DEFAULT_CAPACITY = 10; //表示集合数组的默认大小为10

2、private static final Object[] EMPTY_ELEMENTDATA = {}; // 空数组,当设置大小为0时用

3、private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //默认空参构造函数使用

4、transient Object[] elementData;数组的值

5、private int size; 当前数组的大小,也就是数组里包含的元素的个数
复制代码

4、构造方法

(1)无参构造方法

    public ArrayList() {
   	 	// 就相当于 elementData = { } 
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
复制代码

(2)指定初始化大小的构造方法

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) { 
        	// 当初始化大小大于0,则直接赋值
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
        	//当初始化大小等于0时, == { }
            this.elementData = EMPTY_ELEMENTDATA;
        } else { //否则参数错误
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
复制代码

(3)引入另一个集合的构造方法

    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;
        }
    }
复制代码

5、执行添加操作

(1)普通添加数据

    public boolean add(E e) {
        //先对当前数组的大小 +1,然后进行判断是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //在数组最末位置填充值
        elementData[size++] = e;
        return true;
    }
复制代码

(2)向指定位置添加数据

    public void add(int index, E element) {
    	 //这个方法用来判断添加的位置是否越界(合理的位置应该:< size、>0)
        rangeCheckForAdd(index);
        
        // 先对当前数组的大小 +1,然后进行判断是否需要扩容
        ensureCapacityInternal(size + 1);  
        
        // //将index位置之后的数据往后移
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //移动完之后 index还是老的值,然后新值直接覆盖                  
        elementData[index] = element;
        
        //长度加1
        size++;
    }
复制代码

在这里插入图片描述

(3)扩容操作

    //方法1:扩容入口
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    //查看当前的数组大小,如果第一次存,则直接返回初始值10
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //判断当前是否是一个空的集合,第一次数组里还啥也没有,就将默认的10和当前传过来的数(当前数据长度 size + 1)
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //第一次都返回10
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //如果当前数组不是空的,直接返回当前数组长度 + 1
        return minCapacity;
    }
    //方法2:
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++; //对数据的操作次数 + 1
        // size 加1 之后的值与数组总长度进行比较,如果加1之后的数组的个数大于总长度,则需要扩容 
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    //方法3:扩容
    private void grow(int minCapacity) {
    	//先获取旧数组的总长度,比如是10
        int oldCapacity = elementData.length;
        
        // 新长度 = 旧长度 + 旧长度/2 (即:新的 = 旧的 * 1.5)
        int newCapacity = oldCapacity + (oldCapacity >> 1); 
        
        //如果扩容1.5倍之后还小于 size + 1的值
        if (newCapacity - minCapacity < 0)
        	//新长度 = size + 1 
            newCapacity = minCapacity; 
            
            //新长度大于Integer.MAX_VALUE - 8么?
        if (newCapacity - MAX_ARRAY_SIZE > 0) 
        //扩容到最大值时(int.Max - 8)时返回int的最大值,表示再也不能扩容了
            newCapacity = hugeCapacity(minCapacity); 
     
    	 //创建完新的数组,然后实现数据复制。
        elementData = Arrays.copyOf(elementData, newCapacity); 
    }
    
通过这个过程你应该理解,在创建list的时候尽可能的指定集合的大小,避免频繁的扩容。    
复制代码

6、执行删除操作

(1)根据下标删除

    public E remove(int index) {
    	//判断下标是否符合标准
        rangeCheck(index);
        
        modCount++;
        
        // 获取到旧值,删除成功后返回
        E oldValue = elementData(index); 

		//获取需要移动的元素的个数(数组元素个数 - 当前位置)
        int numMoved = size - index - 1; 
        if (numMoved > 0)
        //将当前位置往后的一位数到数组的最后一位数统一往前移动一位,那当前位置的元素就被index +1的元素覆盖掉了。
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
       // 将数组最后一位置空
        elementData[--size] = null; 
        return oldValue;
    }
    注意:这个操作没有遍历数组所以复杂度是o(1
复制代码

(2)根据内容删除

    public boolean remove(Object o) {
     /	/ 传入空值则删掉数组中的空值
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else { 
       	 //删掉数组中对应的值
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        //如果值不存在直接返回false
        return false;
    }
复制代码

(3)fastRemove(index)

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
         //将当前位置往后的一位数到数组的最后一位数统一往前移动一位,那当前位置的元素就被index +1的元素覆盖掉了。
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }
复制代码

(4)在循环遍历删除

        List<Integer> list = new ArrayList<>();
        
        list.add(3);
        list.add(5);
        list.add(9);

        list.add(0,999);

        System.out.println(list);
        
        for (Integer num : list){
            if(num == 9){
                list.remove(num); //会报错:java.util.ConcurrentModificationException
            }
        }
        
复制代码

Result:上面这个代码报错的原因是因为在遍历之前就已经把当前集合的操作数赋值给一个预期操作数的变量,执行remove之前会把当前的操作指令数 + 1,然后当前操作数会跟预期操作数进行比较,如果不一致就会报上述错误,总而言之就是在改变集合之前先对当前操作数进行修改了。

So:怎么正确删除集合的元素呢?

        List<Integer> list = new ArrayList<>();

        list.add(3);
        list.add(5);
        list.add(9);

        list.add(0,999);

        System.out.println(list);
        
        // 先将集合变成iterator
        Iterator<Integer> iterator = list.iterator();
        
        while(iterator.hasNext()){
           Integer num = iterator.next();
           if(num == 5){
               iterator.remove();
           }
       }
        System.out.println(list);
复制代码

7 查询操作

    public E get(int index) {
   	 	//检查index大小是够合理 (0<=  x <= size)
        rangeCheck(index);
		//注解返回 elementData[index]的值
        return elementData(index);
    }
复制代码

8 集合清空

    public void clear() {
        modCount++;

       //循环删除所有元素
        for (int i = 0; i < size; i++)
            elementData[i] = null;
	//长度置为0
        size = 0;
    }

//这里数组的总长度没有变
复制代码

总结面试题:

1、ArrayList的初始化大小事几?

答:10

2、ArrayList扩容后的长度是多少?

答:原来长度的1.5倍

3、ArrayList添加和查找元素的时间复杂度是多少?

答:都是O(1)

4、ArrayList是线程安全的么?会有什么问题?有线程安全的List吗?

答:不是线程安全的的,会有很多问题,比如重复赋值、重复扩容等,线程安全的List有Vector 、CopyOnWriteArrayList 。

5、ArrrayList和LinkedList有什么区别?

答:(1)结构不同,ArrayList底层是基于数组实现的,所以在内存中数据存储是连续的。而LinkedList底层是基于链表实现的,所以内存中的数据可能是连续的也可能是不连续的。 (2)场景不同,ArrayList因为是内存连续的数组,所以它支持随机访问,当我们需要访问某个位置的数据时,ArrayList可直接获取,LinkedList则需要遍历链表获取,在这个过程中寻址是很慢的。当需要向中间位置新增某一个值的时候,因为ArrayList是连续的内存则需要将所添加位置之后的所有元素后移,留出一个位置来存放添加元素,而LinkedList链表只需要修改添加位置的元素的前后指针就可以了,删除和修改类似。所以对于随机访问ArrayList效果显著,当增删改时,LinkedList的效果显著。

注:增删改时ArrayList到底比LinkedList慢多少,这个得看电脑配置。我用MacBookPro 17款 4核 16G内存的电脑,在200万长度之前是一样的,后面开始越多越慢,在300万和3000万时对5001位置新增,ArrayList分别慢1ms 和 10ms.

分类:
后端
标签: