Java 集合类 源码分析学习 ----(3)ArrayList类的实现与设计

面试题:
看的面经里面,关于 ArrayList 的题目不多,牛客里面的选择题关于 ArrayList 的有一些。

关注点:

  • 讲讲 ArrayList 吧;

  • ArrayList 的设计细节:
    数据结构,默认参数,扩容机制等;

—————————————— 美丽的分割线 ——————————————-

本以为 ArrayList 应该就是,维护一个数组,然后实现增删改查,比较简单,没想到 上来就是 1000+ 行的代码…
浏览了一下,大部分和迭代器有关
先开始撸增删改查的代码吧~

  1. 先从数据结构开始吧~
    /**
     * 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; 

蹩脚翻译一下大概意思:
(1)这个 elementData 是 ArrayList 中数据储存缓存的地方;
(2)这个 elementData 的大小就是 ArrayList 的Capacity;
(3)当 elementData等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,第一次 add 的时候,elementData 的大小会被扩容到 DEFAULT_CAPACITY。

再看看 三个 构造函数 和 几个变量:

    /**
     * 默认容量,不指定容量初始化时,在第一次 add 时,会被扩容到capacity = 10 的大小.
     */
    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.
     * 
     *   默认初始化大小的共享实例
     *  !!!这个常量 是用来 在第一次 add 时,与 EMPTY_ELEMENTDATA 区别开。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;
    /**
    *  指定 初始化 容量的 构造函数
    */
    public ArrayList(int initialCapacity) {
    	// 初始化 容量 > 0,直接初始化数组
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
       // 初始化 容量 > 0,使用共享的空数组常量 EMPTY_ELEMENTDATA,从而 与 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 区分开来。
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
        }
    }
    /**
    * 默认无参的构造函数,使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 暂时作为内部数组 的实例
    **/
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

   /**
   *  对于 Collection 参数的初始化,Collection --> 数组 --> elementData
   */
    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;
        }
    }

以上,需要记住的几个细节问题:
(1)ArrayList 的使用无参构造函数初始化时,容量(capacity)时多少?
         答:DEFAULT_CAPACITY = 10,默认初始化容量为 10。

(2)ArrayList 初始化后,内部数组的情况?
         答:无参构造函数初始化时,内部数组都为一个空数组;指定初始容量initCapacity的构造函数初始化时,内部是一个 容量为 initCapacity 的数组。

(3)initCapacity = 0 和 无参构造函数初始化后,内部数组都为一个空数组,如何区别(第一次 add时)?
         答:无参构造函数初始化后 his.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;但是 initCapacity = 0 后的情况是,this.elementData = EMPTY_ELEMENTDATA。

  1. 增 :add(E e)方法:

主要过程分为两个部分 —- 1个是在需要的时候 对数组进行扩容;2 在内部数组的 size+1 处赋值

    public boolean add(E e) {
        // 1.保证扩容,必要时扩容
        ensureCapacityInternal(size + 1);  
        // 2. 赋值
        elementData[size++] = e;
        return true;
    }

先来看看扩容的时机 和 过程(撸代码):

    private void ensureCapacityInternal(int minCapacity) {
        // 确定当前需要的 最小容量
        // 如果是 无参构造函数 的话,会让它 >= 10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

以上,就区分开了 无参构造函数 还是指定容量构造函数 的初始化。
无参构造函数的话,最小容量 = 10
指定容量的构造函数初始化,最小容量可以是 0(其实是1)

    private void ensureExplicitCapacity(int minCapacity) {
        // fast-fail 机制
        modCount++;

        // overflow-conscious code
        // 内部数组不够大 --> 扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 扩容 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 保证容量 > 0,也就是 当 oldCapacity = 0的时候,newCapacity = minCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 主要针对 minCapacity > MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 的情况
        // minCapacity > MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 ? MAX_ARRAY_SIZE: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);
    }

再来关注一下几个细节问题吧~
(1)保证 无参构造函数 的初始化情况下,在第一次 add 时,保证容量 为10(DEFAULT_CAPACITY )?
         答: 在每次add 的时候,判断elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,如果处理就是无参初始化的情况,指定最小容量为 10。

(2)如何保证 add 的时候,数组不越界?
         答:首先确定本次 add 所需的最小容量 minCapacity :无参构造函数初始的情况下,minCapacity = 10,其他情况下都是 minCapacity = size + 1;当 minCapacity > elementData.length 时,会对数组进行扩容;在 gorw 函数中进行扩容,会对 旧数组的长度 * 1.5(记为 newCapacity)进行扩容,newCapacity < minCapacity 的情况下,直接让newCapacity = newCapacity,也就保证了 newCapacity > newCapacity,保证数组不越界。

在 ArrayList 中,还有一个 add 函数 add(int index, E element) ,在指定 index 位置添加元素。

过程也很简单:1. 必要时扩容;2. index 开始及后的元素(包括index)全部向后移动一位;3. elementData[index] = element。
直接看代码:

    public void add(int index, E element) {
        // 检查 index <= size ?小于就没有必要插入了
        rangeCheckForAdd(index);
		//必要时扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 复制数组
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
         // 赋值
        elementData[index] = element;
        size++;
    }
  1. 删除过程 remove(int index):
    简单啊,看代码:
    public E remove(int index) {
    
    	// 1. 检查 index
        rangeCheck(index);
        
        //2.  取出 旧值
        modCount++;
        E oldValue = elementData(index);

 		// 3. 调整size - index - 1 后的元素 向前移动 一个(复制)
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
                             
         // size - 1 位置 赋值为 null,以让空间被gc 回收
        elementData[--size] = null; // clear to let GC do its work
        
        // 返回旧值
        return oldValue;
    }
	// 1. 查找 元素的 index
	//2. 删除 index 位置的元素
    public boolean remove(Object o) {
    	//这里一个小细节就是 o==null 的判断,放在 for 循环外面,
    	//只需要一次判断,否则 放在 for 循环里面就要多次判断 ---- 效率问题呀
        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;
                }
        }
        return false;
    }

 	// fastRemove 过程 和 remove(int index) 一样的,类比一下
    private void fastRemove(int index) {
        modCount++;
        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
    }
  1. 查找 get(int index),contains(Object o):

get(int index) 非常简单,也就是 内部采用数组实现的优势了吧~ 直接 获取 elementData[index] 位置元素返回即可:

    public E get(int index) {
    	// 检查 index 范围
        rangeCheck(index);
        //返回  elementData[index]
        return elementData(index);
    }

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

contains(Object o) 相对没有那么简单,但是思路也很清晰:1.遍历内部数组 2. 返回第一个匹配的索引

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

	// 遍历数组,返回对应索引, -1表示没有找到
	// o == null 的在for循环的设计,同样是为了避免多次判断
    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
  1. 增删改查的时间复杂度
    add :O(1)
    remove:O(n)要对数组进行移动
    get:O(1)
    contains:O(n)要遍历一遍内部数组

关于 System.arraycopy 的效率问题(https://blog.csdn.net/wangyangzhizhou/article/details/79504818):
虽然说啊,System.arraycopy为 JVM 内部固有方法,它通过手工编写汇编或其他优化方法来进行 Java 数组拷贝,这种方式比起直接在 Java 上进行 for 循环或 clone 是更加高效的。
但是内部也是要对数组每个元素进行拷贝,最坏情况下,还是O(n)

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/qq_17832777/article/details/84638930
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞