一、ArrayList继承结构
java.lang.Object
java.util.AbstractCollection<E>
java.util.AbstractList<E>
java.util.ArrayList<E>
所有实现的接口
Serializable , Cloneable , Iterable <E>, Collection <E>, List <E>, RandomAccess
笔记
1.为什么其父类AbstractList实现了List接口而ArrayList也实现了List接口?
答:搜索资料最后得出的答案有以下几种:
- 抽象类是一种模板式设计。接口代表了一种规范设计,里面规定的方法具有一定的通用性,实现他的类就要实现其所有的方法。但是对于一些类来说他可能并不需要里面的一些方法,如果全部都实现,是没有必要的。这个时候抽象类就体现出他的作用了,他继承该接口,实现一部分方法,其余的不实现。剩下的方法谁需要谁继承再去实现就好了。
- 更明确的告诉你这个类都继承了或者实现了哪些方法,不用往回一个一个的找了。而且这样也安全,不会漏掉一些抽象的方法
- 源代码作者说:
I’ve asked Josh Bloch, and he informs me that it was a mistake. He used to think, long ago, that there was some value in it, but he since “saw the light”. Clearly JDK maintainers haven’t considered this to be worth backing out later
好吧这就是一个冗余设计,他以前觉得这样写或许会有一些价值,后来就一直这样没改回来。
2.所实现接口说明:
Serializable
:
Serializability of a class is enabled by the class implementing the java.io.Serializable interface. Classes that do not implement this interface will not have any of their state serialized or deserialized. All subtypes of a serializable class are themselves serializable. The serialization interface has no methods or fields and serves only to identify the semantics of being serializable.
就是说类的序列化由实现java.io.Serializable接口的类启用。
不实现此接口的类将不会使任何状态序列化或反序列化。 可序列化类的所有子类型都是可序列化的。 此外该接口没有任何属性和方法,只是一个用于标识可串行化的语义。
Cloneable
:
A class implements the Cloneable interface to indicate to the Object.clone()method that it is legal for that method to make a field-for-field copy of instances of that class.
一个类实现Cloneable接口,以指示Object.clone()方法,该方法对于该类的实例进行现场复制是合法的。
RandomAccess
:
The primary purpose of this interface is to allow generic algorithms to alter their
behavior to provide good performance when applied to either random or
sequential access lists.
此接口的主要目的是允许通用算法更改其行为,以便在应用于随机访问列表或顺序访问列表时提供良好的性能。
这个接口告诉我们,实现了该接口的类支持随机访问,并且用for循环遍历比使用Iterator遍历更有效率。所以遍历Array List最好使用for循环。LinkList没有实现该接口,说明Link List最好用Iterator遍历。
二、字段
//序列化版本ID
private static final long serialVersionUID = 8683452581122892189L;
/**
* .默认初始化化容量为10
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 一个空的数组对象
* 当调用ArrayList(int)构造方法时,若传入0,则会把该对象给elementData
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 当调用ArrayList()构造方法时则会把该对象给elementData,这也就是默认的意思啦
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
*这个就是ArrayList最本质的数据结构,一个普通的Object数组
*注意:它的修饰符为transient,且不是private的
* /
transient Object[] elementData;
/**
1. 数组的大小
*/
private int size;
//数组支持的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
笔记
transient
:表示一个域不是该对象序行化的一部分,当一个对象被序行化的时候,transient修饰的变量的值是不包括在序行化的表示中的。
疑问:为什么序列化的时候不把elementData也序列化,这样不会丢失elementData的数据吗?
答:重点就在他的两个方法
void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException
和
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException
具体实现大家就去看源代码了,他就是在序列化时调用writeObject把size和实际在数组中的元素写进去,然后在反序列化的时候调用readObject恢复正确的size和正常的elementData。之所以这样做是因为如果粗暴的把elementData全部写进去有可能浪费资源,因为elementData里面的容量往往比实际元素的数量要大。
构造方法
- ArrayList()
- ArrayList(int initialCapacity)
- ArrayList(Collection<? extends E> c)
ArrayList()
:实际上执行了this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
这条语句,这个时候他的长度还是0,但是官方注释说Constructs an empty list with an initial capacity of ten.是因为在添加第一个元素的时候他会初始化长度为10,后面我们会验证的。
证实代码:
List<String> list =new ArrayList<>() ;
System.out.println(list.size());
输出为0.
ArrayList(int initialCapacity)
:核心代码为
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
也就是说如果传入initialCapacity不为零则创建一个长度为initialCapacity的数组
为0,则是一个空的数组,这里用到了EMPTY_ELEMENTDATA
ArrayList(Collection<? extends E> c)
:这里重要是elementData = Arrays.copyOf(elementData, size, Object[].class);
这行代码。具体的大家去看吧,这个构造平时用得不多。
笔记
关于ArrayList的初师容量设置的思考
答:虽然现在没讲到该类的自动扩容的机制,其实想必大家都知道ArrayList每次扩容1.5倍。试想一下如果我们的ArrayList需要经常动用扩容算法对程序性能会造成影响,如果我们原本数据量就大,而使用ArrayList()构造,着就会面临多次扩容,比如1千需要分配 11次,如果我们使用ArrayList(int initialCapacity) 设置一个较大的初始容量,那么就会好很多。但是如果我们的数据量大,但是增长量很少的话,使用较大的初始化容量也是不好的,试想一下设置初始化为500,而你的数据量为800,那么就只需要扩容两次,第二次数组长度为1125了,然而你的数据量之后不会增加了,那么就会浪费325个元素空间。所以,在实战过程中合适的初始值的设置是很有必要的。
四、方法
方法这部分是学习的重点部分,本人学识浅薄,有错误还请指出。
- add(E e) 主要研究add(E e)方法
源代码:
//该方法会在数组的最后添加一个元素e
public boolean add(E e) {
//每添加一个元素都要检查是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
进入ensureCapacityInternal(size + 1);方法
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
看到if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
再回想我们的无参构造方法,无参构造方法创建的数组就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,如果相等,则证明该Array List是由无参构造方法得到的,这个时候他的elementData长度为零,接下来执行minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
语句,取默认数组长度和size + 1(就是minCapacity)之间的最大值。
现在试想一下,如果你刚创建以无参构造数组,要添加第一个元素时,size + 1是不是就是1(size初始化为0),这个时候minCapacity就会取值为10(10>1)。
然后进入ensureExplicitCapacity(minCapacity);
记住此时minCapacity为10
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
现在让我们集中注意力关注if (minCapacity - elementData.length > 0) grow(minCapacity);
这两条语句。
minCapacity - elementData.length > 0
这条语句其实就是判断是否扩容的语句,由于我们刚试想的情况是第一次添加元素的情况,minCapacit=10,现在elementData.length=0,所以结果显而易见是需要执行 grow(minCapacity);方法
现在进入grow(minCapacity);
private void grow(int minCapacity) {
// overflow-conscious code
//c按照我们的场景,此时oldCapacity=0
int oldCapacity = elementData.length;
//关键语句!!!扩容规则所在
//此时newCapacity=0,因为oldCapacity为0
int newCapacity = oldCapacity + (oldCapacity >> 1);
//0-10<0为真,这里就是决定我们初始化数组容量为10的关键所在
if (newCapacity - minCapacity < 0)
//执行该语句, newCapacity = 10
newCapacity = minCapacity;
//该语句用于判断数组是否已经扩容到支持的最大数量了,显然我们刚初始化的数组是不可能达到这种程度的 10<MAX_ARRAY_SIZE 不执行
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//这里就是给数组真正初始化的语句了,Arrays.copyOf(elementData, 10);
elementData = Arrays.copyOf(elementData, newCapacity);
}
执行完elementData = Arrays.copyOf(elementData, newCapacity);
后,数组elementData长度为10.
到此我们设想的情况走完了,回到add(E e)方法的最后两条条语句:
//添加元素,且size+1,
elementData[size++] = e;
return true;
到这里大家就知道为什么官方文档会在无参构造方法那么说
Constructs an empty list with an initial capacity of ten.
因为添加第一个元素时minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
这里把minCapacity赋值为DEFAULT_CAPACITY,也就是10.之后执行步骤就在上面。
那么接下来说另外一种情况,就是需要扩容的情况,当我们添加到第十一个元素的时候,也就是原来length=10已经无法满足了。
假设我们数组中已经有10个元素了,此时size=10(因为每次添加一个元素他总是自增),那么回到ensureCapacityInternal(size + 1);
,即传入11。
这时候,(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
判断为false(毕竟又不是空数组了)
接着进入ensureExplicitCapacity(minCapacity);
,传入的仍为11
if (minCapacity - elementData.length > 0)
判断为真,执行扩容方法grow(minCapacity);
传入仍然为11
private void grow(int minCapacity) {
//oldCapacity =10
int oldCapacity = elementData.length;
//newCapacity =10+10*0.5 =15
int newCapacity = oldCapacity + (oldCapacity >> 1);
//15-11>0 false 不执行
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//15-MAX_ARRAY_SIZE<0 不执行
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// OK,最终可以肯定执行Arrays.copyOf(elementData, 15);,也就是得到扩容为15了
elementData = Arrays.copyOf(elementData, newCapacity);
}
最后返回进行正常添加元素
//添加元素,且size+1,
elementData[size++] = e;
return true;
相信看到这里大家已经很明白了ArrayList的初始化和扩容机制了。
你可以这样想,如果你是用ArrayList()创建一个数组,这个数组长度为0,当你添加第一个元素时,容量不够用,进行扩容,不过第一次有点特殊,扩容量是 DEFAULT_CAPACITY(10)。这就是官方所说的Constructs an empty list with an initial capacity of ten。之后每次扩容,都是在原来的基础上提升1.5倍,原因在这条语句int newCapacity = oldCapacity + (oldCapacity >> 1);
这样一直扩容到大于MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8),会执行hugeCapacity(minCapacity)
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
即最后会扩容到Integer.MAX_VALUE,一般不会使用到这个地步,想想Integer.MAX_VALUE是多大啊,是2147483647
其他的添加方法都差不多,搞懂这个其他的也懂了,原理一样的
2.remove(int index)
删除方法其实很简单,简单分析一波就好了
public E remove(int index) {
//检查下标是否越界
rangeCheck(index);
modCount++;
//得到所要删除的元素,用于返回
E oldValue = elementData(index);
//需要移动的位数
int numMoved = size - index - 1;
//大于0,移动
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//把最后的元素设置为null方便回收,size-1
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
其他删除的方法就不细说了,都需要移动元素。此外在用add(int index, E element)
方法时,都需要移动。由此可见,ArrayList不适合用于经常插入删除,这样程序性能会很差。
方法这一部分到此结束,其实还有很多方法,但是理解了这两个,其他的你看源代码都能看得懂,就不一一叙述了。
五、一些需要注意的点
- remove(Object o)方法中有一个判断
if (o == null)
说明,ArrayList是允许数组元素为null的。 - 在add和remove方法中经常可以看到 modCount++, modCount代表数组别修改的次数,他是一个很重要的变量。在进行遍历的时候,如果对数组进行修改,很容易就出现ConcurrentModificationException 异常。关于解决这个异常方法网上很多讨论,建议大家都去看。
- 可以通过使用ensureCapacity方法在操作前增大ArrayList实例的容量。
这可能会减少增量重新分配的数量。来自官方文档
An application can increase the capacity of an ArrayList instance
before adding a large number of elements using the ensureCapacity
operation. This may reduce the amount of incremental reallocation.
总结
ArrayList本质是一个名为elementData的Object[],对随机访问有着很好的性能,不适合用于经常需要插入删除的数据,还能够实现自动扩容。,实现了克隆功能,支持序列化。同时它又是线程不安全的。
以上。注:环境JDK1.8