源码终结者之ArrayList源码解读

前言

同学们好,欢迎来到源码终结者系列,很多同学在编程的道路上会感觉读源码有很大的难度, 认为我们都是普普通通的开发人员, 没必要去研究那么"高大上"的底层源码, 实际则不然, 读源码有很多的好处, 并且可以解决我们在工作or学习中遇见的疑难杂症, 一个高级程序员是需要去深入研究一些技术的,那么读源码的能力则极其重要。那么就从今天起, 跟随我一起来撕开各种技术源码的面纱吧!

ArrayList介绍

ArrayList 是一个由数组构成的一个集合,并且具备可扩展性(可扩容),并且ArrayList也支持null元素, 并且支持泛型

ArrayList实现的接口

List接口

ArrayList是一个实现了List,RandomAccess,Cloneable,Serializable的类
List接口可以实现对元素的增删改查等功能

RandomAccess接口

RandomAccess则是对元素进行随机访问的支持(JDK作者提供的一个无方法的标志性接口,实现该接口则代表可以支持随机访问)

Cloneable接口

Cloneable也是一个标志性接口, 也是实现该接口则代表可以克隆一个不同地址的对象

Serializable接口

Serializable也是一个标志性接口,代表该类可以被序列化

核心参数

那么当我们了解完ArrayList实现了哪些接口后,我们就对ArrayList中的核心参数做一个说明吧!

// 代表数组的默认长度
private static final int DEFAULT_CAPACITY = 10;
// 如果使用有参构造函数创建集合时使用该常量创建
private static final Object[] EMPTY_ELEMENTDATA = {};
// 如果使用无参构造函数创建集合时使用该常量创建
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 该数组则是ArrayList真正用来存数据的地方, 实际上就是一个数组!
// transient 代表该对象不会被序列化
transient Object[] elementData;
// 数组的实际长度
private int size;
// 数组的最大长度, 我的内存顶不住了啊!
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

那么看到这里,恭喜你,对ArrayList中的各个参数应该都明白了,那么我们即将开始源码的旅途!

核心方法源码解读

从这里开始我们将要开始我们的方法源码解读啦,将会对ArrayList中的核心方法做一个细致的解读,感兴趣的同学们不要错过哦!

get方法

俗话说得好,从简至繁,那么我们首先先从较为简单的get方法来看一看

// 此方法代表根据传递的index索引值去获取该数组中对应位置的元素
public E get(int index) {
    // 该方法则是判断索引是否越界, 如果越界则抛出索引越界异常
    rangeCheck(index);
    // 这里会发现elementData(index)实际上是一个方法, 其实等同于elementData[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];
}

那这个时候相比同学们已经掌握了get方法的细节, 那么我们来看一下一个ArrayList对象被创建的时候会做什么呢?

ArrayList构造

首先来看最简单的无参构造

无参构造

public ArrayList() {
	// 将数组赋值为默认的{}, 代表是一个空数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

传int类型参数的构造

// initialCapacity为用户指定的容量大小
public ArrayList(int initialCapacity) {
	// 当指定的容量大小>0的时候
    if (initialCapacity > 0) {
    	// 新创建一个对应容量的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
    	// 当指定容量=0的时候, 则使用上面讲到的有参构造创建数组的常量对应的{}
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
    	// 当指定容量<0的时候, 则抛出异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
    }
}

传集合的构造

	// c是一个集合
	public ArrayList(Collection<? extends E> c) {
		// 首先将传过来的集合转成数组
        elementData = c.toArray();
        // 将数组的长度赋值给size变量后判断是否不为0
        if ((size = elementData.length) != 0) {
        	// 如果不为0的话判断数组的类型是否是Object类型, 如果不是的话需要对数组进行复制改为Object类型
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
        	// 如果数组长度为0则赋值为默认的{}
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

那么到这里ArrayList的构造则就解读完了, 是不是没有什么难度呢?创建好集合后我们要往里面插入新的值呀,那么我们来看一下add的方法

add方法

	// e代表要插入到数组中的参数
	public boolean add(E e) {
		// 该方法是为了判断是否需要扩容
        ensureCapacityInternal(size + 1);
        // 将数组当前长度对应的索引位置赋值为新插入的元素e
        elementData[size++] = e;
        // 返回true代表插入成功
        return true;
    }

	// calculateCapacity返回新增数据后的最大长度, ensureExplicitCapacity则判断是否需要扩容
	private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

	//返回新增数据后的最大长度, 传参elementData就是数组变量, minCapacity是size变量+1
	private static int calculateCapacity(Object[] elementData, int minCapacity) {
		// 判断数组是否是无参构造创建的并且是第一次初始化的
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        	// 返回10或者minCapacity(谁大返回谁)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

	// 该方法判断是否需要进行扩容
	private void ensureExplicitCapacity(int minCapacity) {
		// 操作数加一
        modCount++;
        // 如果数组新增后的长度减去现在数组的长度大于0, 那么则进行扩容
        if (minCapacity - elementData.length > 0)
        	// 扩容方法
            grow(minCapacity);
    }

	// 扩容方法
	private void grow(int minCapacity) {
		// 先用oldCapacity 接收原来数组的长度
        int oldCapacity = elementData.length;
        // newCapacity 代表扩容后的长度, 等于原来长度加上原来的长度除以2, 相当于1.5倍(不懂左右移的同学可以去学习一下, 还是比较简单的
        // >>代表二进制的数往右移动一位,那么就相当于除2。 <<则是往左移动一位, 相当于乘以二)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 判断新的最大长度减去新增后的数组长度是否小于0
        if (newCapacity - minCapacity < 0)
        	// 将新增后的数组长度赋值给扩容后的最大长度
            newCapacity = minCapacity;
        // 如果扩容后的最大长度超过了默认的最大长度后
        if (newCapacity - MAX_ARRAY_SIZE > 0)
        	// 返回一个边界长度
            newCapacity = hugeCapacity(minCapacity);
		// 将原来的数组进行拷贝, 变成新数组后赋值给原来的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

	// 返回一个边界长度
	private static int hugeCapacity(int minCapacity) {
		// 判断新增之后的长度是否小于0, 小于0则抛出异常
        if (minCapacity < 0)
            throw new OutOfMemoryError();
        // 如果新增后的长度大于了默认的最大值, 则返回int类型最大值, 否则是默认最大值
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

那么add还支持通过索引位置去添加元素,我们一起来看一下

	// index代表要插入的元素位置, element代表要插入的元素
    public void add(int index, E element) {
    	// 检查索引是否越界
        rangeCheckForAdd(index);
		// 该方法是为了判断是否需要扩容(同上面一致)
        ensureCapacityInternal(size + 1);
        // 拷贝数组, 实际上就是将该index位置的元素和之后的元素都往后移动一位, 留出空间给新插入的元素。
        // 参数概念:第一个是原数组,第二个是原数组中的开始位置,第三个是目标数组,第四个是目标数据中的开始位置,第五个是要复制数组元素的数量
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 插入新元素
        elementData[index] = element;
        // 数组长度++
        size++;
    }
    
    // 检查索引是否越界
	private void rangeCheckForAdd(int index) {
		// 当index大于数组长度或者小于0时, 抛出异常
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

set方法

那add方法是往数组中新增一个元素, 我们能不能修改数组中的元素呢, 显然也是可以的, 提供了一个Set方法用来修改元素, 那么我们来一探究竟!

	// index代表要修改的元素索引位置, element是修改后的元素
	public E set(int index, E element) {
		// 检查数组是否越界(get方法中已经讲过, 忘记的同学回顾一下)
        rangeCheck(index);
		// 获取旧元素数据并在下面返回
        E oldValue = elementData(index);
        // 修改对应的索引位置为新元素
        elementData[index] = element;
        // 返回旧元素
        return oldValue;
    }

remove方法删除元素

那么当我们添加完元素后, 不需要的元素肯定是需要删除方法的

	// 根据索引位置删除元素
    public E remove(int index) {
    	// 检查索引是否越界(参考上面已讲过的内容...)
        rangeCheck(index);
		// 修改次数加一
        modCount++;
        // 先取出将要被删除的元素
        E oldValue = elementData(index);
		// 计算删除需要移动的元素数量, 比如size=10, 我们要删第五个元素, 那么就是size - 4 - 1, 假设数组是1 2 3 4 5 6 7 8 9 0那么我们删除5这个元素的时候需要把后面的6 7 8 9 0往前移动一位
        int numMoved = size - index - 1;
        // 当要移动的元素数量大于0时, 就需要进行数组拷贝
        if (numMoved > 0)
        	// 参数概念:第一个是原数组,第二个是原数组中的开始位置,第三个是目标数组,第四个是目标数据中的开始位置,第五个是要复制数组元素的数量
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 将最后一个元素设置成null
        elementData[--size] = null;
		// 返回被删除的元素
        return oldValue;
    }

clear方法

清空数组中所有元素, 灰常简单!

    public void clear() {
    	// 修改次数加1
        modCount++;
        // 循环将每个元素赋值为null.....
        for (int i = 0; i < size; i++)
            elementData[i] = null;
		// 数组长度设置为0
        size = 0;
    }

到这里, 一些ArrayList对数组操作的核心方法就告一段落了, 下面我们再来讲几个极其常用的方法源码

indexOf方法

indexOf方法是判断一个元素是否存在于该数组中, 如果存在则返回对应的索引位置

	// o是要检查是否存在的元素
    public int indexOf(Object o) {
    	// 如果o==null的时候, 则判断元素中是否存在null的元素, 如果存在就返回对应的索引位置
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
        	// 如果o!=null, 则判断元素中是否存在相等的元素, 如果存在就返回对应的索引位置
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        // 没有找到则返回-1
        return -1;
    }

lastIndexOf方法

lastIndexOf和indexOf方法基本一致, 只不过是从后往前找而已

	// 与indexOf基本一致, 参考上面的反向遍历即可
    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;
    }

contains方法

实际也就是调用了一下indexOf方法

	// 当indexOf方法查到的元素>=0的时候就返回true
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

isEmpty方法

判断数组是否为空

    public boolean isEmpty() {
    	// 判断数组长度是否为0
        return size == 0;
    }

结语

到这里ArrayList的核心方法基本都讲解完了, 如果哪里有错误欢迎指出, 共同学习与进步。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值