zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【Java集合】ArrayList源码分析

JAVA源码集合 分析 ArrayList
2023-09-27 14:19:52 时间

目录

一、ArrayList介绍

1.1 简介

1.2 继承体系

二、源码剖析

2.1 成员属性

2.2 构造方法

2.2.1 带int类型的构造方法:ArrayList(int initialCapacity)

2.2.2 无参构造方法:ArrayList()

2.2.3 Collection型构造方法:ArrayList(Collection c)

2.3 常用方法

2.3.1 add()方法

2.3.2 get(int index)方法

2.3.3 remove()方法

2.3.4 set()方法

2.3.5 size()方法

2.3.6 indexOf(Object o) 方法

三、ArrayList的优缺点

四、总结


一、ArrayList介绍

前言:作为一个常用的List接口实现类,日常开发过程中使用率非常高,因此有必要对其原理进行分析。

注:本文jdk源码版本为jdk1.8.0_172

1.1 简介

ArrayList 的底层是数组,相当于动态数组。默认容量为10,它具有动态扩容的能力,线程不安全,元素可以为null。

与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。

ArrayList继承于 AbstractList ,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。

ArrayList简介:

  • ArrayList 是从JDK1.2 引入的。
  • ArrayList是可调整大小的数组,实现了List接口。 实现所有可选列表操作,并允许所有元素包括null 。 除了实现List 接口之外,该类还提供了一些方法来操纵内部使用的存储列表的数组的大小。 (这个类是大致相当于Vector,不同之处在于它是不同步的)。
  • ArrayList内部封装一个数组,用数组来存储数据。内部数组的默认初始容量 10,存满后 1.5 倍增长
  • 从 JDK1.8开始, ArrayList 一开始创建一个长度为 0 的数组,当添加第一个元素时再创建一个始容量为 10 的 数组。

1.2 继承体系

java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractList<E>
               ↳     java.util.ArrayList<E>

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
  • ArrayList实现了List, RandomAccess, Cloneable, java.io.Serializable等接口。
  • ArrayList实现了List,提供了基础的添加、删除、遍历等操作。
  • ArrayList实现了RandomAccess,提供了随机访问的能力。RandomAccess 是一个标志接口,表明实现这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • ArrayList实现了Cloneable,即覆盖了函数clone(),可以被克隆。
  • ArrayList实现了Serializable,这意味着ArrayList支持序列化,能通过序列化去传输。

二、源码剖析

2.1 成员属性

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 序列化 id
    private static final long serialVersionUID = 8683452581122892189L;

    // 默认容量的大小为 10,也就是通过new ArrayList()创建时的默认容量。
    private static final int DEFAULT_CAPACITY = 10;

    // 空实例共享的一个空数组。如果通过new ArrayList(0)传入的容量为0时会使用这个空数组。
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 默认的空数组常量为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,
    // 这个空数组是通过new ArrayList()创建时使用的,与EMPTY_ELEMENTDATA的区别是在添加第一个元素时使用这个空数组的会初始化为DEFAULT_CAPACITY(10)个元素,添加第一个元素的时候会重新初始为默认容量大小。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 真正存放元素的地方,存放元素的数组。
    // 使用transient是为了不序列化这个字段,transient表示不可序列化
    // 至于没有使用private修饰,后面注释是写的“为了简化嵌套类的访问”,但是实测加了private嵌套类一样可以访问。
    // private表示是类私有的属性,只要是在这个类内部都可以访问,嵌套类或者内部类也是在类的内部,所以也可以访问类的私有成员。
    transient Object[] elementData; // non-private to simplify nested class access
    
    // 元素的数量,默认是0。存储真正元素的个数,而不是elementData数组的长度。
    private int size;
    
    ......
    
    // 最大数组容量为 Integer.MAX_VALUE – 8
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

注意:被标记为transient的属性在对象被序列化的时候不会被保存,也就是它不会被序列化。

elementData设置成了transient,那ArrayList是怎么把元素序列化的呢?

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
    // 防止序列化期间有修改
    int expectedModCount = modCount;
    // 写出非transient非static属性(会写出size属性)
    s.defaultWriteObject();
    // 写出元素个数
    s.writeInt(size);
    // 依次写出元素
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
    // 如果有修改,抛出异常
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    // 声明为空数组
    elementData = EMPTY_ELEMENTDATA;
    // 读入非transient非static属性(会读取size属性)
    s.defaultReadObject();
    // 读入元素个数,没什么用,只是因为写出的时候写了size属性,读的时候也要按顺序来读
    s.readInt();
    if (size > 0) {
        // 计算容量
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        // 检查是否需要扩容
        ensureCapacityInternal(size);
        
        Object[] a = elementData;
        // 依次读取元素到数组中
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

查看writeObject()方法可知,先调用s.defaultWriteObject()方法,再把size写入到流中,再把元素一个一个的写入到流中。

一般地,只要实现了Serializable接口即可自动序列化,writeObject()和readObject()是为了自己控制序列化的方式,这两个方法必须声明为private,在java.io.ObjectStreamClass#getPrivateMethod()方法中通过反射获取到writeObject()这个方法。

在ArrayList的writeObject()方法中先调用了s.defaultWriteObject()方法,这个方法是写入非static非transient的属性,在ArrayList中也就是size属性。同样地,在readObject()方法中先调用了s.defaultReadObject()方法解析出了size属性。

elementData定义为transient的优势,自己根据size序列化真实的元素,而不是根据数组的长度序列化元素,减少了空间占用。

2.2 构造方法

2.2.1 int类型的构造方法:ArrayList(int initialCapacity)

传入初始容量:

  • 如果传入参数也就是初始容量大于0,则新建一个初始容量大小的数组;
  • 如果传入的参数初始容量等于0,将EMPTY_ELEMENTDATA赋值给 elementData;
  • 如果传入的参数初始容量小于0,将抛出异常。
/**
 * Constructs an empty list with the specified initial capacity.
 *
 * @param  initialCapacity  the initial capacity of the list
 * @throws IllegalArgumentException if the specified initial capacity
 *         is negative
 */
public ArrayList(int initialCapacity) {
    // 如果初始容量大于0,就新建一个数组存储元素
    if (initialCapacity > 0) {
        // 新建一个初始容量大小的数组
        this.elementData = new Object[initialCapacity];
    // 如果初始容量为0,使用空数组EMPTY_ELEMENTDATA
    } else if (initialCapacity == 0) {
        // 将EMPTY_ELEMENTDATA赋值给 elementData
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 如果传入初始容量小于0,则抛出异常 “非法的容量”
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

 

2.2.2 无参构造方法:ArrayList()

不传初始容量,初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,会在添加第一个元素的时候扩容为默认的大小,即10。

/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    // 没有指定初始容量,将成员变量elementData的值设为默认值
    // 使用这个数组是在添加第一个元素的时候会扩容到默认大小10
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

 

2.2.3 Collection<? extends E>型构造方法:ArrayList(Collection<? extends E> c)

传入集合并初始化elementData,这里会使用拷贝把传入集合的元素拷贝到elementData数组中,如果元素个数为0,则初始化为EMPTY_ELEMENTDATA空数组。

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 * 
 * 把传入集合的元素初始化到ArrayList中
 */
public ArrayList(Collection<? extends E> c) {
    // 将参数中的集合转换为数组,赋值给 elementData 
    elementData = c.toArray();
    // 如果数组的大小不等于 0
    if ((size = elementData.length) != 0) {
        // 检查c.toArray()返回的是不是Object[]类型,如果不是,重新拷贝成Object[].class类型
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        // 如果c.toArray()返回的数组类型不是Object[]类型的
        // 则利用Arrays.copyOf()创建一个大小为size的、类型为Object[]的数组, 并将elementData中的元素复制到新的数组中
        // 最后让elementData 指向新的数组
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    // 否则设置元素数组为空数组        
    } else { 
        // replace with empty array.
        // 如果c的空集合,则初始化为空数组EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

总结:

  • 将参数中的集合转换为数组,赋值给 elementData
  • 给size进行赋值,size代表集合元素数量。判断参数是否为空,如果数组的大小不等于 0,则进一步判断是否转化为Object类型的数组,如果不是,则进行复制;
  • 否则,设置元素数组为空数组

为什么c.toArray();返回的有可能不是Object[]类型呢?请看下面的代码:

public class ArrayTest {
    public static void main(String[] args) {
        Father[] fathers = new Son[]{};
        // 打印结果为class [com.coolcoding.code.Son]
        System.out.println(fathers.getClass());
        List<String> strList = new MyList();
        // 打印结果为class [java.lang.String]
        System.out.println(strList.toArray().getClass());
    }
}

class Father {}

class Son extends Father {}

class MyList extends ArrayList<String> {
    /**
     * 子类重写父类的方法,返回值可以不一样
     * 但这里只能用数组类型,换成Object就不行
     * 应该算是java本身的bug
     */
    @Override
    public String[] toArray() {
        // 为了方便举例直接写死
        return new String[]{"1", "2", "3"};
    }
}

2.3 常用方法

2.3.1 add()方法

  • add(E e) 方法 将指定的元素追加到此集合的末尾。平均时间复杂度为O(1)。
/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    // 确认容量,插入元素之前,会检查是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将元素添加到数组中最后一个元素的后面,最后将集合中实际的元素个数加 1
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    // 进一步确认ArrayList的容量,看是否需要进行扩容
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果elementData为空,则返回默认容量和minCapacity中的最大值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 否则直接返回minCapacity
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    // 修改次数自增
    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);
    // 确认最终容量
    if (newCapacity - minCapacity < 0)
        newCapacity = 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);
}

其实add方法整体逻辑还是比较简单。主要注意扩容条件:只要插入数据后整个数组中存储的数据数量size比此时的数组长度大就会进行扩容。因此如果在循环中使用ArrayList时需要特别小心,避免频繁扩容造成OOM异常。

流程:

(1)检查是否需要扩容;

(2)如果elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA则初始化容量大小为DEFAULT_CAPACITY;

(3)新容量是老容量的1.5倍(oldCapacity + (oldCapacity >> 1)),如果加了这么多容量发现比需要的容量还小,则以需要的容量为准;

(4)创建新容量的数组并把老数组拷贝到新数组;

  • add(int index, E element) 在集合元素序列 index 位置处插入。平均时间复杂度为O(n)。
/**
 * Inserts the specified element at the specified position in this
 * list. Shifts the element currently at that position (if any) and
 * any subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    // 检查下标索引是否越界
    rangeCheckForAdd(index);
    // 插入元素之前,会检查是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将 index 位置及其后面的所有元素都向后移一位,将index位置空出来
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 将新元素插入到 index 位置处,元素个数加 1
    elementData[index] = element;
     // 元素个数自增
    size++;
}
// 抛出越界异常
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

整体逻辑简单:越界检查 -> 确认容量 -> 元素后移 -> 插入元素。

流程:

(1)检查索引是否越界;

(2)检查是否需要扩容;

(3)把插入索引位置后的元素都往后挪一位;

(4)在插入索引位置放置插入的元素;

(5)大小加1;

  • addAll(Collection<? extends E> c)方法,将集合c中所有元素添加到当前ArrayList中。就相当于求两个集合的并集,会保留重复的元素。
public boolean addAll(Collection<? extends E> c) {
    // 将集合c转为数组
    Object[] a = c.toArray();
    int numNew = a.length;
    // 检查是否需要扩容
    ensureCapacityInternal(size + numNew);
    // 将c中元素全部拷贝到数组的最后
    System.arraycopy(a, 0, elementData, size, numNew);
    // 大小增加c的大小
    size += numNew;
    // 如果c不为空就返回true,否则返回false
    return numNew != 0;
}

流程:

(1)拷贝c中的元素到数组a中;

(2)检查是否需要扩容;

(3)把数组a中的元素拷贝到elementData的尾部;

2.3.2 get(int index)方法

获取指定索引位置的元素,时间复杂度为O(1)。

public E get(int index) {
    // 检查下标是否越界
    rangeCheck(index);
    // 返回数组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];
}

流程:

(1)检查索引是否越界,这里只检查是否越上界,如果越上界抛出IndexOutOfBoundsException异常,如果越下界抛出的是ArrayIndexOutOfBoundsException异常。

(2)返回索引位置处的元素;

2.3.3 remove()方法

  • remove(int index) 方法是删除该集合中指定位置的元素,时间复杂度为O(1)。
/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).
 *
 * @param index the index of the element to be removed
 * @return the element that was removed from the list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
    // 检查下标索引是否越界
   rangeCheck(index);
    // 修改次数自增
   modCount++;
   // 返回被删除的元素值
   E oldValue = elementData(index);
   
   // 需要移动的元素的个数
   int numMoved = size - index - 1;
   // 如果index不是最后一位,则将index之后的元素往前挪一位
   if (numMoved > 0)
       // 将 index + 1 及其后面的元素向前移动一位,覆盖被删除的元素值
       System.arraycopy(elementData, index+1, elementData, index,
                        numMoved);
   // 把数组最后一个元素赋值为null,将元素个数减一,帮助GC
   elementData[--size] = null; // clear to let GC do its work
   // 返回旧值
   return oldValue;
}

remove逻辑还是比较简单,但是这里需要注意一点是ArrayList在remove的时候,并没有进行缩容

流程:

(1)检查索引是否越界;

(2)获取指定索引位置的元素;

(3)如果删除的不是最后一位,则其它元素往前移一位;

(4)将最后一位置为null,方便GC回收;

(5)返回删除的元素。

可以看到,ArrayList删除元素的时候并没有缩容。

  • remove(Object o) 方法是删除指定的元素,如果有重复的元素,则只会删除下标最小的元素,时间复杂度为O(n)。
/**
  * Removes the first occurrence of the specified element from this list,
  * if it is present.  If the list does not contain the element, it is
  * unchanged.  More formally, removes the element with the lowest index
  * <tt>i</tt> such that
  * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
  * (if such an element exists).  Returns <tt>true</tt> if this list
  * contained the specified element (or equivalently, if this list
  * changed as a result of the call).
  *
  * @param o element to be removed from this list, if present
  * @return <tt>true</tt> if this list contained the specified element
  */
public boolean remove(Object o) {
    // 如果被移除元素为null
    if (o == null) {
        // 遍历整个数组,找到元素第一次出现的位置,并将其快速删除
        for (int index = 0; index < size; index++)
            // 如果要删除的元素为null,则以null进行比较,使用==
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        // 遍历整个数组,找到元素第一次出现的位置,并将其快速删除
        for (int index = 0; index < size; index++)
            // 如果要删除的元素不为null,则进行比较,使用equals()方法
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

// 方法是快速删除,删除跳过边界检查的方法,也不返回删除的元素值
private void fastRemove(int index) {
    // 少了一个越界的检查
    modCount++;
    // 如果index不是最后一位,则将index之后的元素往前挪一位
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    // 将最后一个元素删除,帮助GC
    elementData[--size] = null; // clear to let GC do its work
}

流程:

(1)找到第一个等于指定元素值的元素;

(2)快速删除;

fastRemove(int index)相对于remove(int index)少了检查索引越界的操作,因为源码中是通过循环遍历数组找的下标,不可能存在越界情况,可见jdk将性能优化到极致。

  • retainAll(Collection<?> c)方法,求两个集合的交集。
public boolean retainAll(Collection<?> c) {
    // 集合c不能为null
    Objects.requireNonNull(c);
    // 调用批量删除方法,这时complement传入true,表示删除不包含在c中的元素,这样就求出交集了
    return batchRemove(c, true);
}
/**
* 批量删除元素
* complement为true表示删除c中不包含的元素
* complement为false表示删除c中包含的元素
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    // 使用读写两个指针同时遍历数组
    // 读指针每次自增1,写指针放入元素的时候才加1
    // 这样不需要额外的空间,只需要在原有的数组上操作就可以了
    int r = 0, w = 0;
    boolean modified = false;
    try {
        // 遍历整个数组,如果c中包含该元素,则把该元素放到写指针的位置(以complement为准)
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // 正常来说r最后是等于size的,除非c.contains()抛出了异常
        if (r != size) {
            // 如果c.contains()抛出了异常,则把未读的元素都拷贝到写指针之后
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // 将写指针之后的元素置为空,帮助GC
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            // 新大小等于写指针的位置(因为每写一次写指针就加1,所以新大小正好等于写指针的位置)
            size = w;
            modified = true;
        }
    }
    // 有修改返回true
    return modified;
}

将集合与另一个集合求交集,整体逻辑比较简单的。通过读写指针进行操作,不用额外空间。注意complement为true,则将包含在c中的元素写入相应位置。这样就求出了交集,这里还要注意finally中的操作,异常与置空操作。

流程:

(1)遍历elementData数组;

(2)如果元素在c中,则把这个元素添加到elementData数组的w位置并将w位置往后移一位;

(3)遍历完之后,w之前的元素都是两者共有的,w之后(包含)的元素不是两者共有的;

(4)将w之后(包含)的元素置为null,方便GC回收;

  • removeAll(Collection<?> c)求两个集合的单方向差集,只保留当前集合中不在c中的元素,本质就是讲List中c集合中的元素都删除。
public boolean removeAll(Collection<?> c) {
    // 集合c不能为空
    Objects.requireNonNull(c);
    // 同样调用批量删除方法,这时complement传入false,表示保存不在c中的元素,这样就求出差集了
    return batchRemove(c, false);
}

逻辑和retainAll刚好相反,complement为false,保存不包含在C中的元素,这样就求出差集了,注意这里是单向差集

2.3.4 set()方法

set(int index, E element) 方法将index索引处的元素替换成element对象,返回被替换的旧元素。

/**
 * Replaces the element at the specified position in this list with
 * the specified element.
 *
 * @param index index of the element to replace
 * @param element element to be stored at the specified position
 * @return the element previously at the specified position
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E set(int index, E element) {
    // 检查下标索引是否越界
    rangeCheck(index);
    
    // 指定下标处的旧值
    E oldValue = elementData(index);
    // 指定下标处赋新的值
    elementData[index] = element;
    // 返回旧值
    return oldValue;
}

2.3.5 size()方法

返回集合中的元素个数。

/**
 * Returns the number of elements in this list.
 *
 * @return the number of elements in this list
 */
public int size() {
    return size;
}

2.3.6 indexOf(Object o) 方法

返回对象o在集合中第一次出现的位置的索引。

/**
 * Returns the index of the first occurrence of the specified element
 * in this list, or -1 if this list does not contain the element.
 * More formally, returns the lowest index <tt>i</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
 * or -1 if there is no such index.
 */
public int indexOf(Object o) {
    // 如果查找的元素为空
    if (o == null) {
        // 循环遍历数组
        for (int i = 0; i < size; i++)
            // 如果 i 位置的原素为空 
            if (elementData[i]==null)
                // 返回下标
                return i;
    // 否则,查找的元素不为空
    } else {
        // 循环遍历数组
        for (int i = 0; i < size; i++)
            // 找到第一个和要查找的元素相等的元素
            if (o.equals(elementData[i]))
                // 返回下标
                return i;
    }
    // 返回 -1.则表示没有找到
    return -1;
}

三、ArrayList的优缺点

1. 优点

  • 能够自动扩容,默认每次扩容为原来容量大小的1.5倍。
  • 根据下标遍历元素,效率高。
  • 根据下标访问元素,效率高。

2. 缺点

  • 线程不安全。
  • 在中间插入或删除元素的时候需要移动元素,效率低。

四、总结

  1. ArrayList的底层数据结构为数组(数组是一组连续的内存空间),默认容量为10,线程不安全,可以存储null值;
  2. ArrayList扩容条件,只要增加容量大于现有容量就会进行扩容,扩容量为原来的1.5倍,但是ArrayList不会进行缩容;
  3. ArrayList支持随机访问,通过索引访问元素极快,时间复杂度为O(1);
  4. ArrayList添加元素到尾部极快,平均时间复杂度为O(1);
  5. ArrayList添加元素到中间比较慢,因为要搬移元素,平均时间复杂度为O(n);
  6. ArrayList从尾部删除元素极快,时间复杂度为O(1);
  7. ArrayList从中间删除元素比较慢,因为要搬移元素,平均时间复杂度为O(n);
  8. ArrayList支持求并集,调用addAll(Collection<? extends E> c)方法即可;
  9. ArrayList中有求交集(retainAll)和求差集(removeAll),注意这里的差集是单向交集;
  10. ArrayList支持求交集,调用retainAll(Collection<? extends E> c)方法即可;
  11. ArrayList支持求单向差集,调用removeAll(Collection<? extends E> c)方法即可;