您的位置:首页 > 编程语言 > Java开发

Java集合——ArrayList源码详解

2016-10-20 21:17 375 查看
0. 前言

面试的时候常会被问到ArrayList和Vector的区别。要回答这个问题,首先得对ArrayList的源码进行彻底理解,带着这个问题开始今天的文章。本文原创,转载请注明出处:SEU_Calvin的博客

先对ArrayList的特性进行一个概述:

(1)ArrayList内部使用数组来保存元素。可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。

(2) ArrayList 实现了RandomAccess, Cloneable, java.io.Serializable三个标记接口,表示它自身支持快速随机访问,克隆,序列化。

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable




(3)ArrayList中允许元素为null,因为我们会在后面的源码解析中看到查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理。

(4) ArrayList 不具备并发访问特性,因为所有的方法没有加锁机制。
(5)如果不指定容量大小,默认情况下ArrayList容量为10,在JDk1.7中ArrayList最大容量为 Integer.MAX_VALUE - 8。并且ArrayList 内部具备自动扩容机制,当容量不足时,会自动申请内存空间。

1. ArrayList初始化

private transient Object[] elementData;
//elementData中已存放的元素的个数,而不是elementData的容量
private int size;

public ArrayList(int paramInt)  {
if (paramInt < 0)
throw new IllegalArgumentException("Illegal Capacity: " + paramInt);
this.elementData = new Object[paramInt];
}
public ArrayList()  {
this(10);
}

//两种常用的构造方式
List<String> list = new ArrayList<String>( );//默认构造大小为10
List<String> list = new ArrayList<String>(5);//自定义大小
elementData是用于保存具体元素的数组,构造方法中指定的大小是elementData数组的长度,即初始化开辟一定数量的内存,并不是list.size()即元素个数的值。

elementData属性采用了transient来修饰,表明其不使用Java默认的序列化机制来实例化,但是该属性是ArrayList的底层数据结构,在网络传输中一定需要将其序列化,之后使用的时候还需要反序列化,那不采用Java默认的序列化机制,那采用什么呢?直接翻到源码的最下边有两个方法,发现ArrayList自己实现了序列化和反序列化的方法。

/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that is,serialize it).
* @serialData The length of the array backing the <tt>ArrayList</tt>
*instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
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 array length
s.writeInt(elementData.length);

// 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();
}

}

/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();

// Read in array length and allocate array
int arrayLength = s.readInt();
Object[] a = elementData = new Object[arrayLength];

// Read in all elements in the proper order.
for (int i = 0; i < size; i++)
a[i] = s.readObject();
}


2. ArrayList的add操作

第一种是在数组尾部加入要加入的数据:

public boolean add(E e) {
ensureCapacity(size + 1);//确保对象数组elementData有足够的容量
elementData[size++] = e;//加入新元素e,size加1
return true;
}
第二种当然也可以在指定位置add进数据:

public void add(int index, E element) {
//检查index是否在已有的数组中
if (index > size || index < 0)
throw new IndexOutOfBoundsException("Index:"+index+",Size:"+size);
ensureCapacity(size + 1);//确保对象数组elementData有足够的容量
   //将index及其后边的所有的元素整块后移,空出index位置 
System.arraycopy(elementData, index, elementData, index+1, size-index);
elementData[index] = element;//插入元素
size++;//已有数组元素个数+1
}


3. ArrayList的扩容操作

在add方法里,会看到ensureCapacity()方法的出现,作用是确保数组的容量足够存放新加入的元素,若不够,就会进行扩容操作:

public void ensureCapacity(int minCapacity) {
modCount++; //记录这个list被修改的次数
int oldCapacity = elementData.length;//获取数组容量
//当数组满了,又有新元素加入的时候,执行扩容逻辑
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3) / 2 + 1;//新容量为旧容量的1.5倍+1
//如果扩容后的新容量还是没有传入的所需的最小容量大或等于
//主要发生在addAll(Collection<? extends E> c)中
if (newCapacity < minCapacity)
newCapacity = minCapacity;//新容量设为最小容量
elementData = Arrays.copyOf(elementData, newCapacity);//复制新容量
}
}
在上述代码的扩容结束后,调用了Arrays.copyOf(elementData,newCapacity)方法,这个方法中先创建了一个新的容量为newCapacity的对象数组,然后使用System.arraycopy()方法将旧的对象数组复制到新的对象数组中。新数组大小为原来的1.5倍加1,arraycopy 为System类中的native方法,有比较大的性能开销,所以在我们使用ArrayList时,最好能预计数据的数量并在第一次创建时就申请够内存,否则建议使用LinkedList。如下所示:

//在第一次创建时就申请够内存
ArrayList list = new ArrayList();
list.ensureCapacity(N);
此外,ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。

它可以通过trimToSize方法来实现。代码如下:

public void trimToSize()  {
this.modCount += 1;
int i = this.elementData.length;
if (this.size < i)
this.elementData = Arrays.copyOf(this.elementData, this.size);
}


4. ArrayList的set操作

/**
* 更换特定位置index上的元素为element,返回该位置上的旧值
*/
public E set(int index, E element) {
RangeCheck(index);//检查索引范围
E oldValue = (E) elementData[index];//旧值
elementData[index] = element;//该位置替换为新值
return oldValue;//返回旧值
}
//那么根据某个下标值来取值的方法也就很简单了:
public E get(int index) {
RangeCheck(index);//检查索引范围
return (E) elementData[index];//返回元素,并将Object转型为E
}




5. ArrayList的remove操作

//第一种是根据value值来删除,会从前向后进行遍历,删除第一个出现的元素
public boolean remove(Object o) {
if (o == null) {//移除对象数组elementData中的第一个null
for (int index = 0; index < size; index++){
if (elementData[index] == null) {
fastRemove(index);
return true;
}
}
}
else {//移除对象数组elementData中的第一个
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)//删除的不是最后一个元素
System.arraycopy(elementData, index + 1, elementData, index,numMoved);//删除的元素到最后的元素整块前移
//要删除的是最后一个元素的情况,将最后一个元素设为null
elementData[--size] = null;
}
//第二种是根据下标值来删除某个子元素
public E remove(int index) {
RangeCheck(index);//检查索引范围
E oldValue = (E) elementData[index];//被删除的元素
fastRemove(index);
return oldValue;
}


6. ArrayList的contains操作

//判断动态数组是否包含元素o,是一个很常用的功能
//内部调用的是indexOf()方法,只返回遍历到的第一个满足条件的值
//在遍历时是区分null值和非null值的,这也说明ArrayList是支持null值的。

public boolean contains(Object o) {
return indexOf(o) >= 0;
}
/**
* 返回第一个出现的元素o的索引位置
*/
public int indexOf(Object o) {
if (o == null) {//返回第一个null的索引
for (int i = 0; i < size; i++)
if (elementData[i] == null)
return i;
} else {//返回第一个o的索引
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;//若不包含,返回-1
}


//说到indexOf()就不得不提及lastIndexOf()方法
//用于寻找最后一个出现的元素o的索引位置,原理就是反向对数组进行遍历。
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size - 1; i >= 0; i--)
if (elementData[i] == null)
return i;
} else {
for (int i = size - 1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}


7. ArrayList的遍历操作
//1. 通过迭代器即通过Itreator去遍历
Integer value=null;
Iterator i=list.iterator();
while(i.hasNext()){
value=(Interger)i.next();
}

//2. 随机访问模式,通过索引值去遍历所有元素,这种方式是速度最快的
Interger value=null;
int size=list.size();
fo(int i=0;i<size;i++){
value=(Integer)list.get(i);
}
//3. ForEach循环遍历
//ForEach 编译成字节码之后,本质上还是使用迭代器实现的
//只比迭代器遍历多了生成中间变量这一步,因为性能也略微下降了一些,因此这种方法是速度最慢的
Integer value=null;
for(Integer i : list){
value=i;
}


8. ArrayList和Vector的关系Vector与ArrayList基本是一致的,也是基于数组实现的,都有扩容机制,都允许元素为null。

但是不同点也是很明显的,这也是面试容易被问到的地方:

(1)Vector是线程安全的,会在可能出现线程安全的方法前面加上synchronized关键字。

(2)构造方法上面, Vector比 ArrayList多了一个方法,这个方法能够使Vector指定初始化容量(默认情况下,Vector容量为10)。

public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
(3)两者的扩容方案不同。

//Vector的扩容方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
在 ArrayList中,扩容的时候一般都是增加0.5倍左右,而在 Vector中,如果在构造方法中指定了capacityIncrement的值,就会在原来容量的基础上线性增加 capacityIncrement个,如果没有指定,则默认增加一倍容量。如果设置后的新容量还不够,则直接设置新容量为传入的参数(所需的容量),而后同样用Arrays.copyof()方法将元素拷贝到新的数组。

(4)Vector已经被废弃掉,以下是官方给出的解释:

//If you need synchronization, a Vector will be slightly faster than an ArrayList synchronized with *Collections.synchronizedList.
// But Vector has loads of legacy operations,
//so be careful to always manipulate the Vector *with the List interface
//or else you will not be able to replace the implementation at a later time.

//如果你需要做同步,Vector会比调用Collections.synchronizedList方法的ArrayList更快一点
//但是Vector有很多遗留下来的操作
//所以用List接口操作Vector时要小心,否则之后就不能改变接口的实现了
//说白了就是List list = new Vector()的时候要小心一点

至此关于ArrayList和Vector的区别也总结完毕了。

转载请注明出处:SEU_Calvin的博客。请多点下面的赞支持~ 谢谢




内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: