螺丝钉笔记之ArrayList

ArrayList是什么

ArrayList就是动态数组,简单的说,就是Array的复杂版本,它提供了动态的增加和减少元素,实现了Collection和List接口,灵活的设置数组的大小等。

ArrayList用法

ArrayList是一个泛型容器,新建一个ArrayList需要实例化泛型参数,比如:

ArrayList<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
for (int i = 0; i < intList.size(); i++) {
    System.out.println(intList.get(i));
}

其他ArrayList Api 可自行查看java core API或者ArrayList源码。

实现原理

在jdk8中,ArrayList的基本原理是内部有一个数组elementData,一般会有一些预留空间,有一个变量size记录数组元素中实际个数。

transient Object[] elementData;

private int size;

随着其内部方法都是基于elementData和size,elementData会随着实际元素的变化而重新分配,size则会始终记录元素的实际个数。

ArrayList构造器

先看ArrayList以下三个构造器:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

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 = c.toArray();
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

我们一般要用ArrayList的话,会用前两个构造器生成其对象,然后操作内部API。
当我们使用无参构造器的时候,其elementData数组会指向一个内部定义的静态Object空数组。该空数组是用static,final两个关键字修饰的,其含义在于ArrayList类在被加载的同时,被static修饰的变量就存在于内存当中了,这样一来就独立于该类的所有对象,被该类所有对象共享,若我们直接用无参构造生成对象时,其生成的引用或者匿名对象都是指向空数组,这样能够避免内存过度开销。

当我们用含int参数构造器生成对象时,首先判断其initialCapacity是正整数时,就将elementData初始化为initialCapacity的长度的Object数组;若initialCapacity等于0,则和无参构造器初始化一般,指向空数组;否则抛出非法参数异常。

下面是生成一个初始化长度为3的ArrayList对象:

ArrayList<Person> persons = new ArrayList<>(3);

当initialCapacity大于0时,其elementData就已经开始初始化为initialCapacity大小的数组。所以,上面的代码中初始化为3的数组,其Object数组中每个下标对于的默认值是null,其在内存中开辟一块区域,具体示意图如下:

在这里插入图片描述

add方法

add方法涉及代码为:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

它首先调用ensureCapacityInternal方法,判断数组是不是空的,若是空的,则首次至少分配大小为DEFAULT_CAPACITY,其DEFAULT_CAPACITY的值为10。
接下来调用ensureExplicitCapacity方法,其中有modCount++;操作,其含义是modCount是内部的修改次数,modCount++表示增加修改的次数。
若设定的长度大于当前数组的长度,则调用grow方法。在grow方法中,首先在原来的数组长度oldCapacity上,扩大其1.5倍,若新的长度还是小于设定的长度minCapacity,就扩展到设定的长度;将elementData指向新的数组拷贝。
最后将新元素指向到其elementData数组的末尾,这样就添加了一个元素。

下面演示其调用add方法时,内存分配示意图:

ArrayList<Person> persons = new ArrayList<>(3);
persons.add(new Person("张三", 23));
persons.add(new Person("李四", 25));
persons.add(new Person("王五", 24));
for (int i = 0; i < persons.size(); i++) {
    System.out.println(persons.get(i));
}

在这里插入图片描述

get方法

get方法涉及的代码如下:

public E get(int index) {
    rangeCheck(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];
}

这里get方法里,首先rangeCheck方法判断其index下标值有没有越界,然后通过其数组下标值找到对应的元素。

remove方法

remove方法涉及的代码如下:

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null;
    return oldValue;
}

remove方法首先判断index下标有没有越界,它也增加了modCount,然后计算要移动的元素的个数,从index往后的元素都往前移动一位,实际调用System.arraycopy方法移动元素。elementData[–size] = null;这行代码表示size减1,并且值设为null,设为null后不再引用原来的对象,则原来的对象就可以被垃圾回收了。

迭代

下面来看一个迭代的例子:

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
System.out.println("for循环遍历");
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}
System.out.println("forEach循环遍历");
for (String str : list) {
    System.out.println(str);
}

上面有两种遍历方式,不过,forEach看上去更为简洁,而且它适用于各种容器,更为通用。
不过,这种foreach语法背后是怎么实现的呢?
其实,foreach循环也是Java提供的一个语法,若将foreach代码编译后的class文件进行反编译的话,可以得到以下代码:

System.out.println("forEach循环遍历");
Iterator var4 = list.iterator();
while(var4.hasNext()) {
  String str = (String)var4.next();
  System.out.println(str);
}

可以发现,原来的增强for循环,其实是依赖了while循环和Interator实现的。

迭代接口

ArrayList实现了Iterable接口,Iterable表示可迭代,其定义:

public interface Iterable<T> {
  Iterator<T> iterator();
}

定义很简单,就是要求实现iterator方法。
它返回一个实现了Iterator接口的对象,接口定义如下:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

hasNext()判断是否还有元素未访问,next()返回下一个元素。

迭代的陷阱

关于迭代器,有一种常见的误用,就是在迭代的中间调用容器的删除方法。比如,要删除ArrayList中的某个字符串,直觉上,可以这么写:

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
for (String str : list) {
    if ("B".equals(str)) {
        list.remove(str);
    }
}

但运行时会抛出异常:
Exception in thread “main” java.util.ConcurrentModificationException
同样的,可以尝试下在增强for循环中使用add方法添加元素,结果也会同样抛出该异常。

之所以会出现这个异常,是因为触发了一个Java集合的错误检测机制——fail-fast。
接下来,我们就来分析下在增强for循环中add/remove元素的时候会抛出java.util.ConcurrentModificationException的原因,即解释下到底什么是fail-fast进制,fail-fast的原理等。
fail-fast,即快速失败,它是Java集合的一种错误检测机制。当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。

同时需要注意的是,即使不是多线程环境,如果单线程违反了规则,同样也有可能会抛出改异常。
那么,在增强for循环进行元素删除,是如何违反了规则的呢?
要分析这个问题,我们先将增强for循环这个语法进行反编译(使用jad对编译后的class文件进行反编译),得到以下代码:

ArrayList list = new ArrayList();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
    String str = (String)var2.next();
    if("B".equals(str)) {
        list.remove(str);
    }
}

然后运行以上代码,同样会抛出异常。我们来看一下ConcurrentModificationException的完整堆栈:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.simple.base.Test.main(Test.java:16)

通过异常堆栈我们可以到,异常发生的调用链Test的第16行,Iterator.next 调用了 Iterator.checkForComodification方法 ,而异常就是checkForComodification方法中抛出的。
其实,经过debug后,我们可以发现,如果remove代码没有被执行过,iterator.next这一行是一直没报错的。抛异常的时机也正是remove执行之后的的那一次next方法的调用。
我们直接看下checkForComodification方法的代码,看下抛出异常的原因:

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

代码比较简单,modCount != expectedModCount的时候,就会抛出ConcurrentModificationException。

那么,就来看一下,remove/add 操作室如何导致modCount和expectedModCount不相等的吧。

首先,我们要搞清楚的是,到底modCount和expectedModCount这两个变量都是个什么东西。

通过翻源码,我们可以发现:

modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。
expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。expectedModCount表示这个迭代器期望该集合被修改的次数。
其值是在ArrayList.iterator方法被调用的时候初始化的。只有通过迭代器对集合进行操作,该值才会改变。
Itr是一个Iterator的实现,使用ArrayList.iterator方法可以获取到的迭代器就是Itr类的实例。
他们之间的关系如下:

class ArrayList{
    private int modCount;
    public void add();
    public void remove();
    private class Itr implements Iterator<E> {
        int expectedModCount = modCount;
    }
    public Iterator<E> iterator() {
        return new Itr();
    }
}

其实,看到这里,大概很多人都能猜到为什么remove/add 操作之后,会导致expectedModCount和modCount不想等了。
通过翻阅代码,我们也可以发现,remove方法核心逻辑如下:

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

可以看到,它只修改了modCount,并没有对expectedModCount做任何操作。

简单总结一下,之所以会抛出ConcurrentModificationException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改。

至此,我们介绍清楚了不能在foreach循环体中直接对集合进行add/remove操作的原因。

但是,很多时候,我们是有需求需要过滤集合的,比如删除其中一部分元素,那么应该如何做呢?有几种方法可供参考:

1、直接使用普通for循环进行操作
我们说不能在foreach中进行,但是使用普通的for循环还是可以的,因为普通for循环并没有用到Iterator的遍历,所以压根就没有进行fail-fast的检验。

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
for (int i = 0; i < list.size(); i++) {
    if ("B".equals(list.get(i))) {
        list.remove(i);
    }
}
System.out.println(list);

2、直接使用Iterator进行操作
除了直接使用普通for循环以外,我们还可以直接使用Iterator提供的remove方法。

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    if ("B".equals(iterator.next())) {
        iterator.remove();
    }
}
System.out.println(list);

如果直接使用Iterator提供的remove方法,那么就可以修改到expectedModCount的值。那么就不会再抛出异常了。

3、使用Java 8中提供的filter过滤
Java 8中可以把集合转换成流,对于流有一种filter操作, 可以对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list = list.stream().filter(str -> !str.equals("B")).collect(Collectors.toList());
System.out.println(list);

4、直接使用fail-safe的集合类
在Java中,除了一些普通的集合类以外,还有一些采用了fail-safe机制的集合类。这样的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。

以上这几种方式都可以避免触发fail-fast机制,避免抛出异常。如果是并发场景,建议使用concurrent包中的容器,如果是单线程场景,Java8之前的代码中,建议使用Iterator进行元素删除,Java8及更新的版本中,可以考虑使用Stream及filter。

参考:https://mp.weixin.qq.com/s/e9ITxUmsMFhfjeHhOgTtfA

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值