Java集合系列(二)—— ArrayList

System.arraycopy(elementData, index, elementData, index + numNew,

numMoved);

System.arraycopy(a, 0, elementData, index, numNew);

size += numNew;

return numNew != 0;

}

二、修改与获取元素的方法


ArrayList 中,修改与获取元素的方法分别只有一个,就是 getset

E set(int index, E element) —— 修改 index 位置的元素,结果返回被修改的元素。

public E set(int index, E element) {

//这个方法与前面判断越界的方法有所不同,具体看下面的分析

rangeCheck(index);

//获取旧的值

E oldValue = elementData(index);

//设置新的值

elementData[index] = element;

return oldValue;

}

private void rangeCheck(int index) {

//显然,不同之处就是少了一个条件:index < 0,这也就是意味着可以传递一个负数作为参数

if (index >= size)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

}

E get(int index) —— 获取 index 位置的元素。

public E get(int index) {

//检查下标是否越界

rangeCheck(index);

//返回数组index位置的元素

return elementData(index);

}

E elementData(int index) {

return (E) elementData[index];

}

三、删除元素的方法


ArrayList 中,提供了四个用于删除元素的方法。

1. E remove(int index) —— 移除集合中某个位置的元素,结果返回这个元素。

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; // clear to let GC do its work

return oldValue;

}

2. boolean remove(Object o) —— 删除某个元素,如果集合中存在多个,则删除首次出现的,结果返回布尔值,表示是否成功。

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 {

for (int index = 0; index < size; index++)

if (o.equals(elementData[index])) {

fastRemove(index);

return true;

}

}

return false;

}

private void fastRemove(int index) {//该方法用于删除某个位置的元素(其实逻辑跟前面那个remove方法是一致的!)

//修改计数器

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

}

3. boolean removeAll(Collection<? extend E> c) —— 在源集合中移除参数集合 c 所有的元素,意思就是求差集,返回布尔值表示是否成功

public boolean removeAll(Collection<?> c) {

//检查参数是否为null

Objects.requireNonNull©;

//批量删除

return batchRemove(c, false);

}

public static T requireNonNull(T obj) {

//如果参数为null,则抛出异常,否则返回本身

if (obj == null)

throw new NullPointerException();

return obj;

}

private boolean batchRemove(Collection<?> c, boolean complement) {

//获取动态数组

final Object[] elementData = this.elementData;

//r为循环计数器;

int r = 0, w = 0;

//修改标识

boolean modified = false;

try {

//遍历动态数组

for (; r < size; r++)

if (c.contains(elementData[r]) == complement)

elementData[w++] = elementData[r];

} finally {

// Preserve behavioral compatibility with AbstractCollection,

// even if c.contains() throws.

if (r != size) {

System.arraycopy(elementData, r,

elementData, w,

size - r);

w += size - r;

}

if (w != size) {

// clear to let GC do its work

for (int i = w; i < size; i++)

elementData[i] = null;

modCount += size - w;

size = w;

modified = true;

}

}

return modified;

}

4. void clear() —— 清空集合。

public void clear() {

//修改计数器

modCount++;

//将所有元素置为null

for (int i = 0; i < size; i++)

elementData[i] = null;

//将长度置为0

size = 0;

}

ArrayList 存在的问题

===============

ArrayList 虽然能动态扩展长度,但是正因为如此而带来了两个问题:

  1. 线程不安全

  2. fail-fast

一、ArrayList 是线程不安全的集合


为什么说它线程不安全呢?或者说到底是什么原因而导致的线程不安全呢?以添加方法为例:

public boolean add(E e) {

ensureCapacityInternal(size + 1); // Increments modCount!!

elementData[size++] = e;

return true;

}

这是前面分析过的添加方法,用于向集合末尾添加元素。ArrayList 在添加一个元素的时候,它可能会被分为两步完成:

  1. elementData[size] 的位置上存放此元素

  2. 改变 size 的值

本质上,因为 size++ 不是一个原子操作从而导致的线程安全问题。在单线程环境下,如果 size=0 ,添加一个元素后,此元素在位置0,size=1;但是,在多线程环境下,比如两个线程,A 线程先将元素放在位置 0 ,但是此时 CPU 调度 A 暂停,线程 B 获取了执行的机会,B 也向列表添加元素,因为此时 size 依然为 0 ,所以线程 B 也将元素放在位置0上,最后,两个线程继续执行,导致都增加了 size

解决的办法有:

  • 使用同步代码块 synchronized

  • 使用 Collections.synchronizedList(new ArrayList())

  • 使用 JUC 中的 CopyOnWriteArrayList

二、fail-fast 问题


fail-fast 机制是 Java 集合中的一种错误机制,当多个线程对同一个集合的内容进行操作时,就可能产生 fail-fast 事件。例如:当线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException ,产生 fail-fast 事件。示例:

public static void main(String[] args) {

ArrayList list = new ArrayList<>();

list.add(1);

list.add(2);

list.add(3);

list.add(4);

for (Integer integer : list) {

if (integer == 2) {

list.remove(integer);

}

System.out.println(integer);

}

}

//运行结果

Exception in thread “main” java.util.ConcurrentModificationException

很奇怪吧,为什么在遍历 list 的时候删除元素会抛出异常呢?

首先,需要明确的是,foreach 本质上就是迭代器遍历集合元素,因此必须从迭代器的角度去寻找问题。在前面分析源码中,有一个修改计数器的属性 modCount,这个属性在扩容判断,删除元素的时候都会记录这个值,它代表着改变的次数!

再来看 ArrayListiterator() 的源码:

//返回List对应迭代器,实际上是返回一个Itr对象。Itr是ArrayList的一个内部类

public Iterator iterator() {

return new Itr();

}

//迭代器的实现类

private class Itr implements Iterator {

int cursor; // 代表下一个要访问的元素下标

int lastRet = -1; // 上一个要访问的元素下标

//记录修改次数,每次创建Itr对象时,都会保存新建该对象时对应的Count,以后每次遍历List中的元素时,都会比较expectedModCount和modCount是否相等,如果不相等,则抛出ConcurrentModificationException异常,产生fail-fast

int expectedModCount = modCount;

public boolean hasNext() {

return cursor != size;

}

//迭代器的next()方法获取下一个元素

@SuppressWarnings(“unchecked”)

public E next() {

//获取下一个元素之前,先判断新建的Itr对象时保存的modCount和当前的modCount是否相等,不相等则抛异常

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];

}

//迭代器的删除

public void remove() {

if (lastRet < 0)

throw new IllegalStateException();

checkForComodification();

try {

ArrayList.this.remove(lastRet);

cursor = lastRet;

lastRet = -1;

expectedModCount = modCount;

} catch (IndexOutOfBoundsException ex) {

throw new ConcurrentModificationException();

}

}

@Override

@SuppressWarnings(“unchecked”)

public void forEachRemaining(Consumer<? super E> consumer) {

Objects.requireNonNull(consumer);

final int size = ArrayList.this.size;

int i = cursor;

if (i >= size) {

return;

}

final Object[] elementData = ArrayList.this.elementData;

if (i >= elementData.length) {

throw new ConcurrentModificationException();

}

while (i != size && modCount == expectedModCount) {

consumer.accept((E) elementData[i++]);

}

// update once at end of iteration to reduce heap write traffic

cursor = i;

lastRet = i - 1;

checkForComodification();

}

//检查modCount和expectedModCount的值是否相同,如果不同,则直接抛出异常

final void checkForComodification() {

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

}

}

或许你会一头雾水,没关系。直接说结论:迭代器的 next()remove() 都会执行 checkForComodification(),如果 modCountexpectedModCount 不相等,则抛出 ConcurrentModificationException 异常。

在使用迭代器遍历元素集合的时候,每次调用 next() 方法都会检查 modCountexpectedModCount 的值是否相同,如果在遍历的同时使用 listremove()方法去删除元素,则会修改modCount的值,此时两者就不同,迭代器遍历一下个元素时就会察觉到这两个值不同了,从而抛出异常。

总结

===

**ArrayList 底层维护着一个 Object[] elementData,所有对集合的操作本质上就是在操作这个数组。**但是,这个数组与普通的数组最大的差别就是在于 elementData 是一个看似动态的数组。所谓动态,本质上其实就是不断创建新的数组去覆盖旧的数组而已(通过 Arrays 工具类或者 System 类)。

如果使用空参的构造器实例化一个 ArrayList ,在没有添加元素之前,它就是一个空数组。当第一次添加元素时,它默认的长度是 10 和添加元素总数的较大者。

笔者福利

以下是小编自己针对马上即将到来的金九银十准备的一套“面试宝典”,不管是技术还是HR的问题都有针对性的回答。

有了这个,面试踩雷?不存在的!

回馈粉丝,诚意满满!!!




remove()方法去删除元素,则会修改modCount`的值,此时两者就不同,迭代器遍历一下个元素时就会察觉到这两个值不同了,从而抛出异常。**

总结

===

**ArrayList 底层维护着一个 Object[] elementData,所有对集合的操作本质上就是在操作这个数组。**但是,这个数组与普通的数组最大的差别就是在于 elementData 是一个看似动态的数组。所谓动态,本质上其实就是不断创建新的数组去覆盖旧的数组而已(通过 Arrays 工具类或者 System 类)。

如果使用空参的构造器实例化一个 ArrayList ,在没有添加元素之前,它就是一个空数组。当第一次添加元素时,它默认的长度是 10 和添加元素总数的较大者。

笔者福利

以下是小编自己针对马上即将到来的金九银十准备的一套“面试宝典”,不管是技术还是HR的问题都有针对性的回答。

有了这个,面试踩雷?不存在的!

回馈粉丝,诚意满满!!!

[外链图片转存中…(img-RkiU8xG8-1714138497234)]
[外链图片转存中…(img-B46l8jEL-1714138497235)]
[外链图片转存中…(img-R3LviYTO-1714138497236)]
[外链图片转存中…(img-49LDt9oI-1714138497236)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值