Java集合源码学习—ArrayList

又是热爱学习的一天… 今天准备学习一下 ArrayList 的源码,研究它都干了什么。
在这里插入图片描述在这里插入图片描述

总结 (先看结论,再寻找如何得出的此结论)

  • ArrayList 是一种以数组实现的 List,与数组相比,它具有动态扩展的能力,因此也可称之为 动态数组

  • ArrayList 实现了 List,提供了基础的添加、删除、遍历等操作。

  • ArrayList 实现了 RandomAccess,提供了随机访问的能力。

  • ArrayList 实现了 Cloneable,可以被克隆。

  • ArrayList 实现了 Serializable,可以被序列化。

1. ArrayList 的成员变量

先看看 ArrayList 的成员变量,我把它的每一个成员变量都加了注释用以解释这个变量有何作用。

 	/**
     * 默认初始容量,也就是说,使用 new ArrayList() 创建的 ArrayList ,它的初始容量为 DEFAULT_CAPACITY;
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空数组,使用 new ArrayList(0) 创建 ArrayList 时,使用的数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {
     };

    /**
     * 空数组,使用 new ArrayList() 创建 ArrayList 时,使用的数组。
     * 在添加第一个元素的时候,会将这个数组的容量初始化为 DEFAULT_CAPACITY 大小
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
     };

    /**
     * 存放真正的元素数据的数组
     * 在添加第一个元素的时候,会使 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组容量初始化为 DEFAULT_CAPACITY 大小,这和上面那句话对应起来了。
     * 疑问?这里加上 transient 关键字的意思应该是为了不让序列化这个数组里面的内容,也就是我们存进 ArrayList的真实数据,可是经过测验,却可以序列化该数组里面的数据。
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList 所包含的实际元素个数,而不是 ArrayList 的长度
     * 它和 elementData.length 的区别是,size是实际元素个数,elementData数组里,可能有4个实际元素,6个空元素,所以 elementData.length,代表了 elementData 的长度,它里面包含了空元素。
     */
    private int size;

2. ArrayList 的构造函数

2.1 指定容量构造函数:ArrayList(int initialCapacity)

    /**
     * 传入初始容量,如果容量大于0,就将 elementData 初始化为对应大小。如果等于0,那就使用空数组:EMPTY_ELEMENTDATA。如果小于0,就抛异常了。
     * @param initialCapacity 传入指定的容量
     */
    public ArrayList(int initialCapacity) {
     
        if (initialCapacity > 0) {
     
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
     
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
     
            throw new IllegalArgumentException("Illegal Capacity: "+
                    initialCapacity);
        }
    }

2.2 默认构造函数:ArrayList()

	/**
     * 如果不传参数,那就使用空数组:DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
     * 目前 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的容量为0,要等到添加第一个元素时。它才会初始化为 DEFAULT_CAPACITY 的大小。
     */
    public ArrayList() {
     
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

2.3 传入集合构造函数:ArrayList()

     /**
     * 传入一个 Collection 集合,并调用 toArray() 方法将它里面的内容初始化给 elementData。
     * 然后再判断元素个数是否为0,如果为0,就将 elementData 初始化为 EMPTY_ELEMENTDATA 这个空数组
     * @param c
     */
    public ArrayList(Collection<? extends E> c) {
     
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
     
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
     
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

这里请注意!! 该构造方法中写道:c.toArray might (incorrectly) not return Object[] (see 6260652)
意思是: c.toArray方法返回的可能不是 Object[] 类型,详情见JDK bug编号 6260652

那么这里的 if 判断是说,如果 elementData 不是 Object[] 的类型,就通过 copyOf 这个方法将不是 Object[] 的类型的 elementData 数组(它有可能是String[],int[]…不管它是什么,暂时不管),转换成 Object[] 类型。

这句话有点绕,简单来说就是:如果 elementData 如果不是 Object[] 的类型,那就通过 copyOf 方法把它转换成 Object[] 的类型

2.4 疑问:为什么 elementData 会可能不是 Object[] 的类型呢?

为什么这里会有这一步操作呢?为什么 elementData 会不是 Object[] 的类型呢?下面举例子解释:

我们首先来看一下这个构造器里面的内容,我们聚焦在第二层的这个 if 判断上,它判断了 elementData.getClass() 是否等于 Object[].class。那么,elementData 是怎么来的呢?构造器第一句就告诉我们了,他是 Collection 类型的参数 c 调用toArray方法初始化来的。 也就是说,这个 if 判断的是 c.toArray 方法返回的类型是否是 Object[] 类型。

先理解上面这段说明,下面就举一个栗子来分析说明:

	// 1、首先使用工具类 Arrays 的  asList 方法,将数组转换成 List。
	List<String> myList = Arrays.asList("123456", "ABCDEF");
	
	// 2、模拟进入构造器,构造器需要 Collection 类型的数据,为什么这里传 List 也可以呢?
	//因为 ArrayList 实现与 List 接口,List 接口又继承于 Collection 类,所以 ArrayList 也算是 Collection 的子类。所以这里传 List 也 OK。
	List<String> arrayList = new ArrayList<>(myList);
	
	// 这里的 myList 就是构造器里面的 c 变量。 myList 为实参, c为形参。
	// 根据上面的理解,如果 c.toArray方法返回的类型,也就是这里的 myList.toArray 方法返回的类型不为 Object[] 类型,那就做转换动作,那么 myList 的类型到底是什么呢?
	System.out.println(myList.toArray());

打印结果为:

	[Ljava.lang.String;@27c170f0

结果说明 c.toArray方法返回的类型 还真有不是 Object[] 类型的,所以构造器里面存在这个if判断操作。

这个时候,可能有同学要问了:

  1. 为什么 myList.toArray 方法返回的不是 Object[] 类型呢???
  2. 在例子中 myList 已经是一个 List 了,我为什么不直接用它呢,为什么还要把它当作参数再创建新的ArrayList呢,我直接用
    myList 不就得了吗?

2.5 解答问题1:为什么 myList.toArray 方法返回的不是 Object[] 类型呢???

首先看例子中,我们是使用工具类 Arrays 的 asList 方法,将字符串数组转换成 List。那么我们点进 asList 方法看一下源码:

	@SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
     
        return new Arrays.ArrayList<>(a);
    }

可以看见它只不过是 new 了一个 Arrays.ArrayList<>(a) 对象而已。
那么这个 Arrays.ArrayList<> 是个什么东西呢?它其实就是 Arrays 类里面的一个内部类,它就在 asList 方法的下面,源码中他们紧挨着。部分代码如下:

Arrays.ArrayList<> 的源码

 private static class ArrayList<E> extends AbstractList<E>
            implements RandomAccess, java.io.Serializable
    {
     
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
     
            a = Objects.requireNonNull(array);
        }

        @Override
        public int size() {
     
            return a.length;
        }

        @Override
        public Object[] toArray() {
     
            return a.clone();
        }
        ......

也就是说 Arrays.asList 方法返回的是 Arrays 的一个内部类,它继承于 AbstractList,所以它返回的并不是 java.util.ArrayList。
现在知道 Arrays.asList 方法返回的是什么之后,再来看为什么它调用 toArray() 方法之后,返回的不是 Object[] 类型。

现在将目光聚焦在我贴出来的 Arrays.ArrayList 这个内部类的部分源码,看最后一个方法,这是啥!!!!!其实他重写了父类的toArray()方法,所以当我们在使用 Arrays.asList 方法创建出来的对象的 toArray 方法时,调用的是他自己重写的方法!!

现在再看他是如何重写的,它是 clone 了一份 成员变量 a 的数据,a 是我们创建对象时传进来的,一直往上追,可以发现 a 里面就是我们用例中传的字符串数组,如果我们用例中写的两个int类型的数据,那么此时的 a 就会是这个int数组。所以 toArray 方法其实返回的是一个数组,它并不是一个真正的List。

现在明白为啥例子中打印出来是字符串数组类型的,而不是 Object[] 类型了吧。

2.6 解答问题2:在例子中 myList 已经是一个 List 了,我为什么不直接用它呢,为什么还要把它当作参数再创建新的ArrayList呢,我直接用 myList 不就得了吗?

满足愿望,那就先使用 myList 增加一个元素试试:

	myList.add("HyugaNeji");

结果显示:

    java.lang.UnsupportedOperationException
	at java.util.AbstractList.add(AbstractList.java:148)
	at java.util.AbstractList.add(AbstractList.java:108)
	at AsListTest.main(AsListTest.java:25)

结果是抛异常了,原因是 myList 的类型是 Arrays.ArrayList 类型的,它自己没有实现 add 方法,但是它继承的父类 AbstractList 实现了,所以这里调用 add ,是调用到了它的父类的 add 方法,而它的父类是实现了 add 方法的,其内容如下:

public boolean add(E e) {
     
  	 add(size(), e);
	 return true;
}

发现这个方法又调用了 另一个 add 方法,继续往下看,在 147 行的位置发现了这个方法:

public void add(int index, E element) {
     
     throw new UnsupportedOperationException();
}

结果已经很明显了,它的实现方法就是抛异常…

所以,Arrays.asList 创建的所谓的List,是不可以使用 add、set、remove这些方法的,既然这些方法都不能用,我要他有何用,所以才会使用它作参数再创建新的ArrayList。所以才有了构造器里面的那段代码。

至于,Arrays.ArrayList为什么不直接返回一个 java.util.ArrayList 的原因,我现在暂时还没搞清楚,可能是设计模式层面的东西,因为它现在这样实现,不就是适配器模式吗?

一口气学习了这么多,可累坏了。歇会儿
Java集合源码学习—ArrayList_第1张图片

3. 保证数组容量安全的核心辅助方法

先介绍几个保证数组容量安全的核心辅助方法

3.1 ensureCapacityInternal 方法

private void ensureCapacityInternal(int minCapacity) {
     
		ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

ensureCapacityInternal 方法的意思是 确保 ArrayList 内部容量的意思。如果容量不够装了就进行扩容,确保容量。
它的实现是调用了两个方法,它通过调用 calculateCapacity 方法,拿到返回值,再传给 ensureExplicitCapacity 方法。下面我们先看 calculateCapacity 方法做了什么。

3.2 calculateCapacity 方法

calculateCapacity 方法,看名字就知道它是 计算容量的,它的目的是返回 ArrayList 要存放数据的最小的目标容量。
它通常在 add 数据的时候被使用到,参数 minCapacity 的意思是存放数据需要最小的容量,它是: ArrayList 的实际存放元素个数 + 新增的个数

private static int calculateCapacity(Object[] elementData, int minCapacity) {
     
   	 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
     
      	  return Math.max(DEFAULT_CAPACITY, minCapacity);
   	 }
   	 return minCapacity;
}

首先判断 elementData 的引用和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的引用是否是相同的,也就是判断是否调用了无参构造器。
因为如果调用了无参构造器,那么 elementData 的容量 == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的容量 == DEFAULT_CAPACITY(原因详见构造器函数)
如果是相同的,那么 elementData 的初始容量就是 DEFAULT_CAPACITY,所以比较 DEFAULT_CAPACITY 和 minCapacity ,谁大返回谁。

换句话说就是:如果调用了无参构造器,那么最小容量最小就是10。这个10容量,可能是几个实际元素 + 几个空元素;

如果不是调用的无参构造器,那么就直接返回 minCapacity。也就是说:我新增元素的个数 + 数组中已有的元素个数 = 最小容量minCapacity,那你最少的给我准备 minCapacity 个数的容量,才够装我的数据。

3.3 ensureExplicitCapacity 方法

ensureExplicitCapacity 方法决定了 ArrayList 要不要扩容

private void ensureExplicitCapacity(int minCapacity) {
     
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
 }
  1. 首先累加修改次数
  2. 如果我实际要存放数据的个数(minCapacity) 减去 elementData.length 大于0,
    也就是说,我要存放数据的个数大于数组的长度,这个数组装不下了,就需要调用 grow() 方法 扩容了。

3.4 允许最大数组长度

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

减8的目的是为了留位置存放数组的长度,因为数组自己不能计算长度,需要留个位置记录一下

3.5 扩容的方法 grow (扩容核心函数)

 private void grow(int minCapacity) {
     
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
  }

grow 扩容方法,传入参数(minCapacity) 告诉这个方法,我需要存放的数据的个数,也就是最小的容量,你的容量最小得等于我的个数,不然就不够装啊。

  1. 首先,算出数组旧的容量,赋值给 oldCapacity 变量
  2. 然后计算出 旧容量的1.5倍,在赋值给新容量 newCapacity。这里 oldCapacity >> 1 相当于除以2,在加上原来的就是1.5倍了。JDK1.6的做法是:int newCapacity = (oldCapacity*3)/2+1,这里位运算的速度是要比整除效率高。我这是JDK1.8。
  3. 接着判断,如果 newCapacity - minCapacity 如果小于 0 表示新容量比最小我要求容量还要小,也就是扩容后你还不够装的话,那就使用我要求的最小容量吧。将最小容量的值赋值给新容量变量。
  4. 再接着判断,如果 newCapacity - MAX_ARRAY_SIZE 大于 0 表示 新容量比允许最大数组长度都还要大,那咋办。那就只有请出我的大宝贝了,啊,呸…请出巨大容量函数:hugeCapacity(minCapacity)。并将它的返回值赋值给新容量。
  5. 最后,再将老数组的里面的数据拷贝到新数组里面去。

下面看看 巨大容量函数:hugeCapacity(minCapacity) 做了啥。

3.6 hugeCapacity 方法

 private static int hugeCapacity(int minCapacity) {
     
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
 }
  1. 首先,该函数判断了 minCapacity 是否是小于 0,如果是,那就表示内存溢出啦,直接抛错误。因为int是有范围的,超出了整个范围之后,就会变成一个负数。
  2. 然后判断,我所需要的容量(minCapacity),是否是大于 MAX_ARRAY_SIZE,如果是那就返回 Integer.MAX_VALUE:整型的最大值 2^{31}-1。否则就返回 MAX_ARRAY_SIZE

所以 ArrayList 扩容的核心思想是:扩容原来数组长度的1.5倍,然后再将老数组里面的数据拷贝到新数组中

4. 平时常用的方法

4.1 添加元素方法:add(E e)

public boolean add(E e) {
     
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}
  1. 首先它调用了 ensureCapacityInternal 方法确保容量,检查是否需要扩容,关于这个方法的详细解释和调用链在前面已经学习过了,这里就不多解释了。
  2. 然后进行赋值

4.2 添加元素到指定位置方法:add(int index, E element)

public void add(int index, E element) {
     
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
}

private void rangeCheckForAdd(int index) {
     
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
  1. 首先 调用 rangeCheckForAdd 函数,检查指定位置是否越界,这个函数只有 add 和 addAll 的时候使用。
  2. 确保容量,检查是否需要扩容
  3. 把指定索引位置后的元素都往后挪一位;
  4. 在指定索引位置放置插入的元素;
  5. 累加实际存放元素个数。

4.3 将指定集合的数据添加到之前的集合中:addAll(Collection c)

public boolean addAll(Collection<? extends E> c) {
     
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
 }
  1. 首先,将参数 c 的数据拷贝到数组 a 中
  2. 算出 a 的长度,也就是这次于要添加数据的个数
  3. 确保容量,检查是否需要扩容
  4. 把数组a中的元素拷贝到elementData的尾部;
  5. 累加实际存放元素个数。

4.4 获取指定索引位置的数据:E get(int index)

public E get(int index) {
     
        rangeCheck(index);

        return elementData(index);
    }
        private void rangeCheck(int index) {
     
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

@SuppressWarnings("unchecked")
E elementData(int index) {
     
	return (E) elementData[index];
}

首先检查索引是否越界,这里只检查是否越上界,如果越上界抛出IndexOutOfBoundsException异常,如果越下界抛出的是 ArrayIndexOutOfBoundsException异常。
然后在返回指定索引位置处的元素;

4.5 移除指定位置的元素: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;
 }
  1. 检查索引是否越界;
  2. 获取指定索引位置的元素;
  3. 如果删除的不是最后一位,则其它元素往前移一位;(计算后 numMoved 如果等于0,那么表示删除的最后一位)
  4. 将最后一位置为null,方便GC回收;
  5. 返回删除的元素。

注意:ArrayList删除元素的时候并没有缩小容量。

4.5 删除指定对象:remove(Object o)

public boolean remove(Object o) {
     
        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) {
     
	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
}
  1. 如果指定对象为 null的话,单独处理,ArrayList是可以存储null 的。 两段处理逻辑本质并没有变化:找到第一个等于指定元素值的元素
  2. 调用 fastRemove方法 快速删除;

fastRemove(int index)相对于remove(int index)少了检查索引越界的操作,并且不会返回已删除的值。

4.6 求两个集合的交集:retainAll(Collection c)

public boolean retainAll(Collection<?> c) {
     
      Objects.requireNonNull(c);
      return batchRemove(c, true);
}

private boolean batchRemove(Collection<?> c, boolean complement) {
     
        final Object[] elementData = this.elementData;
        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;
}
  1. 遍历elementData数组;
  2. 如果元素在c中,则把这个元素添加到elementData数组的w位置并将w位置往后移一位;
  3. 遍历完之后,w之前的元素都是两者共有的,w之后(包含)的元素不是两者共有的;
  4. 将w之后(包含)的元素置为null,方便GC回收;

4.7 根据指定集合,删除原数组中与集合数据相同的数据:removeAll(Collection c)

public boolean removeAll(Collection<?> c) {
     
        Objects.requireNonNull(c);
        return batchRemove(c, false);
}

与retainAll(Collection c)方法类似,只是这里保留的是不在c中的元素。

4.8 清楚所有元素:clear()

public void clear() {
     
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
}
  1. 首先记录修改次数
  2. 循环 size ,将 elementData 里面的元素设置为 null,方便 GC 工作。
  3. 最后将 size 设置为 0。

5. 解答上面疑问:transient Object[] elementData 为何 elementData 要设置成 transient

transient 关键字的意思是不让序列化该关键字修饰的内容
而 ArrayList 它是实现了 java.io.Serializable 接口,这表示它可以被序列化,但是真正存储数据的数组却修饰成了不让序列化。那么这么做有什么意义呢?

原因是:ArrayList 源码里面有 writeObject() 和 readObject() 两个方法,这两个方法声明为private,在只有再 java.io.ObjectStreamClass#getPrivateMethod() 方法中通过反射获取到 writeObject() 这个方法

这样做的目的是:为了自己控制序列化的方式! 因为 elementData 是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量, 所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组,这样减少了空间占用。

6. 总结

  • ArrayList内部使用数组存储元素,当数组长度不够时进行扩容,每次扩容1.5倍空间,remove 和 clear 不会让 ArrayList进行缩容
  • ArrayList添加元素到中间比较慢,因为要挪动元素
  • ArrayList从中间删除元素也比较慢,因为要挪动元素
  • ArrayList支持随机访问,通过索引访问元素很快。

Java集合源码学习—ArrayList_第2张图片


技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。


你可能感兴趣的:(Java,java,arraylist,源码,扩容)