ArrayList作为最基础的集合类,其底层是使用一个动态数组来实现的,这里“动态”的意思是可以动态扩容(虽然ArrayList可以动态扩容,但却不会动态缩容)。但是与HashMap不同的是,ArrayList使用的是1.5的扩容策略,而HashMap使用的是2的方式。还有一点与HashMap不同:ArrayList的默认初始容量为10,而HashMap为16。
有意思的一点是:在Java 7之前的版本中,ArrayList的无参构造器是在构造器阶段完成的初始化;而从Java 7开始,改为了在add方法中完成初始化,也就是所谓的延迟初始化。在HashMap中也有同样的设计思路。
另外,同HashMap一样,如果要存入一个很大的数据量并且事先知道要存入的这个数据量的固定值时,就可以往构造器里传入这个初始容量,以此来避免以后的频繁扩容。
2 构造器 /** * ArrayList: * 无参构造器 */ public ArrayList() { //DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空实现“{}”,这里也就是在做初始化 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 有参构造器 */ public ArrayList(int initialCapacity) { if (initialCapacity > 0) { //initialCapacity>0就按照这个容量来初始化数组 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //EMPTY_ELEMENTDATA也是一个空实现“{}”,这里也是在做初始化 this.elementData = EMPTY_ELEMENTDATA; } else { //如果initialCapacity为负数,则抛出异常 throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } } 3 add方法 3.1 add(E e)添加指定的元素:
/** * ArrayList: */ public boolean add(E e) { //查看是否需要扩容 ensureCapacityInternal(size + 1); //size记录的是当前元素的个数,这里就直接往数组最后添加新的元素就行了,之后size再+1 elementData[size++] = e; return true; } /** * 第6行代码处: */ private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { /* minCapacity = size + 1 之前说过,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空实现“{}”,这里也就是在判断是不是调用的无参构造器 并第一次调用到此处 */ if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { /* 如果是的话就返回DEFAULT_CAPACITY(10)和size+1之间的较大者。也就是说,数组的最小容量是10 这里有意思的一点是:调用new ArrayList<>()和new ArrayList<>(0)两个构造器会有不同的默认容量(在HashMap中 也是如此)。也就是说无参构造器的初始容量为10,而传进容量为0的初始容量为1。同时这也就是为什么会有 EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA这两个常量的存在,虽然它们的值都是“{}” 原因就在于无参构造器和有参构造器完全就是两种不同的实现策略:如果你想要具体的初始容量,那么就调用有参构造器吧, 即使传入的是0也是符合这种情况的;而如果你不在乎初始的容量是多少,那么就调用无参构造器就行了,这会给你默 认为10的初始容量 */ return Math.max(DEFAULT_CAPACITY, minCapacity); } //如果调用的是有参构造器,或者调用无参构造器但不是第一次进来,就直接返回size+1 return minCapacity; } /** * 第16行代码处: */ private void ensureExplicitCapacity(int minCapacity) { //修改次数+1(快速失败机制) modCount++; /* 如果+1后期望的容量比实际数组的容量还大,就需要扩容了(如果minCapacity也就是size+1后发生了数据溢出, 那么minCapacity就变为了一个负数,并且是一个接近int最小值的数。而此时的elementData.length也会是一个接近 int最大值的数,那么该if条件也有可能满足,此时会进入到grow方法中的hugeCapacity方法中抛出溢出错误) */ if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { //获取扩容前的旧数组容量 int oldCapacity = elementData.length; //这里扩容后新数组的容量是采用旧数组容量*1.5的方式来实现的 int newCapacity = oldCapacity + (oldCapacity >> 1); /* 如果新数组容量比+1后期望的容量还要小,此时把新数组容量修正为+1后期望的容量(对应于newCapacity为0或1的情况) 这里以及后面的判断使用的都是“if (a - b < 0)”形式,而不是常规的“if (a < b)”形式是有原因的, 原因就在于需要考虑数据溢出的情况:如果执行了*1.5的扩容策略后newCapacity发生了数据溢出,那么它就一样 变为了一个负数,并且是一个接近int最小值的数。而minCapacity此时也必定会是一个接近int最大值的数, 那么此时的“newCapacity - minCapacity”计算出来的结果就可能会是一个大于0的数。于是这个if条件 就不会执行,而是会在下个条件中的hugeCapacity方法中处理这种溢出的问题。这同上面的分析是类似的 而如果这里用的是“if (newCapacity < minCapacity)”,数据溢出的时候该if条件会返回true,于是 newCapacity会错误地赋值为minCapacity,而没有使用*1.5的扩容策略 */ if (newCapacity - minCapacity < 0) newCapacity = minCapacity; /* 如果扩容后的新数组容量比设定好的容量最大值(Integer.MAX_VALUE - 8)还要大,就重新设置一下新数组容量的上限 同上面的分析,如果发生数据溢出的话,这里的if条件也可能是满足的,那么也会走进hugeCapacity方法中去处理 */ if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); /* 可以看到这里是通过Arrays.copyOf(System.arraycopy)的方式来进行数组的拷贝, 容量是扩容后的新容量newCapacity,将拷贝后的新数组赋值给elementData即可 */ elementData = Arrays.copyOf(elementData, newCapacity); } /** * 第83行代码处: */ private static int hugeCapacity(int minCapacity) { //minCapacity对应于size+1,所以如果minCapacity<0就说明发生了数据溢出,就抛出错误 if (minCapacity < 0) throw new OutOfMemoryError(); /* 如果minCapacity大于MAX_ARRAY_SIZE,就返回int的最大值,否则返回MAX_ARRAY_SIZE 不管返回哪个,这都会将newCapacity重新修正为一个大于0的数,也就是处理了数据溢出的情况 其实从这里可以看出:本方法中并没有使用*1.5的扩容策略,而只是设置了一个上限而已。但是在Java中 真能申请得到Integer.MAX_VALUE这么大的数组空间吗?其实不见得,这只是一个理论值。实际上需要考虑 -Xms和-Xmx等一系列JVM参数所设置的值。所以这也可能就是MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8) 其中-8的含义吧。但不管如何,当数组容量达到这么大的量级时,乘不乘1.5其实已经不太重要了) */ return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } 3.2 add(int index, E element)