【Java从头开始到光头结束】No4.JDK1.8 ArrayList源码学习及扩展

JAVA 之ArrayList

书中自有黄金屋,书中自有颜如玉
————————————————————————————————————
本文在《码出高效:Java开发手册》书本讲解内容的基础上,将和大家一起对JDK1.8版本中的ArrayList源代码进行分析及拓展,争取将ArrayList周边的知识点做一个全面的复习回顾。

上一个图,我们今天看红色框List的ArrayList部分:
在这里插入图片描述
其中 红色代表接口 蓝色代表抽象类 绿色代表并发包中的类,灰色(Vector,Stack)代表早期线程安全的类(基本已经弃用)。

单从ArrayList的继承和实现接口关系来讲,下图会更为清晰
在这里插入图片描述
List 集合是线性数据结构的主要实现,集合元素通常存在明确的上一个和下一个
元素,也存在明确的第一个元素和最后一个元素(所以我们List家族中的实现类都是有序的)。 并且List 集合的遍历结果是稳定的。该体系最常用的是 ArrayList ,LinkedList 两个集合类。
ArrayList 是容量可以改变的非线程安全集合。内部实现使用数组进行存储,集合扩容时会创建更大的数组空间,把原有数据复制到新数组中。 ArrayList 支持对元素的快速随机访问,但是插入与删除时速度通常很慢,因为这个过程很有可能需要移动其他元素。
——————————————————————————
上边是对ArrayList特点的一个总结,下来我们开始源码学习

  1. ArrayList的初始化
    集合初始化通常进行分配容 、设置特定参数等相关工作。简要说明初始化的相关工作,并解释为什在任何情况下,都需要显式地设定集合容器的初始大小
    直接上代码:
/**
 * Default initial capacity.
 * 默认初始容量为10
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * Shared empty array instance used for empty instances.
 * 空的数组。
 */
private static final Object[] EMPTY_ELEMENTDATA = {
    
    };

/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 * 同样是空的数组,此空数组对象用来判别何时第一个元素加入。
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
    
    };

/**
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 * 这就是我们存储 ArrayList 真正数据的数组
 * transient 关键字我们后边再聊
 */
transient Object[] elementData; // non-private to simplify nested class access

/**
 * The size of the ArrayList (the number of elements it contains).
 *
 * @serial
 * 数组的大小,也可以理解为数组中存储数据单元的个数。
 */
private int size;

ArrayList的内部属性值没什么需要特别说明的,下来我们看一下构造方法:

/**
* Constructs an empty list with an initial capacity of ten.
* 构造初始容量为10的空列表
* 在1.8之前,默认的无参构造容量为10,在1.8后默认的构造容量为0,在第一次add一个元素时会对容量进行一个分配,容量为默认值10,后边会详细说明。
*/
public ArrayList() {
    
    
   this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
* Constructs an empty list with the specified initial capacity.
*
* @param  initialCapacity  the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
*         is negative
* 初始带容量的构造方法,简单易懂
* 当你传的值大于0,则就按照你穿的值设定数组的初始容量
* 当你传的值等于0,则使用默认的空数组 EMPTY_ELEMENTDATA
* 当你传的值小于0,抛出非法参数异常
*/
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);
   }
}

/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
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)
很多小伙伴看到这块代码可能和我第一次看见一样有点懵,这是个嘛玩意,查了一下发现,这是个JAVA官方的bug,附上一个查bug的信息的网站
Java Bug Database
see 6260652 就是他的bug id,据说在1.9中已经被解决了,这里1.8的解决方法就是在后边加了一个判断,如下边代码,需要判断数组类型是否是Object,不是得话就需要把他转换为Object类型,再拷贝赋值。

       if (elementData.getClass() != Object[].class)
           elementData = Arrays.copyOf(elementData, size, Object[].class);

然后这个bug的问题就如注释中说到的,因为c.toArray()这个方法返回的不一定是Object数组,所以有问题,下边我们详细看一下,
首先确定我们ArrayList集合里边存数据的数组对象elementData是一个Object数组,而java中Object数组对象引用是可以指向非Object数组的,问题就在这,我们看代码

// 首先我们创建一个类myList继承自ArrayList,并重写toArray()方法
// 使得场景先满足第一个条件,toArray()方法返回的不是Object数组
public class MyList<E> extends ArrayList<E> {
    
    
	private static final long serialVersionUID = 8068784835731821475L;
	@Override
	public String[] toArray() {
    
    
		// 这里把返回值先写死,方便
		return new String[] {
    
    "1"};
	}
}
// 接下来看这一段
public static void main(String[] args) {
    
    
	// 创建一个MyList
	MyList<String> mylist = new MyList<>();
	
	// 模拟我们的elementData Object数据
	// 并将返回的String[] 给到 elementData对象。
	// 这里不会有问题,并且elementData看起来还是像一个存储Object对象类型的数组
	Object[] elementData = mylist.toArray();
	// 接下来我们给这个看起来像Object数组的elementData赋一个Integer的值
	elementData[0] = new Integer(1);
	// 最后会报一个错
	// Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
	// 原因就是这里的elementData对象虽然看起来像是一个Object数组,但是他底层
	// 指向的对象已经是一个String类型的数组了,无法再加入String类型以外的对象
	// 所以集合中我们需要保持elementData数组对象指向的是一个Object数组
	// 而这个数组能加入什么类型的元素让泛型去决定,保持数组元素类型的一致性。
}

构造方法看完了,然后我们来看一下为什么大家都是在说ArrayList集合的增删速度慢,我们这边一步一步来,先说明一下Arrays类,引用书中的内容

Arrays 是针对数组对象进行操作的工具类,包括数组的排序、查找、对比、拷贝
等操作。尤其是排序,在多个 JDK 版本中在不断地进化,比如原来的归并排序改成
Timsort ,明显地改善了集合的排序性能。另外,通过这个工具类也可以把数组转成集合。

因为ArrayList的底层是数组,所以我们在ArrayList的源代码中经常能找到Arrays工具类的身影。我们先来看一下最简单的新增一个单独对象的无参方法add(),这里我把它用到的相关的方法也一起列出来了,直接在备注上进行说明

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 * add中主要的方法就是ensureCapacityInternal()
 * 除了这个方法就直接赋值了,所以我们主要看这个方法干了啥
 * 这方法里边其实还调了几个方法,我们把他们直接进行编号①→②→③→④
 */
public boolean add(E e) {
    
    
	// 给①传递的参数是当前存储的元素个数加1
	// 这里强调一下,这个size就是elementData数组当前存储元素的个数
	// 和elementData.length是不一样的,length是表示数组长度,就是最多能存多少元素
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

// ①
// 【ensure→确保 Capacity→容量 Internal→内部】 :确保内部容量,方法没有返回值
// 也就是说假如你之前数组已经存满了,如果没调用这个方法,
// 再往里边加数据 elementData[size++] = e;的话,就加不进去了吧,而且会下表越界
// 所以这里这个方法,里边肯定会有对容量的判断及修改。我们先看③
private void ensureCapacityInternal(int minCapacity) {
    
    
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// ②
// 【ensure→确保 Explicit→明确的 Capacity→容量】 :确保明确的,正确的容量,方法没有返回值
// 参数就是②返回的容量数
private void ensureExplicitCapacity(int minCapacity) {
    
    
	// 这个是父类的属性,用来记录修改次数,和这里的逻辑没有关系,暂时忽略
    modCount++;

    // overflow-conscious code
    // ②方法为我们返回了最新的容量,也就是现在数组中需要存储的元素个数
    // 但是我们得确保元素在数组中能否放的下,先简单重复一下,
    // 我们java中的数组类型,初始化后容量无法改变,我们知道ArrayList的底层是使用数组的,而ArrayList集合的数量大小又是可以改变的,是怎么做到的呢
    // 答案就是使用Arrays工具类,创建一个扩容后大的数组,把原数组内容拷贝过去,就是生成了一个新的数组,就是扩容拷贝这一步导致了在某些新增时间段ArrayList的速度慢。
    if (minCapacity - elementData.length > 0)
    	// 这里判断最新容量是否大过了数组长度,如果超过了,就进行扩容拷贝
    	// 如果没有超过,就直接在数组的对应位置上赋值就可以了,这个新增速度还是可以的
    	// 具体的扩容方法在grow()方法中,我们接下来就去看④
        grow(minCapacity);
}

// ③
//【calculate→计算 Capacity→容量】 :计算容量,静态方法,有返回值
// 入力参数为我们【存数据的数组elementData】和【最新的追加一个元素之后的元素个数】。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    
    
	// 这里先判断elementData数组是不是空数组,当我们用默认空参数的构造方法构建时,elementData就是空数组。
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    
    
    	// 如果是空数组,使用传进来的最新元素个数和默认值为10的定值进行比较,返回较大值
    	// 这里就是我们经常说的默认数组容量的分配地点了
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 如果不是空数组就把最新的容量值返回
    // 我们再看②
    return minCapacity;
	// 方法的最终目的就是返回新增元素后的容量大小,默认最小容量为10。
}

/** 
 * ④
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 * 增加容量以确保它至少可以容纳由最小容量参数指定的元素数。
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    
    
    // overflow-conscious code
    // 这里先保留了我们当前数据的容量长度,保留干啥呢,接着往下看
    int oldCapacity = elementData.length;
    // 扩容,获得最新的容量值newCapacity,其值为旧的数组长度加上一个值,我附上一段书上的备注:JDK6之前扩容50%或50%-1,但是取ceil,而之后版本取floor
    // 这里(oldCapacity >> 1)的值可以理解为oldCapacity的一半
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 正数带符号右移的值肯定是正值,所以oldCapacity+(oldCapacity >> 1)的结果可能超过int可以表示的最大值,反而有可能比参数minCapacity更小
    // 所以我们需要判断newCapacity和参数minCapacity的大小
    if (newCapacity - minCapacity < 0)
    	// 如果newCapacity大小确实超过了int可以表示的最大值
    	// 反而此时比minCapacity更小,则此时容量值就直接设置为minCapacity的值
        newCapacity = minCapacity;
    // 再下来判断此时的新容量newCapacity是否超过了数组最大长度
    // → MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    	// 如果超过了就调用hugeCapacity方法来得到更大的容量
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    // 翻译:最小容量通常接近大小,所以这是一个胜利
    // 这里就是我们上边所说的进行扩容拷贝了,返回值是Object[]
    elementData = Arrays.copyOf(elementData, newCapacity);
}

总结一下:
其实ArrayList的无参插入方法速度大部分时间还行,因为这里的的插入都是在数组最后边插入,除过要扩容的时间点,大部分时间不需要拷贝数据到另一个数组,跨数组拷贝数据才是慢的根本所在,试想一下,ArrayList使用无参构造时,默认大小为 10,也就是说在第一次 add 的时候分配为 10 的容量,后续的每次扩容都会调用 Arrays.copyOf 方法,创建新数组再复制。可以想象,假如需要将 1000 个元素放置在ArrayList中,采用默认构造方法,一个一个加入,则需要被动扩容 13 次才可以完成存储。反之,如果在初始化时便指定了容量 new ArrayList(1000), 那么在初始化ArrayList对象的时候就直接分配 1000 个存储空间,从而避免被动扩容和数组复制的额外开销。最后,进一步设想,如果这个值达到更大量级, 没有注意初始的容量分配问题,那么无形中造成的性能损耗是非常大的,甚至导致 OOM 的风险。(out of memory 内存溢出)
另外就是强调一下,有参的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++;
    }

至于addAll()方法我们就不继续看了,和add()方法差不多,无非就是加入的数据量不一致罢了,实现都是同样的套路,有兴趣的童鞋可以下去自己瞅瞅。
嗯,我们已经看完了插入方法,下来看删除方法↓
删除方法我们还是只看单个元素删除的,删除多个的原理都差不多,
这里有列出三个方法,①remove(int index),②remove(Object o),以及被这两个方法使用的内部私有方法 ③fastRemove(int index),下边直接看代码(过长的原有注释被我删除了)

/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).
 *
 * @param index the index of the element to be removed
 * @return the element that was removed from the list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 * ①
 * 参数是要删除的元素的下标
 */
public E remove(int index) {
    
    
	// 这里rangeCheck方法是判断下标是否越界的
    rangeCheck(index);

    modCount++;
    // remove方法最后会返回被删除掉的元素,就是在这里获取的
    E oldValue = elementData(index);

	// 这个numMoved变量就是在我们删除元素之后需要移动的元素数量
	// 写为size - (index + 1)可能比较好理解一点,假如你集合中现在有十个元素
	// 你现在要删除第九个元素,下标为8,通过计算得出numMoved为1,也就是需要
	// 向前移动一个元素,就是原先集合的第十个元素。
    int numMoved = size - index - 1;
    // 这里什么情况下不需要移动元素呢,就是当你删除的是最后一个元素时。
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 这里删除方法就比较残暴,直接指向null,让垃圾回收器去回收已经删除的元素。
    elementData[--size] = null; // clear to let GC do its work

	// 这里我们可以发现的就是,ArrayList无论是插入还是删除,只要涉及到元素的拷贝
	// 就会慢,当你删除或者插入元素在ArrayList尾部的时候,其实速度还是可以的。
	
    return oldValue;
}

/**
 * @param o element to be removed from this list, if present
 * @return <tt>true</tt> if this list contained the specified element
 * ②
 * 参数就是要删除的元素的对象
 */
public boolean remove(Object o) {
    
    
	// 首先我们ArrayList是能存null的,当然也能删除null
	// 把这个判断单独提出来就是为了防止空指针,因为判断元素相等是需要比较的
    if (o == null) {
    
    
    	// 至于为什么要循环,因为我们删除数组元素还是需要他的下标的,这里速度就已经有慢的隐患了
        for (int index = 0; index < size; index++)
        	// 这里其实只能删除掉第一个值为null的元素
        	// fastRemove在下边再看
            if (elementData[index] == null) {
    
    
                fastRemove(index);
                return true;
            }
    } else {
    
    
        for (int index = 0; index < size; index++)
        	// 这里就是删除一个不是null值元素的地方了
            if (o.equals(elementData[index])) {
    
    
            	// 主要就一个fastRemove方法,我们看下边
                fastRemove(index);
                return true;
            }
    }
    return false;
}

/*
 * Private remove method that skips bounds checking and does not
 * return the value removed.
 * ③
 * 参数为删除元素的下标
 */
private void fastRemove(int index) {
    
    
	// 惊不惊喜,意不意外,这里fastRemove和remove(int index)方法几乎一模一样,唯一的区别就是fastRemove方法里不需要下标越界的判断了。。。
    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
}

这里我们最后发现,上边两个remove方法,都只能一次删除掉一个元素,即使第二个remove方法参数传递的是一个Object,如果我们这边需要删除掉集合中多个一致的值的时候怎么办呢,答案就是使用removeAll(Collection<?> c)方法,他会删除掉所有和你传进来的数组中值一致的元素。看一下测试代码:

List<String> list1 = new ArrayList<String>();
list1.add("aaa");
list1.add(null);
list1.add("bbb");
list1.add(null);

System.out.println(list1.size());
System.out.println(list1);

List<String> list2 = new ArrayList<String>();
list2.add(null);

list1.removeAll(list2);
System.out.println(list1.size());
System.out.println(list1);

输出结果如下
4
[aaa, null, bbb, null]
2
[aaa, bbb]

这里源码基本上读完了,下来进入我们的扩展内容,依旧和ArrayList有关,主要是数组和集合的关系

数组与集合都是用来存储对象的容器,前者性质单一,方便易用,后者类型安全,功能强大,且两者之间必然有互相转换的方式。毕竟它们的性格迥异,在转换过程中,如果不注意转换背后的实现方式,很容易产生意料之外的问题。

下来我们就聊一下数组与集合的相互转换
首先是第一种情况,数组转集合,以 Arrays.asList()为例,它把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出UnsupportedOperationException 异常。示例代码如下:

String[] stringArray = new String[3];
stringArray[0] = "aaa";
stringArray[1] = "bbb";
stringArray[2] = "ccc";

List<String> list1 = Arrays.asList(stringArray);
list1.set(0, "uzi out");
System.out.println(list1.get(0));

// 以下三行编译正确,执行都会报错
list1.add("ddd");
list1.remove(1);
list1.clear();

执行结果为:
uzi out
Exception in thread "main" java.lang.UnsupportedOperationException

这里set方法是没有问题的,那是为什么不能使用add/remove/clear方法呢,因为Arrays.asList 体现的是适配器模式,后台的数据仍是原有数组,set()方法即间接对数组进行值的修改操作。asList 的返回对象是一个Arrays 的内部类,它并没有实现集合个数的相关修改方法,这也正是抛出异常的原因。
Arrays.asList 的源码如下:

public static <T> List<T> asList(T... a) {
    
    
    return new ArrayList<>(a);
}

返回的明明是 ArrayList 对象,怎么就不可以随心所欲地对此集合进行修改呢?
注意此 ArrayList 非彼 ArrayList ,虽然 Arrays 和ArrayList 同属于一个包,但是在
Arrays 类中还定义了一个 ArrayList 的内部类(或许命名为 InnerArrayList 更容易识别),根据作用域就近原则,此处的 ArrayList 是李鬼,即这是个内部类。此李鬼相对于1.6,1.7已经追加了很多实现方法了,但是依旧没有add/remove/clear方法的实现,大都是一些遍历取值改值的方法实现,代码如下:

/**
* @serial include
*/
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();
   }

   @Override
   @SuppressWarnings("unchecked")
   public <T> T[] toArray(T[] a) {
    
    
       int size = size();
       if (a.length < size)
           return Arrays.copyOf(this.a, size,
                                (Class<? extends T[]>) a.getClass());
       System.arraycopy(this.a, 0, a, 0, size);
       if (a.length > size)
           a[size] = null;
       return a;
   }

   @Override
   public E get(int index) {
    
    
       return a[index];
   }

   @Override
   public E set(int index, E element) {
    
    
       E oldValue = a[index];
       a[index] = element;
       return oldValue;
   }

   @Override
   public int indexOf(Object o) {
    
    
       E[] a = this.a;
       if (o == null) {
    
    
           for (int i = 0; i < a.length; i++)
               if (a[i] == null)
                   return i;
       } else {
    
    
           for (int i = 0; i < a.length; i++)
               if (o.equals(a[i]))
                   return i;
       }
       return -1;
   }

   @Override
   public boolean contains(Object o) {
    
    
       return indexOf(o) != -1;
   }

   @Override
   public Spliterator<E> spliterator() {
    
    
       return Spliterators.spliterator(a, Spliterator.ORDERED);
   }

   @Override
   public void forEach(Consumer<? super E> action) {
    
    
       Objects.requireNonNull(action);
       for (E e : a) {
    
    
           action.accept(e);
       }
   }

   @Override
   public void replaceAll(UnaryOperator<E> operator) {
    
    
       Objects.requireNonNull(operator);
       E[] a = this.a;
       for (int i = 0; i < a.length; i++) {
    
    
           a[i] = operator.apply(a[i]);
       }
   }

   @Override
   public void sort(Comparator<? super E> c) {
    
    
       Arrays.sort(a, c);
   }
}

数组上的final关键字使得数组引用始终被强制指向原有数组,并且你会发现他没有实现add/remove/clear方法,至于这个UnsupportedOperationException异常,是其父类AbstractList报出来的
在这里插入图片描述
所以,如果你只是查看集合内容,那这么转换是没有问题的,如果你要修改转换后的集合,那请使用如下代码:

List<Object> objectList = new java.util.ArrayList<Object>(Arrays.asList(数组));

下来我们看一下集合转数组,
其实集合转数组更加简单可控,但是还是要注意一些问题,我们看代码

List<String> list = new ArrayList<String>();
list.add("aaa"); 
list.add("bbb"); 
list.add("ccc"); 

// 第一处
// 泛型丢失,无法使用String[]来接受返回的结果,因为toArray()方法返回的是object[]
Object[] arrayl = list.toArray(); 

// 第二处
// 数组长度小于集合元素个数
String[] array2 = new String[2] ; 
list.toArray(array2); 
System.out.println(Arrays.asList(array2)) ; 

// 第三处
// 数组长度等于集合元素个数
String[] array3 = new String[3]; 
list.toArray(array3);
System.out.println(Arrays.asList(array3));

执行结果如下
[null, null]
[aaa, bbb, ccc]

第一处好理解,不要使用toArray()无参方法将集合转化为数组,这样会导致泛型丢失。第二处,编译没错,运行也没错,结果输出null,第三处,成功将集合元素复制到了数组中,2和3的区别就在于即将复制进去的数组容量是否足够。如果容量不够,则弃用此数组,另起炉灶,关于此方法的源码如下:

@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    
    
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

所以第二处会成功复制集合元素到一个数组中,并且返回这个数组,只不过此时就和你传进来的那个容量不足的数组没有关系了。

到这了,我们再聊一下transient关键字,transient Object[ ] elementData,我们真正存数据的这个集合就是被transient修饰的,这个关键字表示的是此字段修饰的对象在类的序列化时将被忽略。因为集合序列化时系统会调用 writeObject 写入流中,在网络客户端反序列化的 readObject 时,会重新赋值到新对象的 elementData中。为什么多此一举,因为 elementData 容量经常会大于实际存储元素的数 ,所以只需发送真正有实际值的数组元素即可。

还有,我们测试一下三种情况,分别为入参数组容量不够时、入参数组容量刚好时,以及入参数组容量超过集合大小时,并记录其执行时间,这里我直接截图了,就不敲了
在这里插入图片描述
在这里插入图片描述
所以,当我们集合转数组时,如果不会再往数组里边加入元素,请尽量给到一个和集合大小一样的数组,这样效率会最高。
最后再提一嘴subList

public List<E> subList(int fromIndex, int toIndex) {
    
    
        subListRangeCheck(fromIndex, toIndex, size);
        return new SubList(this, 0, fromIndex, toIndex);
    }

这个方法返回的也是一个内部类,并且注意他没有实现序列化接口,小心踩坑
在这里插入图片描述

最后总结一下
ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。


ArrayList的内容大概就到这里了,非常感谢《码出高效:Java开发手册》这本书及写这本书的阿里大佬,活到老学到老,我们下次再见。

猜你喜欢

转载自blog.csdn.net/cjl836735455/article/details/106480541
今日推荐