终生求职之 ArrayList 篇

开发 后端
ArrayList构造函数,除非赋值,否则初始化后内部数组都是空数组,而第一次扩容则发生在添加元素的时候。

[[439233]]

本文转载自微信公众号「稀饭下雪」,作者帅气的小饭饭 。转载本文请联系稀饭下雪公众号。

最近又到了适合交配的季节了,不对,跳槽的季节了,发现好多之前看的知识点都忘记了,为此我做了面壁思过,最终总结如下。

问题的根本:

  • 不是不懂,而是记不住。
  • 一次性了解的东西可能不够深入。

针对这两种情况,解决方案也不难:

  • 每隔一段时间都要重新看回以前的东西,加深记忆力
  • 一次性了解的不够深入,那就多来几次,然后加上备忘录

为此,我定了一个终生求职计划

终生求职是一种状态,让自己一直保持随时可以面试的状态,目前行业越来越卷,这种状态很有必要

我这边预计是每两个月都会回顾一次笔记,然后不定期发文章。

好了,接下来继续聊聊 ArrayList。

ArrayList参数和构造函数

  1. // 存储数组元素的缓冲区 
  2. transient Object[] elementData; 
  3. // 默认空数组元素 
  4. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 
  5. // 默认初始化容量 
  6. private static final int DEFAULT_CAPACITY = 10; 
  7. // 数组的大小 
  8. private int size
  9. // 记录被修改的次数 
  10. protected transient int modCount = 0; 
  11. // 数组的最大值 
  12. private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 

底层ArrayList使用数组实现

构造函数的几种情况:

  • 空构造函数
  1. public ArrayList() { 
  2.     this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 空数组 
  • 有参构造函数
  1. public ArrayList(int initialCapacity) { 
  2.     if (initialCapacity > 0) { 
  3.         this.elementData = new Object[initialCapacity]; // 数组的大小为参数的大小 
  4.     } else if (initialCapacity == 0) { 
  5.         this.elementData = EMPTY_ELEMENTDATA; // 空数组 
  6.     } else { 
  7.         throw new IllegalArgumentException("Illegal Capacity: "
  8.                                            initialCapacity); 
  9.     } 

Collection转Arraylist

  1. public ArrayList(Collection<? extends E> c) { 
  2.     elementData = c.toArray(); 
  3.     if ((size = elementData.length) != 0) { 
  4.         // c.toArray might (incorrectly) not return Object[] (see 6260652) 
  5.         if (elementData.getClass() != Object[].class) 
  6.             elementData = Arrays.copyOf(elementData, size, Object[].class); 
  7.     } else { 
  8.         // replace with empty array. 
  9.         this.elementData = EMPTY_ELEMENTDATA; 
  10.     } 

参数collection个数不为0的直接转成Arraylist,但是用的是system.copy

202112留言: ArrayList构造函数,除非赋值,否则初始化后内部数组都是空数组,而第一次扩容则发生在添加元素的时候。

ArrayList添加元素与扩容

ArrayList.add(E e)源码:

可以看到底层使用的是System.arraycopy,而这个copy的过程是比较耗性能的,因此建议初始化时预估一个容量大小。

202112留言: 用无参构造函数创建ArrayList后进行第一次扩容容量是10,后续则是1.5倍,底层调用的是System.arraycopy,而这个copy的过程是比较耗性能的,因此建议初始化时预估一个容量大小。

ArrayList删除元素

ArrayList提供两种删除元素的方法,可以通过索引和元素进行删除。两种删除大同小异,删除元素后,将后面的元素一次向前移动。

ArrayList.remove(int index)源码:

  1. public E remove(int index) { 
  2.     rangeCheck(index); 
  3.  
  4.     modCount++; 
  5.     E oldValue = elementData(index); 
  6.  
  7.     int numMoved = size - index - 1; 
  8.     if (numMoved > 0) 
  9.         System.arraycopy(elementData, index+1, elementData, index
  10.                          numMoved); 
  11.     elementData[--size] = null; // clear to let GC do its work 
  12.  
  13.     return oldValue; 

删除元素时,首先会判断索引是否大于ArrayList的大小,如果索引范围正确,则将索引位置的下一个元素赋值到索引位置,将ArrayList的大小-1,最后返回移除的元素。

这里也可以看到modCount++,正如前面所说,就是为了做并发处理,不允许其他线程在这个的同时做修改,同时也不允许自身线程在同时遍历修改。

elementData[--size] = null;可以看到,就是将最后一个值置空,方便GC掉。

202112备注: 删除后底层调用的依旧是System.arraycopy,而这个copy的过程是比较耗性能的,因此才说频繁增删的尽量别用ArrayList。

ArrayList遍历

  1. @Override 
  2. public void forEach(Consumer<? super E> action) { 
  3.     Objects.requireNonNull(action); 
  4.     // 预设值了一个expectedModCount值 
  5.     final int expectedModCount = modCount; 
  6.     @SuppressWarnings("unchecked"
  7.     final E[] elementData = (E[]) this.elementData; 
  8.     final int size = this.size
  9.     // 遍历过程中拿出来判断 
  10.     for (int i=0; modCount == expectedModCount && i < size; i++) { 
  11.         action.accept(elementData[i]); 
  12.     } 
  13.     // 如果对不上则报错 
  14.     if (modCount != expectedModCount) { 
  15.         throw new ConcurrentModificationException(); 
  16.     } 

从代码就可以看出来了,在遍历的时候会率先 预设值了一个expectedModCount值,然后再遍历拿出来判断,如果不一样了,则中断流程并且报错,而这个过程则涉及到了快速失败机制了,正常来说,ArrayList不允许遍历删除。

202112备注: ArrayList通过预设值expectedModCount实现了快速失败机制,避免了多线程遍历删除或者增加,以及遍历过程中增删元素。

集合的快速失败(fail-fast)

它是 Java 集合的一种错误检测机制,当多个线程对集合进行结构上的改变操作时,有可能会产生 fail-fast 机制。

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。

集合在被遍历期间如果内容发生变化,就会改变modCount的值。

每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。

如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。

因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

202112备注: 我们日常看到的Concurrent Modification Exception,其实就是触发了快速失败机制的表现,做法也很简单:

在遍历的时候给你给modCount设置个备份expectedModCount,如果有多线程在搞,那么必定会导致modCount被改,那么就容易了,每次遍历的时候都检测下modCount变量是否为expectedModCount就可以了,如果不是意味着被改了,那我就不管,我就要报错。

集合的安全失败(fail-safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

202112备注: 那么为啥并发容器的时候不怕呢?简单,因为采用了安全失败机制,在遍历的时候直接拷贝了一份出来,这样就不会触发了。

使用ArrayList的subList()需要注意的地方

  1. public List<E> subList(int fromIndex, int toIndex) { 
  2.         subListRangeCheck(fromIndex, toIndex, size); 
  3.         return new SubList(this, 0, fromIndex, toIndex); 
  1. SubList(AbstractList<E> parent,int offset,int fromIndex,int toIndex) { 
  2.             this.parent = parent; 
  3.             this.parentOffset = fromIndex; 
  4.             this.offset = offset + fromIndex; 
  5.             this.size = toIndex - fromIndex; 
  6.             this.modCount = ArrayList.this.modCount; 

subList()返回结果不可强制转为ArrayList类型,因为该方法实质是创建一个内部类SubList实例,这个SubList是AbstractList的实现类,并不继承于ArrayList。

通过上面源码可以看出,通过parent属性指定父类并直接引用了原有的List,并返回该父类的部分视图,只是指定了他要使用的元素的范围fromIndex(包含),endIndex(不包含)。

那么,如果对其原有或者子List做数据性修改,则会互相影响。如果对原有List进行结构性修改,则会踩坑Fast-fail,报错会抛出异常ConcurrentModification Exception。

202112备注: XList.subList()不能当作ArrayList来使用,但是内部其实是引用了实际上XList的部分元素,所以如果引用内的对象被改,也会直接影响XList。

ArrayList迭代器

看下迭代器的遍历和删除相关的源码

  1. public boolean hasNext() { 
  2.     return cursor != size
  3.  
  4. @SuppressWarnings("unchecked"
  5. public E next() { 
  6.     // 同样判断modCount != expectedModCount,不同则报错 
  7.     checkForComodification(); 
  8.     int i = cursor
  9.     if (i >= size
  10.         throw new NoSuchElementException(); 
  11.     Object[] elementData = ArrayList.this.elementData; 
  12.     if (i >= elementData.length) 
  13.         throw new ConcurrentModificationException(); 
  14.     cursor = i + 1; 
  15.     return (E) elementData[lastRet = i]; 
  16.  
  17. public void remove() { 
  18.     if (lastRet < 0) 
  19.         throw new IllegalStateException(); 
  20.     checkForComodification(); 
  21.  
  22.     try { 
  23.         ArrayList.this.remove(lastRet); 
  24.         cursor = lastRet; 
  25.         lastRet = -1; 
  26.         // 这里删除后会重新复制一次 
  27.         expectedModCount = modCount; 
  28.     } catch (IndexOutOfBoundsException ex) { 
  29.         throw new ConcurrentModificationException(); 
  30.     } 

202112备注: 为什么ArrayList的迭代器是支持遍历删除的,原因很简单,因为在删除后会重新赋一次值给expectedModCount。

ArrayList和LinkedList的优劣

其实就是数组和链表的优劣势,ArrayList优点,支持随机访问,get(i)的时间复杂度为O(1),而缺点就是需要扩容,要复制数组,而且内部插入数据需要移动数据,插入删除的性能差;

对于LinkedList来说,优点就是容量理论上来说是无限,不存在扩容,而且可以很方便的插入和删除数据(性能损失在查找),而缺点就是不能随机访问,get(i)需要遍历。

貌似就是反过来的,所以在实际开发中也很容易区别,看是查找频繁、还是增删频繁,如果是查找频繁就用ArrayList,如果增删频繁就用LinkedList即可。

202112备注: :ArrayList和LinkedList的优劣可以从数组和链表的结构上来回答,额外补充扩容问题,再回答索引查找频繁还是增删频繁。

原文链接:https://mp.weixin.qq.com/s/QEzHmLNj-uUytOsikpBf7g

 

责任编辑:武晓燕 来源: 稀饭下雪
相关推荐

2021-06-11 07:52:19

网络分层协议

2012-02-22 14:14:43

Java

2010-05-25 10:41:57

求职简历

2019-05-16 15:35:36

2009-03-16 09:39:01

函数词法作用域Javascript

2010-11-16 10:51:55

求职

2018-03-12 13:32:02

编程语言程序员语言

2011-02-25 10:36:12

Proftpd

2011-02-25 10:25:07

Proftpd

2021-10-26 10:22:27

ArrayList阿里云

2010-07-19 15:49:22

求职陷阱

2014-09-01 10:36:35

个推推送

2009-11-06 16:48:03

WCF简介

2021-09-06 06:45:06

Webpack优化MindMaster

2014-06-09 14:18:24

2011-03-02 14:07:24

Pureftpd

2014-08-01 14:01:28

Android UI

2021-09-06 06:45:06

WebpackMindMasterEntry

2016-09-12 15:26:06

戴尔

2009-08-14 17:45:52

C# ArrayLis
点赞
收藏

51CTO技术栈公众号