ArrayList底层源码解析

发布于:2023-01-21 ⋅ 阅读:(360) ⋅ 点赞:(0)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:

ArrayList 是一个动态数组,实现了 List 接口以及 list相关的所有方法,它允许所有元素的插入,包括 null。另外,ArrayList 和 Vector 除了线程不同步之外,大致相等。


提示:以下是本篇文章正文内容,下面案例可供参考

一、ArrayList概述

首先看如下注释(为ArrayList类开头注释内容):

/**
 * Resizable-array implementation of the <tt>List</tt> interface.  Implements
 * all optional list operations, and permits all elements, including
 * <tt>null</tt>.  In addition to implementing the <tt>List</tt> interface,
 * this class provides methods to manipulate the size of the array that is
 * used internally to store the list.  (This class is roughly equivalent to
 * <tt>Vector</tt>, except that it is unsynchronized.)
 */
该注释提供的信息:
		1.ArrayList集合是对List接口的实现
		2.实现了List接口中所有可选的操作。
		3.该集合可以存入任何元素内容,包括null空值
    	4.除了实现list集合外,该类还提供了一些方法用来操作内部存储列表的数组。
    	5.ArrayList与Vector集合大致相同,不同的是ArrayList是线程不安全的,而Vector是线程安全的

​ ArrayList集合实际上就就是一个可变数组,是对List接口的实现。该集合可以存储任何内容元素,包括null值。它实现了List接口中所有的方法,并且提供了一些独有的方法来操作内部存储列表的数组。

​ 每个ArrayList集合初始化时都有一个容量,该容量代表值集合底层数组的长度。ArrayList初始化时,集合底层数组的长度默认值为0,当ArrayList实际要存储的容量大于等于数组的长度时,就需要对数组进行扩容处理。随着数组元素的不断增加,数组的容量也会进行自增长。集合容量自增长(实际上就是创建一个新的数组)完成后,就会将老数组中的数据拷贝到新的数组中。数组自增长时,会自动调用ensureCapacityInternal方法来对集合进行扩容处理。

​ 注意的时ArrayList集合是一个线程不安全的集合,若多个线程同时对一个集合进行操作,那么在操作的外部需要synchronized进行同步。

二、ArrayLIst的实现

ArrayList底层实际上就是数组,所以ArrayList中绝大都数方法都是对数组进行增删改查的操作。

1.ArrayList的属性

// 集合的默认容量
private static final int DEFAULT_CAPACITY = 10;

// 底层数组默认为空
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 底层数组
transient Object[] elementData;

// 集合的大小(集合实际存储的元素)
private int size;

// 集合可扩容的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

通过属性可以分析出:

​ 1.实例化一个空参集合时,其底层默认会创建一个空数组。

​ 2.ArrayList集合中的size()方法,实际上就是获取成员属性的size值。

​ 3.集合最大可以扩容到整型的最大值Interger>MAX_VALUE。

​ 4.底层数组的类型时Object类,所以集合可以插入任意元素,包括null值。

2.ArrayList集合的构造方法

第一种:空参构造器

	// 第一种:空参构造器
 	public ArrayList() {
    	// 初始化集合时将集合的底层数组初始化为一个空数组
    	// this.element = {}
    	this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
 	}

第二种:创建一个包含Collection集合的一个ArrayList集合列表

	// 第二种:创建一个包含Collection集合的一个ArrayList集合列表
 	public ArrayList(Collection<? extends E> c) {
        // 将Collection集合转成数组,然后直接赋值给集合底层数组,
        // 这时候ArrayList集合内的元素就是Collection集合内的元素了
        elementData = c.toArray();
        
        // 判断集合容器中是否有元素
        if ((size = elementData.length) != 0) {
            
         	// 判断字节码文件是否相同
            if (elementData.getClass() != Object[].class)
                // 对数组进行扩容处理
                elementData = Arrays.copyOf(elementData, size, Object[].class);
            
        } else {
            // 没有元素就将底层数组默认为空,达到实例化集合的目的
            this.elementData = 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("Illegal Capacity: " + initialCapacity);
        }
    }

3.集合扩容原理解析(add(E e)方法)

核心的代码

1.elementData[size++] = e; // 向数组中添加元素
2.int newCapacity = oldCapacity + (oldCapacity >> 1); // 每次创建新数组的长度
3.elementData = Arrays.copyOf(elementData, newCapacity); // 将数组扩容 最为核心的代码

核心思想

1.初始化集合时或创建一个集合时,源码会创建一个长度为0的Object数组。

2.当向集合中添加第一个元素时,此时源码会创建一个长度为10的新数组来替换旧的数组。

3.当向集合中添加第十一个元素时,此时源码就会创建一个长度为15的新数组来替换旧的数组。

4.至此以后,每当集合要扩容时,新的数组长度都是旧数组长度的1.5倍。

5.当底层数组长度没有超过Integer.MAX_VALUES - 8(整型的最大值 - 8) ,则新数组的长度为:MAX_ARRAY_SIZE

(也就是整型的最大值-8),当底底层数组的长度超过Integer.MAX_VALUES - 8时,则新数组的长度为整型最大值

(Integer.MAX_VALUES)。

扩容底层代码:
在这里插入图片描述

对于图中第13条语句中:hugeCapacity方法的扩充:

	// 实际该方法使用来判断扩容是否超过整型的最大值,因为扩充数组长度的变量的数据类型都是整型
	// 该判断的目的时为了防止扩容超过整型范围
	private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

4.get(int index) 方法

 	// 根据索引查找相应的元素值
	public E get(int index) {
        // 该方法是用来判断index是否在集合索引的范围内
        rangeCheck(index);

        return elementData(index);
    }
	
	// 该方法是用来判断,输入的索引值是否越界
	private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
	
	// 获取索引所对应的值
 	E elementData(int index) {
        return (E) elementData[index];
    }

总结:因为 ArrayList 是采用数组结构来存储的,所以它的 get 方法非常简单,先是判断一 下有没有越界,之后就可以直接通过数组下标来获取元素了,所以 get 的时间复杂 度是 O(1)。

5.set(int index , E e) 方法

    // 修改集合index对应的元素值
	public E set(int index, E element) {
        // 还是判断输入的index是否超出数组的索引值
        rangeCheck(index);
		
        // 获取到数组中旧的元素
        E oldValue = elementData(index);
        // 将新的值赋值给index对应的位置
        elementData[index] = element;
        // 返回旧的值
        return oldValue;
    }

总结:set 方法的作用是把下标为 index 的元素替换成 element,跟 get 非常类似,所以就不 在赘述了,时间复杂度度为 O(1)。

6.remove(int index) 方法

 	// 删除index所对应的集合中的个元素
	public E remove(int index) {
        // 查看输入的index索引是否在集合所包含的范围内
        rangeCheck(index);
		
        // 计算数组修改的次数,在多线程中可以规避一些问题
        modCount++;
        
        // 获取index对应的旧元素
        E oldValue = elementData(index);
		
        // index后面还有几个元素
        int numMoved = size - index - 1;
        if (numMoved > 0) // 判断index后面是否还有元素
            // 改变原来的数组,将指定源数组中的数组从指定位置复制到目标数组的指定位置。
            // 调用一个 native 的复制方法,把 index 位置开始的元素都往前挪一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //等待GC回收
        elementData[--size] = null; 
		
        // 返回旧的值
        return oldValue;
    }

总结:remove 方法与 add 带指定下标的方法非常类似,也是调用系统的 arraycopy 方法来 移动元素,时间复杂度为 O(n)

7.size()方法

	public int size() {
        return size;
    }

总结:实际上就是调用成员方属性中的size属性,所以时间复杂度为O(1),值得注意的是这里返回的是实际容量,而不是数组的大小。

8.indexOf(Object o) \ lastIndexOf(Object o)方法

 	// indexOf(Object o ) : 返回集合中第一次出现的索引值
	// 通过遍历数组的方式来查找内容
	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;
    }

	// lastIndexOf(Object o) :返回集合中最后一次出现的索引值
 	public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

总结:indexOf 方法的作用是返回第一个等于给定元素的值的下标。它是通过遍历比较数组 中每个元素的值来查找的,所以它的时间复杂度是 O(n)。 lastIndexOf 的原理跟 indexOf 一样,而它仅仅是从后往前找起罢了。

三、总结

1.ArrayList集合底层结构实际上就是Object[]的数组。

2.当实例化空参构造是,其默认将底层数组指向一个空的数组。

3.除了第一次添加扩容的容量为10外,其它的每次扩容都是旧数组长度的1.5倍

4.ArrayList集合中的方法,实际上就是对数组的增删改的操作。

5.当ArrayList集合扩容到整型的最大值时,旧无法再继续扩容。此时就会报出overflow内存溢出异常

本文含有隐藏内容,请 开通VIP 后查看