【手撕Java集合】为了搞懂 ArrayList,我拼了!!!

这是我在学习Java集合框架时对 ArrayList 的源码级解析,我将许多常用的方法都做出了解释,还有许多更为底层的地方没有涉及到,等到以后有机会我再补全。如果这篇文章对您有帮助的话,希望能给作者一个赞哦!!!

类图

在这里插入图片描述

  • 实现了RandomAccess接口,可以随机访问
  • 实现了Cloneable接口,可以克隆
  • 实现了Serializable接口,可以序列化、反序列化
  • 实现了List接口,是List的实现类之一
  • 实现了Collection接口,是Java Collections Framework成员之一
  • 实现了Iterable接口,可以使用for-each迭代

ArrayList 底层是用数组实现的存储。

特点:可自动扩容,查询效率高,增删效率低,线程不安全。

ArrayList 类注释释义:

  • ArrayList是实现List接口的可自动扩容的数组。实现了所有的List操作,允许所有的元素,包括null值。
  • ArrayList大致和Vector相同,除了ArrayList是非同步的。
  • size isEmpty get set iteratorlistIterator 方法时间复杂度是O(1),常量时间。其他方法是O(n),线性时间。
  • 每一个ArrayList实例都有一个capacity(容量)。capacity是用于存储列表中元素的数组的大小。capacity至少和列表的大小一样大。
  • 如果多个线程同时访问ArrayList的实例,并且至少一个线程会修改,必须在外部保证ArrayList的同步。修改包括添加删除扩容等操作,仅仅设置值不包括。这种场景可以用其他的一些封装好的同步的list。如果不存在这样的Object,ArrayList应该用Collections.synchronizedList包装起来最好在创建的时候就包装起来,来保证同步访问。
  • iterator()listIterator(int)方法是fail-fast的,如果在迭代器创建之后,列表进行结构化修改,迭代器会抛出ConcurrentModificationException
  • 面对并发修改,迭代器快速失败、清理,而不是在未知的时间不确定的情况下冒险。请注意,快速失败行为不能被保证。通常来讲,不能同步进行的并发修改几乎不可能做任何保证。因此,写依赖这个异常的程序的代码是错误的,快速失败行为应该仅仅用于防止bug

1. ArrayList 的属性

ArrayList 中共有7个属性:

在这里插入图片描述

属性解析:

在这里插入图片描述

    // 序列化版本UID
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默认的初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 用于零容量列表的共享空数组实例
     * new ArrayList(0);
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用于提供默认大小(10)列表的共享空数组实例
     * new ArrayList();
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 存储ArrayList元素的数组缓冲区
     * ArrayList的容量,是数组的长度
     * 
     * non-private to simplify nested class access
     * 非私有以简化嵌套类访问
     */
    transient Object[] elementData;

    /**
     * ArrayList中元素的数量
     */
    private int size;

transient 关键字

1)一旦变量被 transient 修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

2)transient 关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被 transient 关键字修饰的。变量如果是用户自定义类变量,则该类需要实现 Serializable 接口。

3)被 transient 关键字修饰的变量不再能被序列化,一个静态变量不管是否被 transient 修饰,均不能被序列化。

ArrayList 底层其实就是⼀个数组,ArrayList 中有扩容这么⼀个概念,正因为它扩容,所以它能够实现“动态”增长

2. 构造方法

ArrayList 中有一个无参构造器,两个有参构造器:

在这里插入图片描述

2.1 public ArrayList(int initialCapacity)

带一个初始容量参数的构造方法

    /**
     * 构造一个容量为initialCapacity的空列表
     * @param  initialCapacity  人为传入的列表的初始化容量
     * @throws IllegalArgumentException 
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //传入参数>0,则以此容量构造一个空Object数组,赋给底层数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //传入参数=0,则将属性中的0容量空数组EMPTY_ELEMENTDATA赋给底层数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //传入参数<0,参容量非法,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

2.2 public ArrayList()

无参构造方法

    /**
     * 构造一个容量为默认值10的空列表
     */    
	public ArrayList() {
        //使用空参构造器,则将属性中的默认的10容量空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋给底层数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

2.3 public ArrayList(Collection<? extends E> c)

带一个集合参数的构造方法

    /**
     * 构造一个列表,该列表包含了传入的集合中的所有元素,元素的添加顺序和它们被 传入的集合的迭代器 返回的	  * 顺序一致
     * @param c 元素将被放进新列表的集合
     * @throws NullPointerException
     */
    public ArrayList(Collection<? extends E> c) {
        //将集合中的对象传进一个数组中
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            //将数组大小赋给size(新建ArrayList的实际大小),如果不等于0
            if (c.getClass() == ArrayList.class) {
                //如果传入集合的类型是ArrayList,直接将集合的对象数组赋给底层数组,这里是为了防止 c.toArray 方法不正确的执行导致没有返回Object[]
                elementData = a;
            } else {
                //否则将集合的对象数组中的元素逐个拷贝到大小为size的Object数组中,再赋给底层数组
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            //如果传入的集合是空的,则将属性中的0容量空数组EMPTY_ELEMENTDATA赋给底层数组
            elementData = EMPTY_ELEMENTDATA;
        }
    }

什么情况下c.toArray()会不返回Object[]呢?

java.util.ArrayList.toArray()方法会返回Object[]没有问题。而java.util.Arrays的私有内部类 ArrayList 的toArray()方法可能不返回Object[]。因为 java.util.Arrays的内部 ArrayList 的toArray()方法,是构造方法接收什么类型的数组,就返回什么类型的数组。

3. 添加方法 add()

总览 add() 方法:

在这里插入图片描述

3.1 public boolean add(E e)

在列表最后添加指定元素

步骤:

  • 检查是否需要扩容
  • 插入元素
    /**
     * 将指定元素添加到列表尾部
     * @param e 需要添加到列表尾部的元素
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!——增加modCount
        elementData[size++] = e;
        return true;
    }

在父类AbstractList上,定义了modCount 属性,用于记录数组修改的次数。

其中,ensureCapacityInternal(size + 1);有如下作用:

  • 确认 list 容量,尝试将容量 + 1,看看有无必要
  • 添加元素

接下来看看这个方法的工作机理:

在这里插入图片描述

当添加第11个元素时,minCapacity 为11,底层数组长度为10,那么就需要扩容了

接下来再来看看grow()方法的实现:

在这里插入图片描述

  • 通常情况新容量是原来容量的1.5倍
  • 如果原容量的1.5倍比minCapacity小,那么就扩容到minCapacity
  • 特殊情况扩容到Integer.MAX_VALUE

进入copyOf()方法瞧瞧~

在这里插入图片描述

到此为止,add(E e)的实现流程如下:

首先去检查一下数组的容量是否足够

  • 足够:直接添加新元素
  • 不足够:扩容
    • 将列表容量扩容到原来的 1.5
    • 第⼀次扩容后,如果容量还是小于minCapacity,就将容量扩充为 minCapacity

3.2 add(int index, E element)

在指定位置添加指定元素

步骤:

  • 检查角标
  • 空间检查,如果有需要进行扩容
  • 插入元素
/**
 * 在指定位置添加指定元素
 * 如果指定位置已经有元素,就将该元素和随后的元素移动到右面一位
 *
 * @param index 待插入元素的下标
 * @param element 待插入的元素
 * @throws 可能抛出 IndexOutOfBoundsException
 */
public void add(int index, E element) {
    rangeCheckForAdd(index);//检查角标是否越界

    // 增加 modCount !!
    ensureCapacityInternal(size + 1);
    System.arraycopy(elementData, index, elementData, index + 1, size - index);//调用arraycopy()进行插入
    elementData[index] = element;
    size++;
}

看到 arraycopy(),我们可以发现:该方法是由C/C++ 来编写的,并不是由 Java 实现:

在这里插入图片描述

4. 移除方法 remove()

4.1 public E remove(int index)

移除指定下标元素方法

步骤:

  • 检查角标
  • 删除元素
  • 计算出需要移动的个数,并移动
  • 设置为null,让Gc回收

在这里插入图片描述

详解:

/**
 * 移除列表中指定下标位置的元素
 * 将所有的后续元素,向左移动
 *
 * @param 要移除的指定下标
 * @return 返回被移除的元素
 * @throws 下标越界会抛出IndexOutOfBoundsException
 */
public E remove(int index) {
    rangeCheck(index);//检查下标合法性

    modCount++;//数组修改的次数+1
    E oldValue = elementData(index);
	
    //将所删除元素的右边元素逐个向左移动
    int numMoved = size - index - 1;
    if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,  numMoved);
    // 将引用的最后一个置空,让GC回收
    elementData[--size] = null;

    return oldValue;
}

4.2 public boolean remove(Object o)

移除指定元素方法

在这里插入图片描述

详解:

/**
 * 移除第一个在列表中出现的指定元素
 * 如果存在,移除返回true
 * 否则,返回false
 *
 * @param o 指定元素
 */
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;
            }
    }
    return false;
}

移除方法名字、参数的个数都一样,使用的时候要注意。

4.3 private void fastRemove(int index)

私有移除方法

在这里插入图片描述

详解:

/*
 * 私有的 移除 方法 跳过边界检查且不返回移除的元素
 */
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    //将引用置空,让GC回收
    elementData[--size] = null;
}

5. 查找方法 get()

5.1 public E get(int index)

查找指定位置的元素

在这里插入图片描述

详解:

/**
 * 返回指定位置的元素
 *
 * @param  index 指定元素的位置 
 * @throws index越界会抛出IndexOutOfBoundsException
 */
public E get(int index) {
    rangeCheck(index);//检查角标

    return elementData(index);//返回具体元素
}

检查角标:

// 检查⻆标
private void rangeCheck(int index) {
    if (index >= size)
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

返回元素:

@SuppressWarnings("unchecked")
E elementData(int index) {
	return (E) elementData[index];
}

5.2 public int indexOf(Object o)

查找指定元素的所在位置

在这里插入图片描述

详解:

/**
 * 返回指定元素第一次出现的下标
 * 如果不存在该元素,返回 -1
 * 如果 o == null 会特殊处理
 */
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;
}

6. set() 方法

步骤:

  • 检查角标
  • 替代元素
  • 返回旧值

在这里插入图片描述

7. 序列化方法 writeObject()

/**
 * 将ArrayList实例的状态保存到一个流里面
 */
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // 按照顺序写入所有的元素
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

8. 反序列化方法 readObject()

/**
 * 根据一个流(参数)重新生成一个ArrayList
 */
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt();

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

elementData之所以用transient修饰,是因为 JDK 不想将整个elementData都序列化或者反序列化,而只是将size和实际存储的元素序列化或反序列化,从而节省空间和时间。

9. subList() 方法

创建子数组列表

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);//检查传入参数的合理性
    return new SubList(this, 0, fromIndex, toIndex);//返回创建好的子列表
}

其中的 subListRangeCheck()方法:

static void subListRangeCheck(int fromIndex, int toIndex, int size) {
    if (fromIndex < 0)
        throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
    if (toIndex > size)
        throw new IndexOutOfBoundsException("toIndex = " + toIndex);
    if (fromIndex > toIndex)
        throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")");
}

再来看看 SubList 类的定义:

private class SubList extends AbstractList<E> implements RandomAccess {
    private final AbstractList<E> parent;
    private final int parentOffset;
    private final int offset;
    int size;

    SubList(AbstractList<E> parent,
            int offset, int fromIndex, int toIndex) {
        this.parent = parent;
        this.parentOffset = fromIndex;
        this.offset = offset + fromIndex;
        this.size = toIndex - fromIndex;
        this.modCount = ArrayList.this.modCount;
    }

    public E set(int index, E e) {
        rangeCheck(index);
        checkForComodification();
        E oldValue = ArrayList.this.elementData(offset + index);
        ArrayList.this.elementData[offset + index] = e;
        return oldValue;
    }

    // 省略代码...
}

注意:

  • SubList 的set()方法,是直接修改ArrayListelementData数组的,使用中应该注意
  • SubList 是没有实现Serializable接口的,是不能序列化的

10. 迭代器实现

创建迭代器的方法:

在这里插入图片描述

接下来再来看看 Itr 这个类:

  • Itr 属性

    // 下一个要返回的元素的下标
    int cursor;
    // 最后一个要返回元素的下标 没有元素返回 -1
    int lastRet = -1;
    // 期望的 modCount
    int expectedModCount = modCount;
    
  • Itr的 hasNext() 方法

    public boolean hasNext() {
        return cursor != size;
    }
    
  • Itr的 next() 方法

    public E next() {
        checkForComodification(); //检查修改次数和期望修改次数是否一致,这是为了防止并发修改异常
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }
    
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
    

    什么是并发修改异常?

    当我们在对集合进行迭代操作的时候,如果同时对集合对象中的元素进行某些操作,则容易导致并发修改异常的产生。 这经常出现在多个线程同时对一个列表进行操作时。

  • Itr的 remove() 方法

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();
    
        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            // 移除之后将modCount 重新赋值给 expectedModCount
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    

11. 总结

  • ArrayList底层的数据结构是数组,在增删时候,需要数组的拷贝复制。

  • ArrayList可以自动扩容,不传初始容量或者初始容量是0,都会初始化一个空数组,但是如果添加元素,会自动进行扩容,所以,创建ArrayList的时候,给初始容量是必要的。【ArrayList的默认初始化容量是10,每次扩容时候增加原先容量的⼀半,也就是变为原来的1.5倍】

  • Arrays.asList()方法返回的是的Arrays内部的ArrayList,用的时候需要注意

  • subList()返回内部类,不能序列化,和ArrayList共用同一个数组

  • 迭代删除要用迭代器的remove方法,或者可以用倒序的for循环

  • ArrayList重写了序列化、反序列化方法,避免序列化、反序列化全部数组,浪费时间和空间

  • elementData不使用private修饰,可以简化内部类的访问

  • 删除元素时不会减少容量,若希望减少容量则调用 trimToSize()

    在这里插入图片描述

  • 它不是线程安全的。它能存放null值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kaho Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值