List是一个线性结构的容器,其中实现类ArrayList底层是一个Object数组,它可以根据元素个数自动扩容,也使用到了泛型。相比于List的另一个实现类LinkedList:底层是一条链表。
- ArrayList具有数组的优点:可以根据数组下标直接访问元素。但是由于数组大小不可更改,所以ArrayList在增加元素、删除元素时会有额外的复制数组的开销。
- LinkedList是一条链表,它没有下标,在查找元素的时候,总是从表头开始寻找,效率较低,但是增加、删除元素的时候,只需要更改几个指针的指向就可以,所以它在增加、删除上有更高的效率。
- LinkedList也实现了Deque接口,即它有双向队列的特征。
- Vector继承了和ArrayList一样的父类和接口,但是Vector操作更改数组的方法都用了synchronized修饰,即Vector是线程安全的容器,但是效率没有ArrayList高。
- 还有一个并发的ArrayList :CopyOnWriteArrayList,这个类使用的是同步代码块,而Vector使用的都是同步方法。
一、字段、接口、父类
实现了Serializable接口
ArrayList实现了Serializable接口,但是关于底层的数组,即真正的存储元素的容器是被transient修饰的,所以此数组不能被序列化,但那又为什么要实现Serializable接口呢?
在对ArrayList对象进行序列化的时候,它会自动调用内部定义的writeObject(ObjectOutputStream )方法(对象流调用了writeObject(object)方法,被序列化的对象也调用writeObject()方法)。它的作用是什么呢,序列化就是将对象的信息转化为二进制流从而可以存储在文件中或者在网络中传输,于是我们需要这个二进制流 小但是不缺失信息,但是对于ArrayList,它的数组大小可能很大但是存储的元素却很少(可以在构造器中指定数组大小,然后只加入一个对象),序列化为二进制流时,我们不需要那些不必要的值(0、NULL...),所以ArrayList内部定义了这个方法,对于底层的数组只序列化==size==的大小,size大小为数组中真实元素的个数,从而节省了空间。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioral compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
复制代码
很多集合类中都是使用这种序列化方式。
DEFAULT_CAPACITY
如果没有在构造方法中指定数组大小,则默认容量为10
private static final int DEFAULT_CAPACITY = 10;
复制代码
EMPTY_ELEMENTDATA
这里有个奇怪的点:
ArrayList中有两个属性:
private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 复制代码
两个差不多的属性:按照属性名来看的话一个是 空的ELEMENTDATA,一个是默认空的ELEMENTDATA 。这里在构造方法中还会提到。
elementData
这个就是ArrayList中真实的存储数据的数组了
transient Object[] elementData;
复制代码
还有一个size属性,是数组中真实存储的元素的个数,简单来说就是加入元素size增加,移除元素size减少。
MAX_ARRAY_SIZE
底层数组长度最大值:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
复制代码
二、构造方法
1、空参构造器
默认将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给底层数组。
2、有参构造器
1)、构造器参数是Collection<? extends E>,即当前构造器只接受类型参数是当前类型参数的子类的实现了Collection接口的对象,如果Collection长度为0,elementData被赋值为EMPTY_ELEMENTDATA。
2)、构造器参数是整型,指定底层数组的大小,如果指定大小为0,则把EMPTY_ELEMENTDATA赋给elementData。
三、比较有趣的方法
grow()方法
显然ArrayList底层是一个数组,那么为什么ArrayList比直接使用数组更方便呢,一大原因即:数组初始化过后不能扩展,ArrayList容器为我们做了底层数组扩展的事情。 那么它是怎样扩展的呢。
简单来说就是在向ArrayList容器中add元素的时候,它会判断size == elementData.length,如果是true,会调用grow()方法进行扩容,默认扩容为1.5倍(oldCapacity + (oldCapacity >> 1);),如果扩容后还是不够(调用方法时它会传入minCapacity,大小为当前size+1),多半发生在没有指定容量(那么之后默认容量为Math.max(DEFAULT_CAPACITY, minCapacity)),或者指定容量为0、1,因为0、1右移后是0(之后容量为minCapacity)。
grow()方法有两个重载形式,一个空参,一个参数为int ,前者主要是在add时调用,后者是在addAll时调用。
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
private Object[] grow() {
return grow(size + 1);
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
复制代码
clone()方法
重写的clone()方法是浅拷贝,没有拷贝集合中的元素。
listIterator()方法
list专用迭代器方法,可以进行前后双向遍历,ArrayList中有实现了Iterator接口的内部类和另一个实现了ListIterator的内部类。而Iterator()方法和listIterator()方法就是返回这些内部类实例的方法。
subList(int fromIndex, int toIndex)方法
截取ArrayList中的一段,返回值是ArrayList的内部类,内部类方法和ArrayList的方法差不多。
foreach(consumer ...)方法
接受一个消费者接口,遍历所有元素对其进行操作。
四、总结
ArrayList底层是一个数组,它是一个查找、更新效率高,增删效率低的一个数据结构,因为增删会频繁地进行数组的复制操作。默认数组长度为10,当增加元素时,元素个数等于数组长度就会开始扩容,扩容倍数是1.5。
为什么ArrayList可以使用for(xx : xx) 语法,因为此语法内部调用迭代器,一般称此语法为foreach(增强for循环),注意此语法接受每个容器内的元素并赋值给一个局部变量,修改局部变量不会对容器产生影响。
五、关于Iterable接口和Iterator接口
Iterable接口中有可以一个返回值为Iterator接口的方法,Iterable接口指可以迭代,而Iterator接口才是真正的迭代器。