Java源码学习笔记(1):ArrayList

ArrayList

ArrayList就是一个数组,源码中有几个重要概念

  • index:表示数组下标
  • elementData:表示数组本身
  • DEFAULT_CAPACITY:表示初始数组的大小,默认是10!!!(无参构造器初始化是0,10 是在第一次 add 的时候扩容的数组值。)
  • size:表示当前数组的大小,没有用volatile修饰,非线程安全
  • modCount:统计当前数组被修改的次数,数组结构有变动,就会+1

一些重要注释

  • ArrayList允许put null值
  • size、isEmpty、get、set、add 等方法时间复杂度都是 O(1)
  • 是非线程安全的,多线程情况下,推荐使用线程安全类
  • 增强 for 循环,或者使用迭代器迭代过程中,如果数组大小被改变,会快速失败,抛出异常。

1、 初始化

有三种初始化办法:无参数直接初始化、指定大小初始化、指定初始数据初始化,源码如下:

// 无参初始化,数组大小为空
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 指定初始数据来初始化
public ArrayList(Collection<? extends E> c) {
  			// elementData是保存数组的容器,默认为null
        elementData = c.toArray();
  			// 如果初始集合c有值
        if ((size = elementData.length) != 0) {
            // 如果集合元素不是Object类型,则转成Object类型
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 如果初始集合c没值,则默认空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

注意

  • ArrayList 无参构造器初始化时,默认大小是空数组,并不是10,10 是在第一次 add 的时候扩容的数组值。

2、 新增与扩容

新增就是往数组中添加元素,主要分为两步。

  • 首先看要不要扩容,如果需要就先扩容
  • 直接赋值

新增源码如下

public boolean add(E e) {
  			//确保数组大小是否足够,不够则直接扩容,size是当前数组的大小,+1就是增加后的大小
        ensureCapacityInternal(size + 1);  // Increments modCount!!
  			//直接赋值,这是线程不安全的
        elementData[size++] = e;
        return true;
    }

扩容(ensureCapacityInternal)源码

    private void ensureCapacityInternal(int minCapacity) {
      	//确保容量足够
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    private void ensureExplicitCapacity(int minCapacity) {
      	// 记录数组被修改的次数
        modCount++;

        // 如果我们需要的最小容量 大于 当前数组的长度,那就需要扩容了
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
		//扩容,把现有数据拷贝到新的数组中
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
     		//新数组容量
        int newCapacity = oldCapacity + (oldCapacity >> 1);
      	//如果扩容后的容量 < 期望的容量,那就让期望容量成为新容量,因为至少需要这么多的
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
      
        //如果扩容后的容量 > jvm能分配的最大值,那么就用 Integer 的最大值,上界
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
      
        //通过复制进行扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
  • 扩容成原来大小的1.5倍
  • ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了。
  • 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。

扩容本质

通过代码Arrays.copyOf(elementData, newCapacity);来实现扩容,就是数组的拷贝,新建一个符合预期容量的新数组,然后把老数据拷贝过去。Arrays.copyOf是通过System.arraycopy来实现的,这个方法是native方法,源码如下。

3、 删除

  ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多,这里选取根据值删除方式来进行源码说明:

    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 {
          	//如果要删除的值不为null,找到第一个和要删除的值相等的元素删除
            for (int index = 0; index < size; index++)
              	//!!注意!!这里是根据equals来判断值是否相等,然后根据索引位置来删除
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
  • 新增元素时可以增加null元素,所以删除时也是允许删除null元素的
  • 找到值在数组中的索引位置,通过equals来判断相不相等等。

下面是fastRemove方法

private void fastRemove(int index) {
  			//记录修改次数
        modCount++;
  			//numMoved表示删除index上的元素后,有多少个元素要移动到元素前面去(数据结构知识)
        int numMoved = size - index - 1;
        if (numMoved > 0)
          	//把index后面的元素拷贝过去
            System.arraycopy(elementData, index+1, elementData, index,numMoved);
  					//数组最后一个元素赋值null,帮助GC
        		elementData[--size] = null; // clear to let GC do its work
    }

4、 迭代器

  如果要自己实现迭代器,实现 java.util.Iterator 类就好了,ArrayList 也是这样做的,它里面的Itr实现了迭代器接口。迭代器有三个重要参数,如下:

private class Itr implements Iterator<E> {
    int cursor;       // 迭代过程中下一个元素的位置,默认从0开始
    int lastRet = -1; // add场景:表示上一次迭代过程中索引的位置,remove场景:-1
    int expectedModCount = modCount;	//迭代过程中期望的版本次数。

ArrayList迭代器的三个方法源码

    private class Itr implements Iterator<E> {
        int cursor;       
        int lastRet = -1; 
        int expectedModCount = modCount;

        Itr() {}
				//有没有值可以迭代
        public boolean hasNext() {
          	//如果下一个元素位置和大小相等,说明已经迭代完了,不等则还可以继续迭代
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
          	//迭代过程中判断版本号有没有被修改,如果被修改了,抛出ConcurrentModificationException异常
            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() {
          	//如果lastRet值为-1,说明数组已经被删完了
            if (lastRet < 0)
                throw new IllegalStateException();
          	//迭代过程中判断版本号有没有被修改
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
              	//-1表示元素已经被删除,写这一句是为了避免重复删除的操作
                lastRet = -1;
                //删除后modCount已经发生变化,要把它赋值给expectedModCount,下一次迭代两个值就一致了
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
      
      //补上
      final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
      }

  只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的。ArrayList 有线程安全问题的本质,是因为 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,而且这些变量的类型是不可见(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。

  类注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList 是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低。

5、 面试问题

(1)ArrayList 无参数构造器构造,现在 add 一个值进去,此时数组的大小是多少,下一次扩容前最大可用大小是多少?

答:此处数组的实际大小是 1,但下一次扩容前最大可用大小是 10,因为 ArrayList 第一次扩容时, 是有默认值的,默认值是 10,在第一次 add 一个值进去时,数组的可用大小被扩容到 10 了。

(2) 如果我连续往 list 里面新增值,增加到第 11 个的时候,数组的大小是多少?

答:这里的考查点就是扩容的公式,当增加到 11 的时候,此时我们希望数组的大小为 11,但 实际上数组的最大容量只有 10,不够了就需要扩容,扩容的公式是:oldCapacity + (oldCapacity>> 1),oldCapacity 表示数组现有大小,目前场景计算公式是:10 + 10 /2 = 15,然后我们发现 15 已经够用了,所以数组的大小会被扩容到 15。

(3)数组初始化,被加入一个值后,如果我使用 addAll 方法,再一下子加入 15 个值,那么最终数组的大小是多少?

答:第一题中我们已经计算出来数组在加入一个值后,实际大小是 1,最大可用大小是 10 ,现在需要一下子加入 15 个值,那我们期望数组的大小值就是 16,此时数组最大可用大小只有 10,明显不够,需要扩容,扩容后的大小是:10 + 10 /2 = 15,这时候发现扩容后的大小仍 然不到我们期望的值 16,这时候源码中有一种策略如下:

// 如果扩容后的值 < 我们的期望值,我们的期望值就等于本次扩容的大小 
if (newCapacity - minCapacity < 0)
		newCapacity = minCapacity;

所以最终数组扩容后的大小为 16。

(4)现在我有一个很大的数组需要拷贝,原数组大小是 5k,请问如何快速拷贝?

答:因为原数组比较大,如果新建新数组的时候,不指定数组大小的话,就会频繁扩容,频繁扩容就会有大量拷贝的工作,造成拷贝的性能低下,所以说新建数组时,指定新数组的大小为 5k 即可。

(5)有一个 ArrayList,数据是 2、3、3、3、4,中间有三个 3,现在我通过 for 循环的方式想把3删除,可以删除干净吗?最终结果是什么?为什么

答:不能删除干净,最终删除的结果是 2、3、4,有一个 3 删除不掉,原因我们看下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yL9JKWWP-1581255614347)(/Users/zhangye/Library/Application Support/typora-user-images/image-20191211164341073.png)]

每次删除一个元素后,该元素后面的元素就会往前移动,而此时循环的 i 在不断地增长,最终会使每次删除 3 的后一个 3 被遗漏,导致删除不掉。

(6)还是上面的 ArrayList 数组,我们通过增强 for 循环进行删除,可以么?

答:不可以,会报错。因为增强 for 循环调用的就是迭代器的 next () 方法,当你调用 remove () 方法进行删除时,modCount 的值会 +1,而这时候迭代器中的 expectedModCount 的值却没有变,导致在迭代器下次执行 next () 方法时, expectedModCount != modCount 就会报 ConcurrentModificationException 的错误。

(7)还是上面的数组,如果删除时使用list. Iterator 然后remove () 可以删除么,为什么?

答:可以的,因为 Iterator.remove () 方法在执行的过程中,会把最新的 modCount 赋值给 expectedModCount,这样在下次循环过程中,modCount 和 expectedModCount 两者就会相等。

    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(3);
        list.add(3);
        list.add(4);
        // for (int i = 0; i < list.size(); i++) {
        //     if (list.get(i) == 3) {
        //         list.remove(i);
        //     }
        // }

      	//增强型for循环调用的是迭代器的next,list.remove然后会调用fastRemove
        //前面是迭代器的版本号,后面是list里面持有的版本号,list调用remove,版本号+1
        //但是前面迭代器的版本号是没变的。
        // for (Integer i : list) {
        //     if (i == 3) {
        //         list.remove(i);
        //     }
        // }

        Iterator<Integer> it = list.iterator();
        while (it.hasNext()) {
            Integer next = it.next();
            if (next == 3) {
                it.remove();	//迭代器的remove,而不是list的
            }
        }
	      //主要就是看这个迭代器是不是list自己的
        System.out.println(list);
    }

发布了43 篇原创文章 · 获赞 6 · 访问量 3907

猜你喜欢

转载自blog.csdn.net/weixin_44424668/article/details/104241199