03、集合之ArrayList

ArrayList的底层数据结构是数组,数组的默认长度是10。需要注意的是,当使用new ArrayList()的方式创建了ArrayList集合时,数组的长度为0,只有当调用了add()方法后,数组的长度才会变成默认长度10。

在这里插入图片描述
在这里插入图片描述

基本概念

ArrayList中有几个非常重要的成员变量需要我们注意下,

①elementData 是真正用于存储数据的数组;

②DEFAULT_CAPACITY 表示数组的初始大小,默认是 10;

③size 表示当前数组中的元素数量,类型 int,没有使用 volatile 修饰,非线程安全的;

④modCount 统计当前数组被修改的版本次数,数组结构有变动,就会 +1,例如执行add()、remove()方法modCount都会发生改变;

创建方式

创建ArrayList的方法有三种:

①使用new ArrayList()无参的方式创建;

②使用new ArrayList(10)创建指定大小的ArrayList集合,如果大概知道集合的最终大小,推荐使用这种方式创建,特别是当集合数据量比较大时,可以极大的提高效率,因为在执行add()方法时,如果集合容量不足会自动扩容,将原来的数组中的数据拷贝到扩容后新创建的数组中,非常耗费性能,如果一开始就指定了集合的大小,就可以减少扩容的次数;如果一开始指定了ArrayList的大小,后面容量不足时会自动扩容,这个大小不是固定不变的;

③使用new ArrayList(Collection<? extends E> c)指定初始数据的方式创建,参数c也是集合

新增和扩容

新增就是往数组中添加元素,主要分为以下两步:

  1. 判断是否需要扩容,如果需要执行扩容操作;
  2. 向数组中添加元素;

源码如下:

public boolean add(E e) {
  //确保数组大小是否足够,不够执行扩容,size 为当前数组中存放的元素个数
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  //直接赋值,线程不安全的
  elementData[size++] = e;
  return true;
}

private void ensureCapacityInternal(int minCapacity) {
  //如果初始化数组大小时,有给定初始值,以给定的大小为准,不走 if 逻辑
  if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  }
  //确保容积足够
  ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
  //记录数组被修改
  modCount++;
  // 如果我们期望的最小容量大于目前数组的长度,那么就扩容
  if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}
//扩容,并把现有数据拷贝到新的数组里面去
private void grow(int minCapacity) {
  int oldCapacity = elementData.length;
  // oldCapacity >> 1 是把 oldCapacity 除以 2 的意思
  int newCapacity = oldCapacity + (oldCapacity >> 1);

  // 如果扩容后的值 < 我们的期望值,扩容后的值就等于我们的期望值
  if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;

  // 如果扩容后的值 > jvm 所能分配的数组的最大值,那么就用 Integer 的最大值
  if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
 
  // 通过复制进行扩容
  elementData = Arrays.copyOf(elementData, newCapacity);
}

从源码中可以得知以下几点:

  1. add()方法是线程不安全的,如果将ArrayList作为共享变量会存在线程安全问题;
  2. 第一次执行add()方法后,底层的数组的长度会变成10,后续如果数组容量不够会自动扩容,扩容为原数组的1.5倍(10、15、22、…),并将原数组的值拷贝到新数组中;
  3. 执行add()方法后,版本modCount会增加;
  4. ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了;
  5. 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的;
  6. 扩容是通过public static <T> T[] copyOf(T[] original, int newLength)方法实现的,该方法的作用是实现数组的拷贝,第一个参数是要拷贝的数组,第二个参数是新数组的长度,返回值是拷贝了原数组值后的新数组,Arrays.copyOf()方法底层是通过System.arraycopy()实现的,该方法是native方法
删除

ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等,根据值删除的源码如下:

public boolean remove(Object o) {
  // 如果要删除的值是 null,找到第一个值是 null 的删除
  if (o == null) {
    for (int index = 0; index < size; index++)
      if (elementData[index] == null) {
        fastRemove(index);
        return true;
      }
  } else {
    // 如果要删除的值不为 null,找到第一个和要删除的值相等的删除
    for (int index = 0; index < size; index++)
      // 这里是根据  equals 来判断值相等的,相等后再根据索引位置进行删除
      if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
      }
  }
  return false;
}

private void fastRemove(int index) {
  // 记录数组的结构要发生变动了
  modCount++;
  // numMoved 表示删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去
  // 减 1 的原因,是因为 size 从 1 开始算起,index 从 0开始算起
  int numMoved = size - index - 1;
  if (numMoved > 0)
    // 从 index +1 位置开始被拷贝,拷贝的起始位置是 index,长度是 numMoved
    System.arraycopy(elementData, index+1, elementData, index, numMoved);
  //数组最后一个位置赋值 null,帮助 GC
  elementData[--size] = null;
}

从源码中可以得知以下几点:

  1. 新增的时候是没有对 null 进行校验的,所以删除的时候也是允许删除 null 值的;
  2. 通过for循环遍历数组,找到第一个匹配的元素;
  3. 找到值在数组中的索引位置,是通过 equals 来判断的,如果数组元素不是基本类型,需要我们关注 equals 的具体实现;
  4. 执行remove()方法后,版本modCount也会增加;
  5. 删除操作底层也是通过System.arraycopy()方法进行数组拷贝,将要删除的元素后面所有的元素向前移动,然后将数组最后面的值设置为null

删除的动图:

在这里插入图片描述

执行删除操作的时候需要注意,如果是在遍历ArrayList集合的时候对ArrayList进行删除操作,会有问题,例如以下代码:

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        
        for(int i = 0; i < list.size(); ++i) {
            list.remove(i);
        }
  
  	    System.out.println("list size: " + list.size());
}

以上代码执行不会报错,但是执行完后,list的长度不等于0,而等于2,原因是每次执行remvoe()方法后,都会将后面的元素向前移动,而i自增,所以会有一些元素没有被删除掉

迭代器

如果要自己实现迭代器,需要实现 java.util.Iterator 类,ArrayList 也是这样做的,迭代器的几个重要的参数如下:

int cursor;// 迭代过程中,下一个元素的位置,默认从 0 开始。
int lastRet = -1; // 新增场景:表示上一次迭代过程中,索引的位置;删除场景:为 -1。
int expectedModCount = modCount;// expectedModCount 表示迭代过程中,期望的版本号;modCount 表示数组实际的版本号。

迭代器一般来说有三个方法:

  1. hasNest()方法,判断还有没有值可以迭代;
  2. next()方法,获取当前迭代的值;
  3. remove()方法,删除当前迭代的值

方法的源码如下:

public boolean hasNext() {
  return cursor != size;//cursor 表示下一个元素的位置,size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果不等,说明还可以迭代
}

public E next() {
  //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
  checkForComodification();
  //本次迭代过程中,元素的索引位置
  int i = cursor;
  if (i >= size)
    throw new NoSuchElementException();
  Object[] elementData = ArrayList.this.elementData;
  if (i >= elementData.length)
    throw new ConcurrentModificationException();
  // 下一次迭代时,元素的位置,为下一次迭代做准备
  cursor = i + 1;
  // 返回元素值
  return (E) elementData[lastRet = i];
}
// 版本号比较
final void checkForComodification() {
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
}

public void remove() {
  // 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了
  if (lastRet < 0)
    throw new IllegalStateException();
  //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
  checkForComodification();

  try {
    ArrayList.this.remove(lastRet);
    cursor = lastRet;
    // -1 表示元素已经被删除,这里也防止重复删除
    lastRet = -1;
    // 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
    // 这样下次迭代时,两者的值是一致的了
    expectedModCount = modCount;
  } catch (IndexOutOfBoundsException ex) {
    throw new ConcurrentModificationException();
  }
}

从源码中可以看出:

  1. hasNext()方法是通过比较下一个元素的位置跟集合中的元素总数,从而判断是否还有元素可以迭代;
  2. next()方法就干了两件事情,第一是检验能不能继续迭代,第二是找到迭代的值,并为下一次迭代做准备(cursor+1);
  3. next()方法通过比较期望版本号和当前版本号,判断在迭代过程中集合是否被修改,如果有被修改就快速失败;
  4. remove()方法通过对lastRet进行判断,从而防止重复删除操作(即在一次遍历中执行了多次remove方法),原理是执行remove()方法时将lastRet设置为-1,执行next()方法时将lastRet设置成当前元素在数组中的下标,在remove()方法中删除元素前先判断lastRet是否小于0,从而保证在每次迭代过程中只会执行一次remove()方法;
  5. remove()方法删除元素成功后,当前 modCount 就会发生变化,这里会把 expectedModCount 重新赋值,下次迭代时两者的值就会一致了

注意:在执行remove()方法执行必须先执行next()方法,不然由于lastRet值为-1,会报错

迭代器删除元素代码如下:

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");

        // 可以正常删除集合中的元素,注意要先执行iterator.next()方法然后再执行iterator.remove()方法,让expectedModCount(期望版本号)跟modCount(实际版本号)保持一致
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()) {
            System.out.println(iterator.next()); // iterator.next()会让 expectedModCount = modCount
            iterator.remove();
        }
  
  System.out.println("list size: " + list.size());
}

/**
* 运行结果:
* list size: 0
*/
时间复杂度

ArrayList底层都是对数组元素的操作,只需要根据数组索引,直接新增和删除,所以时间复杂度是 O (1)。

线程安全

只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的。

ArrayList 有线程安全问题的本质,是因为 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,而且这些变量的类型并非是可见(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。

类注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList 是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低,具体实现源码:

public boolean add(E e) {
    synchronized (mutex) {// synchronized 是一种轻量锁,mutex 表示一个当前 SynchronizedList
        return c.add(e);
    }
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值