System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
二、修改与获取元素的方法
在 ArrayList
中,修改与获取元素的方法分别只有一个,就是 get
和 set
。
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
虽然能动态扩展长度,但是正因为如此而带来了两个问题:
-
线程不安全
-
fail-fast
一、ArrayList 是线程不安全的集合
为什么说它线程不安全呢?或者说到底是什么原因而导致的线程不安全呢?以添加方法为例:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这是前面分析过的添加方法,用于向集合末尾添加元素。ArrayList
在添加一个元素的时候,它可能会被分为两步完成:
-
在
elementData[size]
的位置上存放此元素 -
改变
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
,这个属性在扩容判断,删除元素的时候都会记录这个值,它代表着改变的次数!
再来看 ArrayList
中 iterator()
的源码:
//返回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()
,如果 modCount
与 expectedModCount
不相等,则抛出 ConcurrentModificationException
异常。
在使用迭代器遍历元素集合的时候,每次调用 next()
方法都会检查 modCount
和 expectedModCount
的值是否相同,如果在遍历的同时使用 list
的 remove()
方法去删除元素,则会修改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)]