今天要分享的Java集合是List,主要是针对它的常见实现类ArrayList进行讲解
内容目录
什么是List核心方法源码剖析1.文档注释2.构造方法3.add()3.remove()如何提升ArrayList的性能ArrayList可以代替数组吗?
什么是List
List集合是线性数据结构的主要实现,用来存放一组数据。我们称之为:列表。
ArrayList是List的一个常见实现类,它的面试频率和使用频率都非常高,所以我们今天通过学习ArrayList来对Java中的List集合有一个深入的理解。
ArrayList最大的优势是可以将数组的操作细节封装起来,比如数组在插入和删除时要搬移其他数据。另外,它的另一大优势,就是支持动态扩容,这也是我们使用ArrayList的主要场景之一,在某些情况下我们没有办法在程序编译之前就确定存储数据
容器的大小。
核心方法源码剖析
这一部分,选取了ArrayList的一些核心方法进行讲解。分别是:构造方法,add()、和remove()。这里有一个小窍门,我们在读jdk源码的时候,一定要先看类上的doc注释,比较核心的知识点都会写在上面。有一个初步的概念再去看源码,就会容易很多。
1.文档注释
This class is roughly equivalent to Vector, except that it is unsynchronized.
大致相当于Vector,不同之处是不同步(线程不安全)
Implements all optional list operations, and permits all elements, including null
实现所有可选列表操作,并允许所有元素,包括null
in the face of concurrent modification, the iterator fails quickly and cleanly
面对并发修改,迭代器将快速而干净地失败
2.构造方法
ArrayList()提供了三种构造方法。
ArrayList():构造一个初始容量为10的空列表。
ArrayList(int initialCapacity):构造具有指定初始容量的空列表。
ArrayList(Collection c):构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
1/**
2 * Constructs an empty list with an initial capacity of ten.
3 */
4public ArrayList() {
5 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
6}
这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的部数组,不设定初始值时,只是引用这个内部数组。
1/**
2 * Constructs an empty list with the specified initial capacity.
3 *
4 * @param initialCapacity the initial capacity of the list
5 * @throws IllegalArgumentException if the specified initial capacity
6 * is negative
7 */
8public ArrayList(int initialCapacity) {
9 if (initialCapacity > 0) {
10 this.elementData = new Object[initialCapacity];
11 } else if (initialCapacity == 0) {
12 this.elementData = EMPTY_ELEMENTDATA;
13 } else {
14 throw new IllegalArgumentException("Illegal Capacity: "+
15 initialCapacity);
16 }
17}
这里的EMPTY_ELEMENTDATA同样是一个空内部数组,为了和DEFAULTCAPACITY_EMPTY_ELEMENTDATA做区分,所以没有使用一个对象。
3.add()
add方法是ArrayList中的一个核心方法,涉及到内部数组的扩容。
1 /**
2 * Appends the specified element to the end of this list.
3 *
4 * @param e element to be appended to this list
5 * @return <tt>true</tt> (as specified by {@link Collection#add})
6 */
7public boolean add(E e) {
8 ensureCapacityInternal(size + 1); // Increments modCount!!
9 elementData[size++] = e;
10 return true;
11}
该方法是在集合中追加元素。其中核心方法是ensureCapacityInternal,意思是确定集合内部容量。
1private void ensureCapacityInternal(int minCapacity) {
2 ensureExplicitCapacity(
3 calculateCapacity(elementData,minCapacity));
4}
5
6private static int calculateCapacity(Object[] elementData, int
7 minCapacity) {
8 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
9 return Math.max(DEFAULT_CAPACITY, minCapacity);
10 }
11 return minCapacity;
12}
这里首先计算了集合的容量,如果这个ArrayList是通过无参构造创建的,那么比较默认值10,以及传入的minCapacity,取最大值,这里可能有的同学会有疑问,为什么要比较默认值和minCapacity,默认值不是一定大于minCapacity吗?,这里是因为ensureCapacityInternal这个方法不仅仅是add()会调用,allAll()也会调用。
1public boolean addAll(int index, Collection<? extends E> c) {
2 rangeCheckForAdd(index);
3
4 Object[] a = c.toArray();
5 int numNew = a.length;
6 ensureCapacityInternal(size + numNew); // Increments modCount
7 //省略部分代码..
8 }
这里如果numNew大于10,那么默认值就会不够用。所以才会在calculateCapacity方法中引入一个求最大值的步骤。
算出集合存储数据所需的最小空间后,就要考虑,集合原有存储空间是否够用,是否需要扩容。
1private void ensureExplicitCapacity(int minCapacity) {
2 modCount++;
3 // overflow-conscious code
4 if (minCapacity - elementData.length > 0)
5 grow(minCapacity);
6}
7
8/**
9 * Increases the capacity to ensure that it can hold at least the
10 * number of elements specified by the minimum capacity argument.
11 *
12 * @param minCapacity the desired minimum capacity
13 */
14 private void grow(int minCapacity) {
15 // overflow-conscious code
16 int oldCapacity = elementData.length;
17 int newCapacity = oldCapacity + (oldCapacity >> 1);
18 if (newCapacity - minCapacity < 0)
19 newCapacity = minCapacity;
20 if (newCapacity - MAX_ARRAY_SIZE > 0)
21 newCapacity = hugeCapacity(minCapacity);
22 // minCapacity is usually close to size, so this is a win:
23 elementData = Arrays.copyOf(elementData, newCapacity);
24}
这里我们主要关注4个点:
1.int newCapacity = oldCapacity + (oldCapacity >> 1);每次扩容是原数组的1.5倍
2.扩容也是有限的,存在最大值:MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
3.集合扩容底层调用的是:Arrays.copyOf()方法,需要把数组中的数据复制一份,到新数组中,而这个方法底层是System.arrayCopy是一个native方法,效率不高。
4.最重要的一个点:如果我们可以事先估计出数据量,那么最好给ArrayList一个初始值,这样可以减少其扩容次数,从而省掉很多次内存申请和数据搬移操作。(不指定初始值,至少会执行一次grow方法,用于初始化内部数组)。
3.remove()
1/**
2 * Removes the element at the specified position in this list.
3 * Shifts any subsequent elements to the left (subtracts one from their
4 * indices).
5 *
6 * @param index the index of the element to be removed
7 * @return the element that was removed from the list
8 * @throws IndexOutOfBoundsException {@inheritDoc}
9 */
10public E remove(int index) {
11 rangeCheck(index);
12 modCount++;
13 E oldValue = elementData(index);
14
15 int numMoved = size - index - 1;
16 if (numMoved > 0)
17 System.arraycopy(elementData, index+1, elementData, index,
18 numMoved);
19 elementData[--size] = null; // clear to let GC do its work
20 return oldValue;
21}
删除的代码因为不涉及到缩容,所以比起add较为简单,首先会检查数组是否下标越界,然后会获取指定位置的元素,接着进行数据的搬移,将--size位置的元素置成null,让GC进行回收。最后将目标元素返回即可。
另外最后我想提出一个比较容易犯的错误,集合在遍历的时候,对其结构进行修改(删除、新增元素)。举一个例子:
1public class Test {
2 public static void main(String[] args) {
3 List<Integer> list = new ArrayList<>();
4 list.add(1);
5 list.add(2);
6 list.add(3);
7 for (Integer i : list) {
8 if(i.equals(1)){
9 list.remove(i);
10 }
11 }
12 }
13}
结果:
1Exception in thread "main" java.util.ConcurrentModificationException
2 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
3 at java.util.ArrayList$Itr.next(ArrayList.java:859)
4 at jialin.li.Test.main(Test.java:12)
产生问题的原因,其实文档注释已经给出了明确的结果,即:
if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own {@link ListIterator#remove() remove} or {@link ListIterator#add(Object) add} methods, the iterator will throw a {@link ConcurrentModificationException}
如果列表在任何时间从结构上修改创建迭代器之后,以任何方式除非通过迭代器自身remove种或add方法,迭代器都将抛出一个ConcurrentModificationException。这里我建议是遍历的时候,不要对其结构进行修改,而是采用其他方法(打标,或者复制列表)的方式进行。
如何提升ArrayList的性能
1 给定初值,省掉很多次内存申请和数据搬移操作。
2 对于读多写少的场景,可以使用ArrayList替代LinkedList,可以省内存,同时CPU缓存的利用率也会更高。(数组存储的时候,是内存是连续的,CPU读取内存数据、内存读取磁盘数据的时候,都不是一条一条读取,而是一次读取临近的一批数据,所以连续的存储可以让CPU更有机会一次读取较多的有效数据)
ArrayList可以代替数组吗?
不可以,任何数据结构都有它存在的场景和意义,集合没有办法存储基本数据类型,只能存储包装类型,包装类型就意味着需要拆箱和装箱,会有一定的性能消耗,如果对性能要求非常高的系统,或者只需要使用基本类型,那么就应该去使用数组而不是集合。同时数组在表示多维数据的时候,也更加直观,比如二维 int[][] 、ArrayList<arraylist>。我们使用集合更多的情况是想利用它的扩容特性,以及增删数据时不会造成空洞。
最后,期待您的订阅和点赞,专栏每周都会更新,希望可以和您一起进步,同时也期待您的批评与指正!