一、ArrayList源码解读

news2024/12/23 23:32:52

ArrayList源码

一、前言

ArrayList在日常的开发中使用频率非常高,但是JDK是如何去设计ArrayList的,这就需要我们好好去了解底层实现原理,这样使用起来才能做到心中有数;当然,还能应付面试。本篇文章会围绕ArrayList的核心方法进行源码解读,希望对你有所帮助。
在这里插入图片描述

二、提取官方文档核心技术点

在这里插入图片描述

  1. ArrayList是由可调整大小数组实现。并允许添加所有元素,包括 null
  2. ArrayList实现 List 接口之外,自己还提供了操作内部用于存储列表的数组大小的方法。(此类大致等同于 Vector,只是它是不同步的)
  3. size、isEmpty、get、set、iterator 和 listIterator 操作在常量时间内运行添加操作在摊销常量时间内运行,即添加 n 个元素需要 O(n) 时间。所有其他操作都以线性时间运行(粗略地说)
  4. 每个 ArrayList 实例都有一个 容量。容量是用于在列表中存储元素的数组的大小。它始终至少与列表大小一样大。当元素添加到 ArrayList 时,其容量会自动增长。除了添加元素具有恒定的摊销时间成本这一事实之外,没有指定增长策略的详细信息。
  5. 应用程序可以在使用 ensureCapacity 操作添加大量元素之前增加 ArrayList 实例的容量。这可能会减少增量重新分配的数量。请注意,此实现不同步。
  6. 如果多个线程同时访问 ArrayList 实例,并且至少有一个线程在结构上修改了该列表, 则必须在外部同步该列表。(结构修改指的是添加或删除一个或多个元素,或显式调整后备数组大小的任何操作;仅设置元素的值不是结构修改。这通常是通过在自然封装列表的某个对象上进行同步来实现的。如果不存在此类对象,则应使用该方法**“ Collections.synchronizedList 包装”**列表。最好在创建时执行此操作,以防止意外地对列表进行不同步访问:
    List list = Collections.synchronizedList(new ArrayList(…));
  7. 此类返回的迭代器 iterator 并且 listIterator 方法是 快速失败的:如果在创建迭代器后的任何时间对列表进行结构修改,则除了通过迭代器自己的 remove OR add 方法之外,迭代器将抛出 ConcurrentModificationException.因此,面对并发修改,迭代器会快速而干净地失败,而不是冒着在未来不确定的时间出现任意、非确定性行为的风险。
  8. 请注意,无法保证迭代器的快速故障行为,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException 。因此,编写一个依赖于此异常来判断正确性的程序是错误的;迭代器的快速故障行为应仅用于检测错误

三、前置知识

在ArrayList中在进行扩容、删除值、增加值的操作都会使用到数组复制操作,在源码中会涉及到两种:

  • System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)-System类提供的native方法
public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);// 表示从src的srcPos位置拷贝length个值到dest从destPos开始的位置
  • Arrays.copyOf(elementData, newCapacity)- Arrays类提供的方法
public static <T> T[] copyOf(T[] original, int newLength) {// 传递一个original数组和一个newLength最后返回一个新数组
    return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
  @SuppressWarnings("unchecked")
  T[] copy = ((Object)newType == (Object)Object[].class)
    ? (T[]) new Object[newLength] // 直接new一个newLength长度的数组
    : (T[]) Array.newInstance(newType.getComponentType(), newLength);
  System.arraycopy(original, 0, copy, 0,
                   Math.min(original.length, newLength));// 使用arraycopy执行copy操作
  return copy;// 返回新数组
}

四、继承体系

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

在这里插入图片描述

从上面的继承关系中可以看出,ArrayList至少有以下几大特性:

  1. 实现Cloneable接口,说明是支持克隆的
  2. 继承AbstractList得到AbstractList封装的通用逻辑
  3. 实现RandomAccess接口,说明支持快速随机访问,下面会介绍
  4. 实现Serializable接口,说明支持序列化和反序列化

五、RandomAccess接口

public interface RandomAccess {
}

淦。发现真的只是一个接口然后什么都没了,看看官方解释:

List实现使用RandomAccess标记接口表明它们支持快速(通常是恒定时间)随机访问。此接口的主要目的是允许通用算法在应用于随机或顺序访问列表时改变其行为以提供良好的性能
用于操作随机访问列表(例如ArrayList )的最佳算法在应用于顺序访问列表(例如LinkedList )时会产生二次行为。鼓励通用列表算法在应用算法之前检查给定列表是否是此接口的实例,如果将其应用于顺序访问列表会提供较差的性能,并在必要时更改它们的行为以保证可接受的性能。
众所周知,随机访问和顺序访问之间的区别通常是模糊的。例如,如果某些List实现变得很大,则提供渐近线性访问时间,但在实践中访问时间是恒定的。这样的List实现一般应该实现这个接口。根据经验,如果对于类的典型实例,如果出现以下循环,则List实现应该实现此接口:比如当前要讲的ArrayList使用下面的迭代方式才快。
for (int i=0, n=list.size(); i < n; i++)
list.get(i);

运行速度比这个循环快:【注意:LinkedList使用这种遍历方式更快,因为LinkedList没有继承RandomAccess接口
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();

可以看看ArrayList和LinkedList不同遍历方式的耗时差距:

ArrayList traverse : 13 【普通遍历方式】
ArrayList traverse by iterator : 31 【迭代器方式】
ArrayList traverse by foreach : 34 【foreach方式】

LinkedList traverse : 6422 【普通遍历方式】
LinkedList traverse by iterator: 19【迭代器方式】
LinkedList traverse by foreach : 7【foreach方式】

六、核心变量

// 默认容量大小为10
private static final int DEFAULT_CAPACITY = 10;
// 【使用有参构造的方式,但是initialCapacity为0的时候】对应的空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量下【使用无参构造的方式】指向的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存放数据的数组
transient Object[] elementData; // non-private to simplify nested class access
// 当前list中存放值的个数
private int size;
// 支持的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

七、构造方法

无参构造

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;// 指向默认的空数组,注意:DEFAULTCAPACITY_EMPTY_ELEMENTDATA和上面的EMPTY_ELEMENTDATA在扩容的时候就能看出这两个之间的区别
}

有参构造

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];// 直接new一个长度为initialCapacity的数组
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;// 标记空数组,elementData指向EMPTY_ELEMENTDATA
    } else {// 如果initialCapacity小于0直接抛异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

// 参数为一个Collection
public ArrayList(Collection<? extends E> c) {
  elementData = c.toArray();// elementData直接指向一个数组
  if ((size = elementData.length) != 0) {
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    // 源码中已经说了,c.toArray返回的值有可能不是Object[]类型
    if (elementData.getClass() != Object[].class)
      elementData = Arrays.copyOf(elementData, size, Object[].class);// 将elementData转换为Object[]类型
  } else {// 说明elementData的size为0
    this.elementData = EMPTY_ELEMENTDATA;// 直接替换为EMPTY_ELEMENTDATA
  }
}

八、核心方法

添加值

add(E e)往末尾添加值

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 1.首先对容量进行检查,看看当前添加一个值到容器中,容量够不够,不够的话需要进行扩容
    elementData[size++] = e;// 2.添加值,能执行到这里,说明容量一定是够的;而且可以看出,add(E e)默认是在列表尾部进行添加操作
    return true;
}

反正就两步逻辑:

  1. 确保数组容量是够的【利用扩容机制】
  2. 将值添加到数组的末尾

add(int index, E element)指定位置插入值

public void add(int index, E element) {
    rangeCheckForAdd(index);// 1.检查index是否合法
    ensureCapacityInternal(size + 1);  // 2.执行容量检测或扩容操作,并让modCount++
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);// 3.执行数组copy操作,把index位置给腾出来
    elementData[index] = element;// 4.替换值操作
    size++;// 5.增加size
}

private void rangeCheckForAdd(int index) {
  if (index > size || index < 0)
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

从源码中可以看出,在指定位置添加值会涉及到数组的copy操作,就意味着比较耗性能

addAll(Collection<? extends E> c)将一个容器的值全部添加到ArrayList的末尾

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();// 转换为数组
    int numNew = a.length;
    ensureCapacityInternal(size + numNew); // 确保数组容量够用
    System.arraycopy(a, 0, elementData, size, numNew);// 执行数组copy操作
    size += numNew;// 更新size
    return numNew != 0;
}

addAll(int index, Collection<? extends E> c)将一个容器的值全部添加到ArrayList的index位置往后

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);// 1.检查index是否合法

    Object[] a = c.toArray();// 2.得到数组
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // 3.确保数组的长度够用

    int numMoved = size - index;// 4.计算移动的个数
    if (numMoved > 0)// 5.将index往后腾出numNew个位置
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);

    System.arraycopy(a, 0, elementData, index, numNew);// 6.将a数组的值copy到elementData的index位置往后
    size += numNew;
    return numNew != 0;
}

扩容机制

// 扩容操作
private void ensureCapacityInternal(int minCapacity) {// minCapacity就是需要的容量大小
  ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 1.计算需要的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
  if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 能执行到这个分支,说明是使用的无参构造的方式创建ArrayList,并且是第一次调用add或addAll方法
    return Math.max(DEFAULT_CAPACITY, minCapacity);//取最大值,DEFAULT_CAPACITY默认为10,然后得到minCapacity和10之间的最大值,因为minCapacity在addAll的时候可能会大于10
  }
  return minCapacity;// 返回需要的容量
}

// 2.扩容操作的判断
private void ensureExplicitCapacity(int minCapacity) {
  modCount++;// 防止迭代器修改,fail-fast机制会用到,就相当于纪录修改的版本号
  if (minCapacity - elementData.length > 0)// 说明数组容量确实不够用了,需要进行扩容
    grow(minCapacity);// 真正执行扩容操作
}

// 3.真正执行扩容操作
private void grow(int minCapacity) {
  int oldCapacity = elementData.length;// 得到旧的容量
  int newCapacity = oldCapacity + (oldCapacity >> 1);// 等价于1.5 * oldCapacity,也就是扩容为原来的1.5倍
  if (newCapacity - minCapacity < 0)// 新容量还是小于需要的容量;上面不是已经扩容了,为啥还小于需要的容量呢???假设一种情况就是在创建ArrayList的时候使用有参构造的方式,将容量设置为0,如果第一次执行add操作的时候,会执行到这里,就会发现上面使用1.5倍计算出来的newCapacity还是为0,显然newCapacity不能为0,所以这里就让newCapacity变为1执行后续的扩容操作
    newCapacity = minCapacity;
  if (newCapacity - MAX_ARRAY_SIZE > 0)// newCapacity已经超过最大的容量了
    newCapacity = hugeCapacity(minCapacity);
  // 创建新数组并将elementData中的值copy到新数组中,elementData指向新的数组
  elementData = Arrays.copyOf(elementData, newCapacity);
}

// 防止容量超过最大值
private static int hugeCapacity(int minCapacity) {
  if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();// 直接超过最大值,抛出OOM
  return (minCapacity > MAX_ARRAY_SIZE) ?
    Integer.MAX_VALUE :// 从这里可以看出,ArrayList支持最大的容量为Integer.MAX_VALUE
  MAX_ARRAY_SIZE;
}

扩容机制有核心的三个步骤:

  1. 计算当前需要的容量大小
  2. 判断是否真正需要进行扩容,并执行modCount++
  3. 执行真正的扩容操作,扩容为原来数据长度的1.5倍,并且会判断长度是否超过容量的最大值,最后使用elementData = Arrays.copyOf(elementData, newCapacity);创建新数组并copy旧数组所有的值到新数组中

删除值

remove(int index)-删除指定位置的值

public E remove(int index) {
    rangeCheck(index);// 1.合法性检查

    modCount++;// 2.fail-fast机制
  
    E oldValue = elementData(index);// 3.根据index位置拿到oldValue,时间复杂度为O(1),因为直接从数组下标取值
    int numMoved = size - index - 1;// 4.计算需要移动数据个数
    if (numMoved > 0)// 说明被删除数据不是末尾的数据
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);// 通过copy的方法去删除值
    elementData[--size] = null; // 让末尾位置置空,for gc

    return oldValue;
}


E elementData(int index) {
  return (E) elementData[index];// 直接就得到数组下标位置的值
}

从源码中可以知道,该方法的时间复杂度为O(N),主要是涉及到数组拷贝

remove(Object 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;
}

// 删除index位置的值
private void fastRemove(int index) {
  modCount++;
  int numMoved = size - index - 1;// 计算移动元素的个数
  if (numMoved > 0)// 不是数组最后一个位置
    System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);// 数组copy操作,将index + 1往后所有的数据copy到index往后
  elementData[--size] = null; // clear to let GC do its work
}

从源码中可以知道,该方法的时间复杂度为O(N)

removeAll(Collection<?> c)-从此列表中删除指定集合中包含的所有元素


public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)// 注意,这里的complement是false
                elementData[w++] = elementData[r];// 说明在c中不包含elementData中的元素
            // 如果c中包含elementData中的元素,w不进行++操作,说明需要被删除
    } finally {
        if (r != size) {// 说明出现异常了
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);// 把elementData后面还没有遍历的值直接copy到r开始位置
            w += size - r;
        }
        if (w != size) {// 说明w后面的都应该需要被删除的,w前面的都是不需要被删除的
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;// 用于GC操作
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}


从源码可以看出,使用双指针的方式实现删除,一个w写指针,一个r读指针,w指针用于保存不需要被删除的值,最后w之前的都是需要保留的,w之后的都是需要删除 ,而且还能保持元素的相对顺序。

retainnAll(Colllection<?> c)-仅保留此列表中包含在指定集合中的元素

public boolean retainAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, true);
}

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)// 注意,这里的complement是true
                elementData[w++] = elementData[r];// 说明在c中包含elementData中的元素,需要删除被保存
            // 如果c中不包含elementData中的元素,直接删除
    } finally {
        if (r != size) {// 说明出现异常了
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);// 把elementData后面还没有遍历的值直接copy到r开始位置
            w += size - r;
        }
        if (w != size) {// 说明w后面的都应该需要被删除的,w前面的都是不需要被删除的
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;// 用于GC操作
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

修改值

public E set(int index, E element) {
    rangeCheck(index);// 1.检查index位置是否合法

    E oldValue = elementData(index);// 2.得到index位置的值
    elementData[index] = element;// 3.直接替换为element
    return oldValue;// 4.返回旧值
}

从源码中可以知道,该方法的时间复杂度为O(1)

获取值

get(int index)得到指定位置的值

public E get(int index) {
    rangeCheck(index);// 校验index的合法性

    return elementData(index);// 查询index位置的值
}


E elementData(int index) {
  return (E) elementData[index];// 再简单不过了
}

从源码中可以知道,该方法的时间复杂度为O(1)

是否包含某值

public boolean contains(Object o) {
  return indexOf(o) >= 0;
}
  
public int indexOf(Object o) {// 时间复杂度为O(N)
  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;
}

九、迭代器

public Iterator<E> iterator() {
    return new Itr();
}

/**
 * An optimized version of AbstractList.Itr
 */
private class Itr implements Iterator<E> {
    int cursor;    // 游标控制
    int lastRet = -1; // 返回-1表示没有数据了
    int expectedModCount = modCount;// 记录数据版本,防止使用迭代器的时候,其他线程删除增加数据

    Itr() {}

    public boolean hasNext() {
        return cursor != size;// 判断cursor是否到末尾,到底末尾说明没数据了
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();// 检查modCount标记
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        // 得到数组
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)// 游标位置已经比数组长度还长,说明有其他线程并发remove了数据,直接抛出异常
            throw new ConcurrentModificationException();
        cursor = i + 1;// 游标往下走一个
        return (E) elementData[lastRet = i];// 返回值
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();// 检查modCount

        try {
            ArrayList.this.remove(lastRet);// 进行删除操作
            cursor = lastRet;// 游标需要回到之前的位置(因为马上删除一个值,下一个数的位置就是当前被删值的位置)
            lastRet = -1;
            expectedModCount = modCount;// 因为执行了删除了操作,所以modCount已经递增了,需要重新设置一下
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

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

十、其他

clear()-清除整个容器

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;// 每个位置变为null,使GC回收

    size = 0;// 重置为0
}

sort(Comparator<? super E> c)-排序操作

@Override
@SuppressWarnings("unchecked")
public void sort(Comparator<? super E> c) {
    final int expectedModCount = modCount;
    Arrays.sort((E[]) elementData, 0, size, c);// 排序操作
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

trimToSize()-把数组长度缩小为当前元素个数

将此 ArrayList 实例的容量修剪为列表的当前大小,应用程序可以使用此操作来最小化 ArrayList 实例的存储

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);// 直接得到一个新数组,长度为siz
    }
}

序列化相关

序列化

// 源码
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    int expectedModCount = modCount;
    s.defaultWriteObject();
    s.writeInt(size);// size长度
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);// 只写入size元素的数据,这样就能节省空间,不需要把整个数组进行序列化,也没必要这么做
    }
    // ArrayList被并发修改,抛出ConcurrentModificationException
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

// 使用
public static void testWrite() {
  ArrayList<User> users = new ArrayList<>();
  users.add(new User("zs", 12));
  users.add(new User("luopo", 18));
  FileOutputStream fos = null;
  ObjectOutputStream oos = null;
  try {
    fos = new FileOutputStream("/Users/apple/Desktop/io_test/user.ser");
    oos = new ObjectOutputStream(fos);
    oos.writeObject(users);// 这个方法最后就会调用上面的writeObject方法
    oos.flush();
  } catch (IOException e) {
    e.printStackTrace();
  } finally {
    try {
      oos.close();
      fos.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

反序列化

// 源码
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    s.defaultReadObject();
  
    s.readInt(); // ignored

    if (size > 0) {
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        ensureCapacityInternal(size);// 确保ArrayList的空间足够

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

// 使用
public static void testRead() {
  List<User> users = new ArrayList<>();
  users.add(new User("zs", 12));
  users.add(new User("luopo", 18));
  FileInputStream fis = null;
  ObjectInputStream ois = null;
  try {
    fis = new FileInputStream("/Users/apple/Desktop/io_test/user.ser");
    ois = new ObjectInputStream(fis);
    ArrayList<User> readUsers = (ArrayList<User>) ois.readObject();// 最后会执行到上面源码readObject方法
    System.out.println(readUsers);
  } catch (Exception e) {
    e.printStackTrace();
  } finally {
    try {
      ois.close();
      fis.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

ArrayList提供的序列化方式可以节省存储空间和IO传输时间,因为ArrayList底层是一个数组,如果只有add一个元素,数组是10,剩下9个位置都是null,所以,剩下9个位置的数据就没必要进行序列化

十一、常见面试题目

ArrayList默认容量大小是多少???

默认为:10

ArrayList多久初始化数组???

  • 有参构造

  • 参数小于0,直接抛出异常

    • 参数为0,数组直接指向一个空对象{}
    • 参数不为0,数组直接初始化new一个长度为传入参数的数组
  • 无参构造

    • 在第一次调用add方法的时候,会进行容量判断,然后使用10为默认数组长度进行数组的初始化

ArrayList扩容机制???

在每次添加add的时候,都会去校验当前容器中容量够不够,如果不够就需要进行扩容操作:

  1. 计算新容量大小,默认为1.5 * oldCap(需要校验容量的合法性,比如是不是超过了Integer.MAX_VALUE:2 ^ 31 - 1)
  2. elementData = Arrays.copyOf(elementData, newCapacity),执行数组的扩容和copy操作

Arrays.copyOf()和System.arraycopy()的对比???

  • System.arraycopy()非常灵活, 可以指定从src的哪个索引处开始拷贝, 可以指定拷贝的长度, 还可以指定从dst的哪个索引处开始接收src的元素; 相比之下, Arrays.copyOf()是从src[0]开始拷贝的, 也是从dst[0]开始接收的, 而且也没法指定拷贝的长度

  • System.arraycopy()是native方法, 不是由java语言实现的; Arrays.copyOf()是由java语言实现的, 不过底层是基于System.arraycopy()方法

ArrayList如何删除一个值???

  1. 校验index位置是否合法
  2. 通过System.copy执行删除操作(实际上还是移动数组上各个元素来完成删除操作)

ArrayList是否是线程安全的???

肯定不是啊,可以使用synchronized进行上锁

ArrayList建议使用什么遍历方式???LinkedList呢???

  • 由于ArrayList继承了RandomAccess接口,说明使用直接索引遍历会比迭代器方式快;

  • LinkedList则反之,使用迭代器方式更快(因为LinkedList使用的index,会从头遍历查找index位置,从而消耗性能)

十二 、总结

看了ArrayList的源码,这里可以进行几个简单的总结:

功能角度

ArrayList是一个Java的Collection,可以存放null和其他引用类型的值,并且是顺序存放的

底层角度

ArrayList底层是利用数组进行实现,数组默认长度为10,每次扩容为原来长度的1.5倍;由于底层使用的是数组,所以对于值的删除、中间插入需要进行数组的copy操作,需要消耗一定性能。但是,对于获取值get(i)的操作非常快,因为直接从数组下标取值。

线程安全角度

ArrayList是线程不安全的,想要使用一个线程安全的List,有如下两种方式:

  1. 使用线程安全的Vector
  2. 使用Collections.synchronizedList(new ArrayList(…));进行包装
  3. 使用CopyOnWriteArrayList

在使用迭代器进行迭代遍历的时候,如果此时出现多线程并发执行的add、remove等改变ArrayList结构的方法会导致抛出 ConcurrentModificationException异常,也就是所谓的fail-fast机制

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

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

相关文章

王道操作系统网课笔记合集

介绍 操作系统是什么&#xff1f; 计算机结构大概分为四层&#xff1a; 用户应用程序操作系统硬件 操作系统是一类系统软件&#xff0c;调度硬件资源&#xff0c;合理分配管理软件&#xff08;因此操作系统又被称作资源管理器&#xff08;resource manager&#xff09;&…

简洁而优美的结构 - 并查集 | 一文吃透 “带权并查集” 不同应用场景 | “手撕” 蓝桥杯A组J题 - 推导部分和

&#x1f49b;前情提要&#x1f49b; 本章节是每日一算法的并查集&带权并查集的相关知识~ 接下来我们即将进入一个全新的空间&#xff0c;对代码有一个全新的视角~ 以下的内容一定会让你对数据结构与算法有一个颠覆性的认识哦&#xff01;&#xff01;&#xff01; ❗以…

【Unity 3D 从入门到实践】Unity 3D 预制体

目录 一&#xff0c;预制体介绍 二&#xff0c;创建预制体 三&#xff0c;实例化预制体 一&#xff0c;预制体介绍 预制体是 Unity 3D 提供的保存游戏对象组件和属性的方法&#xff0c;通过预制体可以快速的实例化挂载不同组件的游戏对象&#xff0c;从而减少开发难度&…

使用光隔离的调制器在电机控制中进行安全、准确的隔离电流传感

介绍 在工业电机或伺服控制应用中&#xff0c;准确的电流测量是控制回路的一部分。目前的测量不仅需要尽可能准确&#xff0c;还需要安全可靠。 工业电机或伺服控制系统通常包含高压&#xff0c;在过流或短路等故障事件中&#xff0c;这些情况需要快速检测和整流&#xff0c…

Android开发基础

文章目录前言工程项目结构hello world界面布局代码操作新页面页面间跳转简单计算器的实现思路前端控件传递数据后端实现逻辑两个Activity之间传值发送数据返回数据SQLite简单使用利用语句写在后面写在后面前言 安卓(Android)是一种基于Linux内核的开源操作系统 使用java、kot…

不就是性能测试吗?竟让我一个月拿了8个offer,其中两家都是一线大厂

随着互联网的发展&#xff0c;单机软件的逐渐减少&#xff0c;系统从单机步入“云”时代&#xff0c;软件系统功能和规模也越来越庞大&#xff0c;盗版也越来越难&#xff0c;用户规模也越来越大&#xff0c;企业盈利随之爆发式地增长。 随着用户数量的增多&#xff0c;系统稳…

Chrome浏览器修改用户资料(User Data)的存放位置

2022.12.13一、 原先采用的在快捷方式中修改目标的方法&#xff0c;没有效果。二、创建链接1. 复制2. 删除3. 创建链接mklink参考用于缓解C盘压力&#xff0c;将浏览器用户数据存放于其他的指定位置。简单记录一下操作步骤。 其中用户数据可以如此查找&#xff0c;在浏览器地址…

数字虚拟人发展简史

虚拟人的发展史就是技术的发展史 作为元宇宙时代的基石&#xff0c;虚拟人的发展历史与制作技术的进步高度相关。在元宇宙概念中&#xff0c;未来每个用户都将依托虚拟人作为自己的化身进入虚拟世界中探索&#xff0c;要达成这个目的&#xff0c;就要求数字虚拟人不仅拥有人的…

java计算机毕业设计ssm智慧消防维保系统后端设计与实现3cmg0(附源码、数据库)

java计算机毕业设计ssm智慧消防维保系统后端设计与实现3cmg0&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都…

【毕业设计】微信小程序校园跑腿系统 校园跑腿小程序 校园跑腿微信小程序

一、前言 大学是一个小社会&#xff0c;我们在学校学习到专业知识的时候&#xff0c;有会遇到很多形形色色的任务&#xff0c;但最重要的依旧是社会经历。很多大学生都会想着在大学闯出一片新天地&#xff0c;所以他们往往会选择自己或者带上志同道合的朋友来一起创业。一次好…

泛微京桥通集成SAP,让采购流程闭环、业务管理一体化

SAP作为强大的业务处理平台&#xff0c;推动着组织采购管理走向数字化。其中的SAP MM&#xff08;MaterialManagement&#xff09;&#xff08;物料管理&#xff09;涉及到物料管理全过程&#xff0c;主要包括&#xff1a;物料需求计划、物料主数据、采购、供应商评价、库存管理…

简单聊聊 WebSocket

简单聊聊 WebSocket 1、简介 ​  WebSocket 是一种在单个 TCP 连接上进行 全双工 通信的协议。 扩展&#xff1a; ​  WebSocket 与 Socket 的区别 WebSocket 是应用层的一个通信协议&#xff1b;Socket 是对 TCP&#xff08;或UDP&#xff09;抽象封装的一组接口&#xf…

浅谈责任链设计模式在框架源码中的运用

写在前面&#xff0c;该篇博文为我在部门的技术分享&#xff0c;所以文字记录不是特别详细。本文更像是一个大纲&#xff0c;由浅入深讲解责任链设计模式。 浅谈责任链设计模式在框架源码中的运用一、分享目的二、简单介绍三、逐个拆解四、源码环节1、Tomcat2、Spring MVC3、Sp…

【MySQL基础】什么是MySQL约束?什么是主键约束?

目录 一、什么是MySQL约束&#xff1f; 二、MySQL约束有什么作用&#xff1f; 三、MySQL约束常见七大类&#xff1f; 主键约束(primary key) PK 1.概念 2.主键约束的相关操作 添加单列主键 添加多列主键(联合主键) 通过修改表结构添加主键 删除主键 &#x1f49f; 创…

【Notebook系列第十三期】基于多模型推理任务的车辆特征识别

在之前的课程中&#xff0c;我们分享的推理任务大部分都只由一个模型构成&#xff0c;但在真实的业务场景下&#xff0c;往往需要我们将多个模型放在一起去运行&#xff0c;才能获取到这个任务的最终预期结果。 因此&#xff0c;本次分享将通过一个简单的示例演示如何使用 Ope…

12月13日(第11天)

腾讯云上通过面板开放端口&#xff0c;不起作用&#xff0c;解决办法&#xff1a;手动在服务器上开放&#xff0c;参考文章地址&#xff1a;在腾讯云控制台下配置防火墙端口无效 直接关闭防火墙是最简单的&#xff0c;systemctl stop firewalld&#xff0c;然后再再腾讯云上打开…

解决端口被占用的方法(查看端口和关闭端口)

目录 前言必读 一.简介 二、解决办法 1.查看该端口是否被占用 2.关闭占用该端口的进程 前言必读 读者手册&#xff08;必读&#xff09;_云边的快乐猫的博客-CSDN博客 一.简介 在使用各种端口时候经常会出现端口被占用导致代码程序无法执行或者执行错误。一般都是由于该端…

DataGear 4.3.0 发布,数据可视化分析平台

DataGear 4.3.0 发布&#xff0c;增强图表和看板功能&#xff0c;具体更新内容如下&#xff1a; 新增&#xff1a;看板模板新增dg-dashboard-auto-render属性&#xff0c;用于控制看板是否自动渲染&#xff1b;新增&#xff1a;看板模版新增dg-dashboard-code属性&#xff0c;…

Spring Batch 批处理-作业参数设置与获取

引言 书接上篇Spring Batch批处理-作业Job简介&#xff0c;上篇带小伙伴们了解色作业Job对象&#xff0c;那这篇就看一下作业参数是啥一回事&#xff0c;同时要怎么设置参数并获取参数的。 JobParameters 前面提到&#xff0c;作业的启动条件是作业名称 识别参数&#xff0…

MQ消息中间件

MQ消息中间件1、应用场景1、流量削峰2、应用解耦3、异步处理2、MQ分类1、ActiveMQ2、kafka3、RocketMQ4、RabbitMQ3、RabbitMQ详解3.1、核心概念3.2 RabbitMQ基本知识点3.3消息发布确认3.4 交换机1、应用场景 1、流量削峰 将访问的大流量通过消息队列做缓冲&#xff0c;我们可…