当前位置: 首页 > news >正文

Interview系列 - 06 Java | ArrayList底层源码分析 | 遍历集合时如何删除集合中的元素

文章目录

    • 1. 底层源码分析
      • 01. 属性
      • 02. 构造方法
      • 03. 在数组的末尾添加元素 add(E e)
      • 04. 在数组的指定位置添加元素 add(int index, E element)
      • 05. 替换指定位置的元素 set(int index, E element)
      • 06. 获取指定索引位置处的元素 get(int index)
      • 07. 删除指定位置的元素 remove(int index)
      • 08. 把集合所有数据转换成字符串 toString()
      • 09. 迭代器 iterator() 方法
    • 2. 如何遍历集合并删除List中的元素?
      • 01. 普通 for 循环删除(不可靠)
      • 02. 普通 for 循环提取变量删除(抛异常)
      • 03. 普通 for 循环倒序删除(可靠)
      • 04. 增强 for 循环删除(抛异常)
      • 05. 迭代器循环迭代器删除(可靠)
      • 06. 迭代器循环集合删除(抛异常)
      • 07. 集合 forEach 方法循环删除(抛异常)

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

(1) ArrayList底层的数据结构为动态数组,数组一旦初始化长度就不可以发生改变,而ArrayList是可调整大小的数组实现。

(2) ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。

(3) ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。

增删慢:每次删除元素,都需要更改数组长度、拷贝以及移动元素位置。
查询快:由于数组在内存中是一块连续空间,因此可以根据地址+索引的方式快速获取对应位置上的元素。

1. 底层源码分析

01. 属性

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

    // 在创建数组时,若没有指明数组的长度,则默认初始化容量为10
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组,使用带参构造方法new ArrayList(0)时使用:elementData = EMPTY_ELEMENTDATA
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 空数组,使用空参构造方法new ArrayList()时使用:elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    // 与EMPTY_ELEMENTDATA的区别:添加第一个元素时会初始化为默认容量(DEFAULT_CAPACITY)大小
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 真正存放元素的地方,数组的容量就是该数组的长度
    transient Object[] elementData; 

    // 真正存放元素的个数
    private int size;
}

02. 构造方法

空参构造方法:

// 空参构造器:构造一个初始化容量为10的空数组 
// 不传初始容量,初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,该数组会在添加第一个元素的时候扩容为默认的大小10
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

带参构造方法:

// 构造一个指定容量大小的数组
// 如果传入的容量为0,则 elementData = EMPTY_ELEMENTDATA
// 如果传入的容量大于0,则 elementData = new Object[initialCapacity]
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    }
}

带参构造方法:

public ArrayList(Collection<? extends E> c) {
    // 传入集合并初始化elementData,这里会使用拷贝把传入集合的元素拷贝到elementData数组中
    // 如果元素个数为0,则初始化为EMPTY_ELEMENTDATA空数组。
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

03. 在数组的末尾添加元素 add(E e)

在这里插入图片描述

public boolean add(E e) {
    // 确保数组能否存放添加的元素
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

① 确保数组的长度加1后内存是充足的,就是说保证能够数组还能存放一个元素

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

② 计算添加元素后所需的最小容量

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果调用的是空参数构造函数,则第一次调用add()方法时会比较默认初识容量10和所需最小容量的大小
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 返回添加元素后所需的最小容量
    return minCapacity;
}

③ 确保数组有足够的容量添加元素

private void ensureExplicitCapacity(int minCapacity) {
    // 集合的快速失败机制
    modCount++;

    // 如果数组的容量小于所需的最小容量,则扩容
    // 使得存在剩余的内存存放要添加的元素
    if (elementData.length < minCapacity)
        grow(minCapacity);
}

④ 增加容量,以确保它至少可以容纳最小容量参数指定的元素数。

private void grow(int minCapacity) {
    // 获取当前数组的容量
    int oldCapacity = elementData.length;
    // 先将数组容量扩容1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果扩容后还不满足需求,则直接扩容到数组添加元素时所需的最小容量
    // 如果调用空参数构造方法,则这里会直接将数组容量扩容到默认初识容量10,因为此时最小容量为10
    // 如果调用的带参构造方法,会将容量扩容为原来的1.5倍
    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指向这个新数组
    // 原来的旧的数组由于没有引用变量指向他,就会被垃圾回收机制回收掉
    elementData = Arrays.copyOf(elementData, newCapacity);
}

在这里插入图片描述

在扩容时会申请一块更大内存容量的数组,将原来数组中的元素拷贝到这个新的数组中,同时让elementData指向该数组,而原来的数组由于没有新的指针指向它,就会被垃圾回收机制回收。

在扩容时,首先考虑扩容1.5倍(如果扩容的太大,可能会浪费更多的内存,如果扩容的太小,又会导致频繁扩容,申请数组拷贝元素等对添加元素的性能消耗较大,因此1.5倍刚好)。如果扩容1.5倍还是内存不足,就会直接扩容到添加元素所需的最小的容量。同时不能超过数组的最大容量Integer.MAX_VALUE - 8。

面试题:ArrayList是如何扩容的?

答:如果调用的是空参数构造方法,第一次调用add方法添加元素时会将容量扩容10,以后每次都是原容量的1.5倍。如果调用的带参构造方法,在容量不满足时,会将容量扩容为原来的1.5倍。

面试题:ArrayList频繁扩容导致添加性能急剧下降,如何处理?

答:创建集合的时候指定足够大的容量。

04. 在数组的指定位置添加元素 add(int index, E element)

在此列表中的指定位置插入指定元素。将当前位于该位置的元素(如果有)和任何后续元素向右移动(将一个元素添加到其索引中)

public void add(int index, E element) {
    // 判断索引是否越界
    rangeCheckForAdd(index);
	// 确保添加元素时容量充足
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 源数组 elementData 从传入形参index处开始复制,复制size-index个元素
    // 目标数组 elementData 从index+1处开始粘贴,粘贴从源数组赋值的元素
    System.arraycopy(elementData, index, elementData, index + 1,size - index);
    // /把index处的元素替换成新的元素。
    elementData[index] = element;
    size++;
}

① 判断索引是否越界:

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
public class IndexOutOfBoundsException extends RuntimeException {
    private static final long serialVersionUID = 234122996006267687L;
    // 使用super关键字调用父类的空参数构造方法
    public IndexOutOfBoundsException() {
        super();
    }
    // 使用super关键字调用父类的带参构造方法
    public IndexOutOfBoundsException(String s) {
        super(s);
    }
}

② 确保数组的容量充足,至少保证能再装下一个元素。

③ 数组时一片连续的内存空间,如果要在指定的位置插入元素,需要移动数组中的元素:

在这里插入图片描述

elementData数组中的元素,从要插入的位置index开始,将index索引元素及其后面的元素向后移动一个位置,给要插入的元素腾出一个位置,将该元素插入到index位置处。

05. 替换指定位置的元素 set(int index, E element)

用指定的元素替换此列表中指定位置的元素。

public E set(int index, E element) {
    // 判断索引是否越界
    rangeCheck(index);
	// 获取指定索引处的元素
    E oldValue = elementData(index);
    // 用指定的元素替换指定索引的元素
    elementData[index] = element;
    // 返回旧值
    return oldValue;
}

06. 获取指定索引位置处的元素 get(int index)

因为数组的内存是连续的,因此可以根据索引直接获取元素。

public E get(int index) {
    // 判断索引是否越界
    rangeCheck(index);
    // 返回指定索引处的元素
    return elementData(index);
}

07. 删除指定位置的元素 remove(int index)

删除该列表中指定位置的元素,将所有后续元素向前移动

public E remove(int index) {
    // 判断索引是否越界
    rangeCheck(index);
	// 集合的快速失败机制
    modCount++;
    // 获取指定索引处的元素
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    // 源数组 elementData 从传入形参index+1处开始复制,复制size-index-1个元素
    // 目标数组 elementData 从index处开始粘贴,粘贴从源数组赋值的元素
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    // 将最后一个元素置为null,再将元素size-1
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

在这里插入图片描述

是将elementData数组中的元素,从要删除的元素的后面一个位置开始到末尾的元素结束都向前移动一个位置,然后将最后一个元素置为null,再将元素size-1。

08. 把集合所有数据转换成字符串 toString()

返回此集合的字符串表示形式。字符串形式由集合元素的列表组成,这些元素按迭代器返回的顺序排列,用方括号(“[]”)括起来。相邻元素由字符“,”(逗号和空格)分隔。元素通过String.valueOf(Object)转换为字符串。

public String toString() {
    // 获取遍历集合的迭代器
    Iterator<E> it = iterator();
    // 判断集合中是否有元素,如果没有则返回:[]
    if (! it.hasNext())
        return "[]";
    // 创建 StringBuilder 拼接字符串
    StringBuilder sb = new StringBuilder();
    // 先拼接一个:[
    sb.append('[');
    for (;;) {
        // 指针向下移动并返回迭代器指针指向的元素
        E e = it.next();
        // 拼接元素
        sb.append(e == this ? "(this Collection)" : e);
        // 如果集合中没有了元素,则追加:]
        if (! it.hasNext())
            return sb.append(']').toString();
        // 元素之间拼接一个逗号和空格
        sb.append(',').append(' ');
    }
}

该方法调用的是AbstractCollection抽象类中的方法:

在这里插入图片描述

09. 迭代器 iterator() 方法

按正确顺序返回此列表中元素的迭代器,返回的迭代器快速失败。

public Iterator<E> iterator() {
    return new Itr();
}
private class Itr implements Iterator<E> {
    // 下一个返回元素的索引
    int cursor;   
    // 最后一个返回元素的索引
    int lastRet = -1;  
    // 将集合实际修改次数赋值给预期修改次数:调用add()方法,remove()方法时modCount都会加1
    // 在迭代的过程中,只要实际修改次数和预期修改次数不一致就会产生并发修改异常
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

   // 调用next() 方法时会先返回光标处的元素,然后将光标向下移动
    @SuppressWarnings("unchecked")
    public E next() {
        // 集合迭代器的快速失败机制
        checkForComodification();
        int i = cursor;
        // 将光标赋值给i
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        // 光标向下移动
        cursor = i + 1;
        // 返回索引i位置的元素
        // 将最后一个返回的元素索引lastRet设置为i
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        // 集合迭代器的快速失败机制
        checkForComodification();

        try {
            // 移除调用next()方法获取的元素
            ArrayList.this.remove(lastRet);
            // 将光标指向删除元素处
            cursor = lastRet;
            lastRet = -1;
            // 将集合实际修改次数赋值给预期修改次数
            // 因此在迭代的过程中调用迭代器的remove()方法不会抛出异常
            // 而调用集合的remove()方法会抛出ConcurrentModificationException异常
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

2. 如何遍历集合并删除List中的元素?

问题主要在于remove(int index)方法的实现:删除该列表中指定位置的元素,会将其所有后续元素向前移动,然后将最后一个元素的值置为null

public E remove(int index) {
    // 判断索引是否越界
    rangeCheck(index);
	// 集合的快速失败机制
    modCount++;
    // 获取指定索引处的元素
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    // 源数组 elementData 从传入形参index+1处开始复制,复制size-index-1个元素
    // 目标数组 elementData 从index处开始粘贴,粘贴从源数组赋值的元素
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    // 将最后一个元素置为null,再将元素size-1
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

01. 普通 for 循环删除(不可靠)

public class Main {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        for (int i = 0; i < list.size(); i++) {
            String str = list.get(i);
            if (str.startsWith("李")) {
                list.remove(i);
            }
        }
        System.out.println(list);
    }
}

在这里插入图片描述

我们发现李白没有删掉,因为删除列表中指定位置的元素,会将其所有后续元素向前移动,最后一个元素的值置为null,数组的实际大小size在减小,因此李白没有删除掉。

02. 普通 for 循环提取变量删除(抛异常)

public class Main {
    public static void main(String[] args) {
        List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        List<String> list = new ArrayList(initList);
        int size = list.size();
        for (int i = 0; i < size; i++) {
            String str = list.get(i);
            if (str.startsWith("李")) {
                list.remove(i);
            }
        }
        System.out.println(list);
    }
}

在这里插入图片描述

抛出下标越界异常,因为size 变量是固定的,但 list 的实际大小是不断减小的,而 i 的大小是不断累加的,一旦 i >= list 的实际大小肯定就异常了。

03. 普通 for 循环倒序删除(可靠)

public class Main {
    public static void main(String[] args) {
        List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        List<String> list = new ArrayList(initList);
        for (int i = list.size() - 1; i > 0; i--) {
            String str = list.get(i);
            if (str.startsWith("李")) {
                list.remove(i);
            }
        }
        System.out.println(list);
    }
}

输出正确,从数组的最后一个元素开始删除,就不会出现问题,可以再看下remove()方法的源码。

04. 增强 for 循环删除(抛异常)

public class Main {
    public static void main(String[] args) {
        List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        List<String> list = new ArrayList(initList);
        for (String element : list) {
            if (element.startsWith("李")) {
                list.remove(element);
            }
        }
        System.out.println(list);
    }
}

在这里插入图片描述
这个是集合操作中很常见的异常之一,即并发修改异常!

其实,for(xx in xx) 就是增强的 for循环,即迭代器 Iterator 的加强实现,其内部是调用的 Iterator 的方法。使用迭代器进行遍历集合时,除了通过迭代器自身的 remove() 方法之外,对集合进行任何其他方式的结构性修改,则会抛出ConcurrentModificationException异常。

每次迭代器使用 next() 方法获取下个元素的时候都会去判断要修改的数量(modCount)和期待修改的数量(expectedModCount)是否一致,不一致则会报错,而 ArrayList 中的 remove 方法并没有同步期待修改的数量(expectedModCount)值,所以会抛异常了。

原理可以看ArrayList源码:remove() 方法,add() 方法,iterator() 方法

05. 迭代器循环迭代器删除(可靠)

public class Main {
    public static void main(String[] args) {
        List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        List<String> list = new ArrayList(initList);
        for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
            String str = iterator.next();
            if (str.contains("李")) {
                iterator.remove();
            }
        }
        System.out.println(list);
    }
}

结果输出正常,这是因为迭代器中的 remove 方法将期待修改的数量(expectedModCount)值进行了同步。

06. 迭代器循环集合删除(抛异常)

public class Main {
    public static void main(String[] args) {
        List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        List<String> list = new ArrayList(initList);
        for (Iterator<String> ite = list.iterator(); ite.hasNext(); ) {
            String str = ite.next();
            if (str.contains("李")) {
                list.remove(str);
            }
        }
        System.out.println(list);
    }
}

在这里插入图片描述
又是那个并发修改异常,这个示例虽然使用了 Iterator 循环,但删除的时候却使用了 list.remove 方法,同样是有问题的。

07. 集合 forEach 方法循环删除(抛异常)

public class Main {
    public static void main(String[] args) {
        List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        List<String> list = new ArrayList(initList);
        list.forEach((e) -> {
            if (e.contains("李")) {
                list.remove(e);
            }
        });
        System.out.println(list);
    }
}

在这里插入图片描述
forEach 方法的背后其实就是增强的 for 循环,底层即迭代器,所以使用 list.remove 同样抛出 ConcurrentModificationException 异常。

相关文章:

  • redis(win版)
  • 【什么程度叫熟悉linux系统】
  • 带您了解TiDB MySQL数据库中关于日期、时间的坑
  • 为什么不建议用 equals 判断对象相等?
  • C/C++ 中#define 的妙用,让代码更美一些
  • 缺少IT人员的服装行业该如何进行数字化转型?
  • Spring Cloud Nacos源码讲解(二)- Nacos客户端服务注册源码分析
  • 复旦发布国内首个类ChatGPT模型MOSS,和《流浪地球》有关?
  • MyBatis基于XML的详细使用——动态sql
  • Qt 开发使用VSCode 笔记2
  • 算法分析详解
  • 进程间通信(二)/共享内存
  • 边玩边学,13个 Python 小游戏真有趣啊(含源码)
  • 「TCG 规范解读」第7章 TPM工作组 TPM 总结
  • 七大排序经典排序算法
  • 带你一步步搭建Web自动化测试框架
  • 踩大坑:json格式存储wav二进制内容
  • ChatGPT 简介
  • 猜数字大小 II
  • Python3 pip
  • 哈马斯:愿就达成一项“全面”协议进行谈判
  • 何立峰会见美国英伟达公司总裁黄仁勋:欢迎美资企业深耕中国市场
  • 秦洪看盘|量能虽萎缩,但交易情绪尚可
  • 习近平会见柬埔寨国王西哈莫尼
  • 商务部:对原产于日本的进口电解电容器纸继续征收反倾销税
  • 远洋渔船上的谋生