ArrayList(下)

102
原文链接: mp.weixin.qq.com

为什么使用transient修饰elementData?

ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。

比如,现在实际有了8个元素,那么elementData数组的容量可能是8x1.5=12,如果直接序列化elementData数组,那么就会浪费4个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。

所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。一句话概括:保证只序列化实际存储的那些元素,而不是整个数组。

采用ArrayList的迭代器遍历集合时,对集合执行相关修改操作时为什么会抛出ConcurrentModificationException,我们该如何避免?

int cursor;       // index of next element to returnint lastRet = -1; // index of last element returned; -1 if no suchint expectedModCount = modCount;
  • cursor:表示下一个要访问的元素的索引,从next()方法的具体实现就可看出

  • lastRet:表示上一个访问的元素的索引

  • expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。

  • modCount是AbstractList类中的一个成员变量,该值表示对List的修改次数,查看ArrayList的add()和remove()方法就可以发现,每次调用add()方法或者remove()方法就会对modCount进行加1。

当调用list.iterator()返回一个Iterator之后,通过Iterator的hashNext()方法判断是否还有元素未被访问,我们看一下hasNext()方法,hashNext()方法的实现很简单:

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

判断下一个要访问的元素是否等于arrayList长度。如果不相等说明还未到最后一个。然后看next()方法:

@SuppressWarnings("unchecked")public E next() {    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();}

重点就在这第一句checkForComodification()。判断modCount与expectedModCount。如果不相等而抛出的异常。

事例代码:

public class Test {    public static void main(String[] args)  {        ArrayList<Integer> list = new ArrayList<Integer>();        list.add(2);        Iterator<Integer> iterator = list.iterator();        while(iterator.hasNext()){            Integer integer = iterator.next();            if(integer==2)                list.remove(integer);        }    }}

初始时,cursor为0,lastRet为-1,那么调用一次之后,cursor的值为1,lastRet的值为0。注意此时,modCount为0,expectedModCount也为0。回到arrayList的remove()方法中。忘记了的可以看我上一篇文章。

通过remove方法删除元素最终是调用的fastRemove()方法,在fastRemove()方法中,首先对modCount进行加1(因为对集合修改了一次),然后删除元素,将size减1,并将引用置为null以方便垃圾收集器进行回收。注意此时各个变量的值:对于iterator,其expectedModCount为0,cursor的值为1,lastRet的值为0。对于list,其modCount为1,size为0。执行完删除操作,调用hasNext方法()判断,此时cursor为1,size为0,那么返回true,所以继续执行while循环,然后继续调用iterator的next()方法:注意,此时要注意next()方法中的第一句:checkForComodification()。很显然,此时modCount为1,而 expectedModCount 为0,因此程序就抛出了 ConcurrentModificationException 异常。

到这里,想必大家应该明白为何上述代码会抛出ConcurrentModificationException异常了。即便是for-each也会出现这个异常。

想要解决这个问题,目前我知道的有两种。1.使用iterator.remove可以解决这个问题。但如果你有两个iterator,还是会出现这个异常。这就涉及到多线程环境下的问题。在这里不展开说多线程环境下怎么处理。画个饼?2.使用JDK1.8中加入的removeIf()方法。不过要记住如果你的数据中有null,它是会报空指针异常。如果使用最好做非空校验。

题外话:上面所述的异常情况,是java集合(Collection)中的一种错误机制。叫快速失败(fail-fast)。至于它是啥。这里也不展开说。画饼+1。感兴趣的朋友可以自行google或者等我出 :D。

为什么ArrayList的最大数组大小是Integer.MAX_VALUE - 8?

数组作为一个对象,需要一定的内存存储对象头信息,对象头信息最大占用内存不可超过8字节。OutOfMemoryError: Java heap space  堆区内存不足(这个可以通过设置JVM参数 -Xmx 来指定)。OutOfMemoryError: Requested array size exceeds VM limit 超过了JVM虚拟机的最大限制,我的window64就是Integer.MAX_VALUE-1 .(这些内容涉及到JVM虚拟机知识)

对象头信息 Object head words数组的对象头信息相较于其他Object,多了一个表示数组长度的信息。简单理解:你去吃杨国福麻辣烫,放食材的容器是不是也一起算进重量里。这放食材的容器就是这8。

关于ArrayList的讲解就全部说完了。文中所涉及的知识其实还是挺多的。等我学完了再补上。

新的一年。加油吧。