Java SE 是什么,包括哪些内容(二十一)?
本文内容参考自Java8标准
随机访问列表的一个常用实现类:
ArrayList
类声明:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
}
源代码图示(图下面有解释说明):
英文注释分别阐述了以下内容(一个段落用一个小标):
⑴、类ArrayList是一个实现了接口List,底层实现是数组的一种列表。它有以下特点:
①、能根据需要调整自身容量的大小,完全解除了数组一旦创建就不能改变自身大小的限制(它提供了一些调整底层数组的方法,实际上,看完了本篇博文你会发现,它仍然没有改变数组的大小,只是在需要的时候会重新创建一个新的数组,再将所有的元素复制到新数组中)。
②、实现了接口List的所有方法,能存储任何元素,包括null。
③、与类Vector类似(类Vector是线程安全的),但是不是线程安全的。
⑵、类ArrayList的以下方法的执行时间是一个常数(也就是一个固定的值,和存储元素的多少没有关系):size()、isEmpty()、get() 、set() 、iterator() 、listIterator()。
方法add()的时间也是固定的,即添加一个元素的时间都是一样的。如果添加一个元素的时间为t,那么添加n个元素的时间则为nt。剩余其它方法的运行时间都是和元素个数成线性相关(也就是线性关系,如果元素的个数为n,每个元素的操作时间为t,那么连续操作n个元素的总时间y=nt+a,a是一个固定的常量,这个就是线性关系)。
类ArrayList与另一个列表类LinkedList相比,常数因子较低。也就是ArrayList较简单,在某些操作上花费的时间较少。
⑶、每个ArrayList实例对象都会用一个int类型的变量来存储当前列表底层数组的空间大小。它的值总是大于等于当前列表所存储的元素个数。随着新的元素对象添加到ArrayList实例对象中,这个表示数组存储空间的值会自动增长(实际就是一个更长的新数组替换了老的数组)。
⑷、在需要向一个ArrayList实例对象添加大量的元素之前,可以通过它的方法ensureCapacity(int minCapacity)一次性创建足够的存储空间,这样可以减少多次添加造成的内存开销(比如可能需要分三次添加9个元素,第一次添加3个,第二次添加3个,第三次添加3个,如果直接调用add方法添加,那么当前ArrayList对象实例会在第一次添加之前创建一个3个空间的数组,然后添加元素。在第二次添加之前会重新创建一个6个空间的数组,将之前的3个元素复制到它里面后再将之前的3个空间的数组对象丢弃,然后添加元素。在第三次添加之前会重新创建一个9个空间的数组,将之前的6个元素复制到它里面后再将之前的6个空间的数组对象丢弃,然后添加元素。方法ensureCapacity(int minCapacity)存在的意义是,在这种情况下,你可以使用它直接创建一个9个空间的数组,再进行添加元素的操作)。
⑸、类ArrayList的删除或者添加元素的方法实现未进行线程同步。如果有多个线程同时访问同一个ArrayList对象实例,并且至少有一个线程进行了删除或者添加元素的操作,那必须在它的外部进行线程同步。通常,通过自然封装列表的某个对象进行同步来完成此操作(比如通过关键字synchronize或者类Lock等方式)。
⑹、如果不存在这样一个自然封装列表的对象,则应该使用以下方式对ArrayList对象实例进行同步(通过类Collections的方法synchronizedList(new ArrayList(…))):
// 对ArrayList列表对象进行同步
//通过类Collections的方法synchronizedList(List list)对
//ArrayList列表对象进行同步
List list = Collections.synchronizedList(new ArrayList(...));
最好是在创建ArrayList对象实例的时候就完成同步操作,可以防止对它的意外非同步访问。
⑺、类ArrayList的迭代器Iterator以及ListIterator都实现了快速失败机制(fail-fast),如果在创建了迭代器对象后,通过除迭代器对象本身提供的其它方法修改了列表结构(比如添加、删除元素等),将会引发ConcurrentModificationException异常(如果是通过迭代器对象本身提供的方法修改列表结构,则不会引发这个异常)。
⑻、类ArrayList不能完全保证快速失败机制一定会执行,通常来说,如果在不同步的情况下并发修改一个列表,快速失败机制无法做出任何严格的保证。 但是实现了快速失败机制的迭代器会尽最大努力抛出ConcurrentModificationException异常。因此,完全依赖快速失败机制来保证程序的正确性是错误的,快速失败机制应该仅用于检测错误。
⑼、类ArrayList是集合(容器)框架重要的一员。
下面进行逐行的源代码分析:
1、变量
⑴、serialVersionUID
//static表示属于类(与对象实例无关),final表示一旦赋值就不可变.
//long表示数据类型,它对于类的作用就好比身份证对于人的作用,主要用于序列化与
//反序列化时的版本控制.
private static final long serialVersionUID = 8683452581122892189L;
⑵、DEFAULT_CAPACITY
//static表示属于类(与对象实例无关),final表示一旦赋值就不可变,
//int表示数据类型,初始化值为10.
//它的作用是:如果使用类ArrayList的无参构造方法创建一个
//对象实例,它就是默认的数组大小.
//也可以这么说,在创建类ArrayList的对象实例的时候,一定要提供一个参数
//来指定底层数组的大小,如果未提供,就默认是DEFAULT_CAPACITY。
private static final int DEFAULT_CAPACITY = 10;
⑶、EMPTY_ELEMENTDATA
//static表示属于类(与对象实例无关),final表示一旦赋值就不可变,
//Object[]表示数据类型,初始化值为{},也就是一个空数组对象.
//它的作用是:为类ArrayList接下来的代码中的初始化做准备
//你肯定会有疑问,为什么不在需要使用它的时候再创建,而是提前创建做准备?
//主要是为了后期的代码维护,如果哪里需要就在哪里创建会造成代码分散,维护成本过高
//提前创建后在需要的时候就直接赋值,而需优化修改的时候,仅修改这里一处就行
//已使用它的所有地方都能同步享受优化后的便利
private static final Object[] EMPTY_ELEMENTDATA = {
};
⑷、DEFAULTCAPACITY_EMPTY_ELEMENTDATA
//static表示属于类(与对象实例无关),final表示一旦赋值就不可变.
//Object[]表示类型,初始化值为{},也就是一个空数组对象.
//它与上面的EMPTY_ELEMENTDATA一样,唯一不同的是,当第一个元素添加进来以后
//它知道如何扩张(对应ArrayList的存储空间自动增长)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
};
为什么同时存在EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA两个空数组,需要去对比JDK1.7的ArrayList源码。这是JDK1.8做的一个性能优化。
⑸、elementData
//为了简化嵌套类的访问,它不是private的.
//用关键字transient修饰表示如果序列化当前类会将这个变量排除在外
//Object[]表示类型
//它的作用是:每个ArrayList对象实例持有的底层数组的引用
//在这里,它仅仅代表了一个数组的引用,因为并未初始化。
transient Object[] elementData;
⑹、size
//int表示数据类型,你需要注意的是:没有将它初始化。
//它的作用是:存储当前ArrayList对象实例所拥有的元素的数量
//也可以这么说,size的值始终表示当前ArrayList对象实例存储的元素个数
private int size;
2、构造方法(这里需要提一个前提:ArrayList的所有构造方法都必须要提供一个int类型的参数来指定底层数组的大小,无参的构造方法使用默认的大小,也就是10)
⑴、构造方法一(带一个int类型的形式参数initialCapacity)
//构造方法一,带一个int类型的形式参数int initialCapacity.
//它的作用是:
//创建一个具有指定初始大小initialCapacity的ArrayList对象实例。
public ArrayList(int initialCapacity) {
//如果initialCapacity的值大于0
if (initialCapacity > 0) {
//创建一个具有initialCapacity存储空间的Object类型的
//数组对象实例,将它赋值给引用elementData
this.elementData = new Object[initialCapacity];
//如果initialCapacity的值等于0
} else if (initialCapacity == 0) {
//直接将EMPTY_ELEMENTDATA赋值给对象引用elementData
this.elementData = EMPTY_ELEMENTDATA;
//实际上,这里也可以这么写
//this.elementData = new Object[] {};
//或者this.elementData = {};
//但是一般不会这么做
} else {
//如果initialCapacity的值小于0,直接抛出IllegalArgumentException
//异常。同时输出非法的initialCapacity值的提示信息:
//"Illegal Capacity: "+initialCapacit
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
在这个有参构造方法中你会发现,当参数为0的时候,初始化使用的是EMPTY_ELEMENTDATA。
⑵、构造方法二不带任何的形式参数)
//构造方法二,不带任何形式参数
public ArrayList() {
//直接将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给对象引用elementData
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
在这个无参构造方法中你会发现,初始化使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
⑶、构造方法三(带一个Collection<? extends E>类型的形式参数c)
//构造方法三,带一个Collection<? extends E>类型的形式参数c,
//它的作用是:创建一个包含了指定集合(容器)c所有元素的列表。此列表中元素的顺序
//与指定集合(容器)c中的完全一致。
public ArrayList(Collection<? extends E> c) {
//将集合(容器)c转换成数组,
//然后赋值给elementData
elementData = c.toArray();
//如果转换得到的数组长度不为0
if ((size = elementData.length) != 0) {
//c.toArray不能保证一定返回正确的结果.所以这里需要进一步判断。
//c.toArray()理论上应该返回Object[]类型。然而使用类
//Arrays的方法asList(T...a)得到的list,其toArray()方法返回的数组却
//不一定是Object[]类型的,而是返回它本来的类型。
//例如Arrays.asList(String)得到的list,
//使用toArray会得到String[]类型。
//如果向这个数组中存储非String类型的元素,
//就会抛出ArrayStoreException异常
//这是一个官方bug,可以通过以下地址查看
//https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652
//如果elementData的类型不是Object[]
//类型(方法getClass()获取当前引用的类型)。
if (elementData.getClass() != Object[].class){
//强制将elementData转换成Object[]类型。
//具体可以查看类Arrays的方法public static <T,U> T[] copyOf
//(U[] original, int newLength, Class<? extends T[]> newType)
elementData = Arrays.copyOf(elementData, size, Object[].class);
如果转换得到的数组长度为0
} else {
//则直接将EMPTY_ELEMENTDATA赋值给elementData.
this.elementData