【Java集合】ArrayList源码分析

news2025/1/16 20:52:14

目录

一、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)方法即可;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/155871.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

flink规则引擎设计思路

在日常工作中我们经常收到一些诸如此类需求&#xff1a;“用户给点击了开屏广告&#xff0c;给用户下发私信”、“用户进入了推荐线&#xff0c;但在60秒内没有任何点击操作&#xff0c;弹框引导用户选择感兴趣的内容”、“用户点赞了某位作者的两篇以上的内容&#xff0c;但并…

C++入门----缺省参数和函数重载

C入门第一讲&#xff1a; 文章目录C入门第一讲&#xff1a;1.C关键字&#xff08;C98&#xff09;2.命名空间2.1命名空间的定义3.C的输入和输出4.缺省参数4.1缺省参数的概念4.2缺省参数的分类4.2.1全缺省参数4.2.2半缺省参数5.函数重载5.1函数重载的概念5.2C支持函数重载的原理…

在 SpringBoot 中 初步使用 MyBatis

这篇文章简单介绍如何初步使用MyBatis框架。MyBatis官网&#xff1a;mybatis – MyBatis 3 | 简介。本文中介绍MyBatis使用在SpringBoot中&#xff0c;Spring帮我们进行了管理&#xff0c;省去了获取sql的步骤。 什么是 MyBatis&#xff1f; MyBatis 是一款优秀的持久层框架&a…

XAML控件宽度为另一控件的一半、静态属性绑定、ObjectDataProvider

控件上当某些数据需要根据其他数据的变化而变化 很多时候&#xff0c;想让某个控件的宽度或者高度是另一个已有控件的一半&#xff0c;一开始打算使用ObjectDataProvider来实现&#xff0c;因为在控件上当某些数据需要根据其他数据的变化而变化时&#xff0c;可以使用ObjectDa…

Selenium实战【滑动验证码破解】【JAVA爬虫】

简介本文主要讲解&#xff0c;利用之前所学到的java selenium如何实战操作,浏览器控制鼠标&#xff0c;模拟人工操作滑动验证码。这里需要用javacv 的代码知识&#xff0c;用于计算图像中滑块需要移动的距离。实战目标网站&#xff1a;https://dun.163.com/trial/jigsaw操作流程…

【Java入门】Java注释和关键字

✅作者简介&#xff1a;CSDN内容合伙人、阿里云专家博主、51CTO专家博主、新星计划第三季python赛道Top1&#x1f3c6; &#x1f4c3;个人主页&#xff1a;hacker707的csdn博客 &#x1f525;系列专栏&#xff1a;Java入门 &#x1f4ac;个人格言&#xff1a;不断的翻越一座又一…

mycat个人详谈

项目背景 现在业务想实现不同的用户分别看到不同的数据库&#xff0c;领导说用mycat中间件试试。没有接触过这个东西&#xff0c;刚拿到手直接头皮发麻&#xff0c;现在搞定了&#xff0c;索性就记录一下 Mycat数据库分库分表中间件 简介 阿里大牛开发的开源免费产品&#x…

设计模式——抽象工厂模式

文章目录1. 抽象工厂模式的定义2. 抽象工厂模式的类图3. 抽象工厂模式的作用4. 抽象工厂模式的实现1. 抽象工厂模式的定义 提供一个接口&#xff0c;用于创建相关或依赖对象的家族&#xff0c;而不需要明确指定具体类。 抽象工厂允许客户使用抽象的接口来创建一组相关的产品&a…

OAuth 2.0简介

OAuth就是一种授权机制。数据的所有者告诉系统&#xff0c;同意授权第三方应用进入系统&#xff0c;获取这些数据。系统从而产生一个短期的、一定权限的令牌&#xff08;token&#xff09;&#xff0c;用来代替密码&#xff0c;供第三方应用使用。 流程 1.第三方客户端要求用…

【欧姆龙控制器NX1系列调研】

欧姆龙控制器NX1系列调研NX系列 NX1 CPU单元NX系列-EtherNet/IP耦合器单元NX系列-系统单元NX系列通信接口单元NX系列数字输入单元NX系列模拟量输入单元NX系列模拟量输出入单元NX系列 高速模拟量模块NX系列IO-Link 主站单元温度控制/温度输入单元位置接口单元NX系列 安全控制单元…

【algorithm】算法基础课---排序算法(附笔记 | 建议收藏)

&#x1f680;write in front&#x1f680; &#x1f4dd;个人主页&#xff1a;认真写博客的夏目浅石. &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd; &#x1f4e3;系列专栏&#xff1a;AcWing算法学习笔记 &#x1f4ac;总结&#xff1a;希望你看完…

CSS入门四、浮动

零、文章目录 文章地址 个人博客-CSDN地址&#xff1a;https://blog.csdn.net/liyou123456789个人博客-GiteePages&#xff1a;https://bluecusliyou.gitee.io/techlearn 代码仓库地址 Gitee&#xff1a;https://gitee.com/bluecusliyou/TechLearnGithub&#xff1a;https:…

mybatis参数配置和日志详解

生命周期和作用域是至关重要的&#xff0c;因为错误的使用会导致非常严重的并发问题 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的&#xff0c;SqlSessionFactory的实例可以通过 SqlSessionFactoryBuilder 获得&#xff0c;而 SqlSessionFactoryBuild…

【stl容器--实践操作】

目录&#xff1a;前言一、string&#xff08;一&#xff09;初始化 、 赋值 、 拼接&#xff08;二&#xff09;单个字符的访问和修改[]、at()&#xff08;三&#xff09;插入和删除insert 、erase&#xff08;四&#xff09;查找和替换find、rfind、replace&#xff08;五&…

代码随想录-47-101. 对称二叉树

目录前言题目1.递归思路2. 本题思路分析&#xff1a;3. 算法实现4. pop函数的算法复杂度5. 算法坑点前言 在本科毕设结束后&#xff0c;我开始刷卡哥的“代码随想录”&#xff0c;每天一节。自己的总结笔记均会放在“算法刷题-代码随想录”该专栏下。 代码随想录此题链接 题目…

FileNotFoundError: [Errno 2] No such file or directory VSCode Python插件的工作路径

文章目录1 前言2 Python主进程工作路径3 解决参考1 前言 在VSCode中&#xff0c;你是否遇到这样的情况 Traceback (most recent call last):File "......*.py", line 1, in <module>with open(filepath, r) as f: FileNotFoundError: [Errno 2] No such file…

提面优秀成功上岸浙大MBA的一点经验——专业管理思维很重要

本人的学历背景比较一般&#xff0c;就是个普通一本&#xff0c;但大学时为了挣学分&#xff0c;参加了不少的活动&#xff0c;还考了教师资格证&#xff0c;并且还去山区支教过3个月&#xff0c;支教一开始也是为了学分&#xff0c;但是通过与孩子们的相处&#xff0c;其实自己…

http相关(更新中)

概念 HTTP&#xff08;hypertext transport protocol&#xff09;协议也叫超文本传输协议&#xff0c;这个协议详细规定了浏览器和万维网服务器之间互相通信的规则。HTTPS更安全&#xff1b;无状态&#xff0c;cookie解决了无状态的问题 请求过程 客户端发送给服务器的称为“…

深度聚类方法之跨实例引导的对比聚类(Cross-instance guided Contrastive Clustering,C3)

深度聚类方法之跨实例引导的对比聚类(Cross-instance guided Contrastive Clustering,C3) 1.参考文献 《C3: Cross-instance guided Contrastive Clustering》2.跨实例引导的对比聚类(Cross-instance guided Contrastive Clustering,C3) 该方法是在对比聚类(Contrastive Clus…

Linux系统编程——信号

1.信号 信号共性: 简单、不能携带大量信息、满足条件才发送。信号的特质: 信号是软件层面上的“中断”。一旦信号产生&#xff0c;无论程序执行到什么位置&#xff0c;必须立即停止运行&#xff0c;处理信号&#xff0c;处理结束&#xff0c;再继续执行后续指令。所有信号的产…