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

jdk源码阅读一:ArrayList

2017-01-14 22:59 831 查看

ArrayList介绍



ArrayList是我们使用的最多的集合类了。特点主要如下:

基于数组实现底层存储,容量可以自动扩展。

实现List全部接口。

允许null对象

提供操作数组容量大小的方法。

粗略来看和Vector类相同,但是 Vector在 方法上加了同步,所以Vector是线程安全的。

size,isEmpty,get,set,iterator和listIterator运行时间为常量。

add方法 如果添加n个元素 时间复杂度O(n)

ArrayList非 thradsafe,所以自己wrap或者用Collections#synchronizedList

返回的iterator是fail-fast,如果iterator创建后,修改了list结构则会抛出异常ConcurrentModificationException,

注意这里异常表明是一个bug,需要避免,而不能处理业务逻辑。

size 变量

size 表示当前list中存储了元素的个数,初始化为0。add一个元素加一,删除一个元素减一。

capacity(容量)增长策略

每个ArrayList示例都有一个capacity。表示数组(list中用来储存元素)的大小。

capacity至少和size相等,不会小于它。capacity大小是自动增长的。你也可以调用ArrayList#ensureCapacity方法改变容量。

不过如果你将容量改小 是不起作用的,ensureCapacity方法不允许。

增长容量的方法有三个。



ensureCapacity

` /**
* Increases the capacity of this <tt>ArrayList</tt> instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param   minCapacity   the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;

if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}`


说明:

public的,说明是提供给用户操作的,因为其他两个方法是private的。这个方法Arraylist内部是没有调用的。内部扩容调用的是ensureCapacityInternal方法。

增加容量如果有必要:原则就是判断minCapacity 和当前的capacty大小,如果大则增加。

minExpand(最小扩展容量)可能是0 ,也可能是DEFAULT_CAPACITY=10。

一般ensureCapacity类型的三个方法只是确保最小容量满足,但是真正的增长策略可能不同,比如add一个元素,minCapacity=size+1。

这时候如果需要扩容(minCapacity>size)那么数组只需要增大1,即可,但是这样后面每次都要复制,所以后面真正讲到的grow方法策略不是+1,而是然后大小增加150%。

源码分析:

minExpand表示最小拓展容量

首先我们看下ArrayList创建的两种创建方法和他的两个常量

ArrayList arr = new ArrayList<>();

ArrayList arr = new ArrayList<>(0);

/**
* 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 = {};


如果使用第一种创建方法那么this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

如果使用第二种创建方法 则this.elementData = EMPTY_ELEMENTDATA;

什么意思?就是说如果你没穿参数,那么Arraylist 设置 默认空数组。如果你传了个0,则认为是你故意设置容量为0的。它用上面两个常量来区分这种行为,区分的目的在于对待这两种不同的空数组, 第一次的容量增长策略不同。

下面我们接着说ensureCapacity方法,就好解释多了,如果elementData!=DEFAULTCAPACITY_EMPTY_ELEMENTDATA 那么minExpand最小扩展容量=0,如果否则minExpand = DEFAULT_CAPACITY=10。

委托给ensureExplicitCapacity方法,扩容

然后比较传入参数和minExpand的大小。取其中大者。然后委托给ensureExplicitCapacity方法。

ensureExplicitCapacity

`private void ensureExplicitCapacity(int minCapacity) {
//修改次数 计数器 在iterator迭代出现并发修改快速失败就是利用到该字段
modCount++;

// overflow-conscious code
// 如果minCapacity大于数组长度则扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}`


grow方法是真正的增长策略方法:

/**
* 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;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}


方法说明:

1. newCapacity先扩展到为原来的1.5倍。

2. 比较newCapacity和minCapacity选择大的。

3. newCapacity如果超过MAX_ARRAY_SIZE(= Integer.MAX_VALUE - 8),则调用hugeCapacity,增大到Integer.MAX_VALUE。

4. 调用Arrays.copyOf调整elementData到新容量。

ensureCapacityInternal

private void ensureCapacityInternal(int minCapacity) {
//如果满足elementData是默认空数组,则比较 minCapacity和DEFAULT_CAPACITY 取大者
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}


到此容量增长策略已经分析完成。

扩容方法总结:

ensureCapacity是public给开发人员调用的api,内部是没有调用的。

ensureCapacityInternal才是内部判断是否需要扩容的方法。

上面两个方法都是调用ensureExplicitCapacity实现。

ensureCapacityInternal和ensureExplicitCapacity是私有的。

这三个方法是判断是否需要扩容,但是不一定真正执行扩容动作,真正的方法是grow。

如果我们能够提前预知list容量 直接赋值容量,可以减少后面自动扩容导致的数组复制的开销,调用ensureCapacity直接到一个比较大的容量也是可行的。

List常用方法分析



插入元素

顺序插入add(E e)

public boolean add(E e) {
ensureCapacityInternal(size + 1);  // Increments modCount!!
elementData[size++] = e;
return true;
}


确保容量加1,然后直接数组赋值。时间复杂度O(1)。

随机插入

public void add(int index, E element) {
//检查index是否在 0-size范围内
rangeCheckForAdd(index);

ensureCapacityInternal(size + 1);  // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}


因为是随机插入,所以要把插入位置index以及以后的元素向后移动一位。所以速度会慢些。

插入集合addAll

这里不再细讲,大致思路是将Collection转成数组然后插入数组。不过如果是随机插入容量足够 还是复制了两次(第一次向后移动n位,第二次赋值Collection 到指定位置)。

clear清空

很简单 循环将数组设置null

检索操作

主要包括contains,containsAll,indexOf,lastIndexOf。

containsAll循环调用的contaions,contaios调用的indexOf。

indexOf和lastIndexOf差不多,一个从数组顺序查找,一个从数组逆序查找。我们主要看下lastIndexOf。

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;
}


方法也很简单,但是时间复杂度还是非常高的,平均O(n*n)。

get 随机获取元素

速度很快O(1),会先检查index是否合法[0,size)。

remove方法

有四个,根据元素删除,需要遍历数组,找到位置,然后移动数组。根据index删除,直接移动数组。效率都不高。

removeIf是1.8新加的,根据传入lambda 表达式判断是否删除元素,会应用于每个元素,内部通过迭代器遍历元素,判断是否需要该元素。

迭代器

有iterator,listiterator 和Spliterator。

iterator主要通过内部类实现,存储了一个cursor,默认无参的=0。

listiterator继承了iterator,实现了可以向前遍历。

Spliterator(splitable iterator可分割迭代器)接口是Java为了并行遍历数据源中的元素而设计的迭代器,这个可以类比最早Java提供的顺序遍历迭代器Iterator,但一个是顺序遍历,一个是并行遍历。

可以参考:https://segmentfault.com/q/1010000007087438

iterator分析

public Iterator<E> iterator() {
return new Itr();
}


是ArrayLIst一个内部类,截取部分代码:

private class Itr implements Iterator<E> {
int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
//每次都会检查modCount是否改变,改变则抛出异常
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}


listiterator分析

private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}

public boolean hasPrevious() {
return cursor != 0;
}

public int nextIndex() {
return cursor;
}

public int previousIndex() {
return cursor - 1;
}

@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}


部分代码如上,继承了Itr,添加了逆序遍历功能。

子列表

通过一个内部类实现,部分代码如下:

private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;


看出来 主要通过一个offset变量来记录开始位置在数组中的起始位置,还是共享父类的数组。

序列化、反序列化

ArrayList实现了Serializable接口,所以它支持序列化,但是我们看到底层存储:

transient Object[] elementData; // non-private to simplify nested class access


又是transient,表示忽略该字段的序列化,其实他是通过writeObject(java.io.ObjectOutputStream s),readObject(java.io.ObjectInputStream s)两个方法重写实现的。注意这两个方法是固定写法。以write为例:

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
 //记录当前修改次数 
int expectedModCount = modCount;
//写入类元信息和非静态成员和非transient成员
s.defaultWriteObject();
// Write out size as capacity for behavioural 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]);
}
//如果序列化过程中发现有修改list结构 则抛出异常

if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}


上面代码中写了两次size,网上说是兼容老板jdk原因。还提到反序列化老版本会恢复容量 新版本只恢复size大小的容量。

具体参考:

http://mushanshitiancai.github.io/2016/06/17/java/language/JDK%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB-ArrayList/#序列化-反序列化

克隆

克隆只是浅拷贝。元素对象还是共享的。

其他总结

用的最多的移动数组方法是Arrays.copyOf和 System.arraycopy。

pdf下载

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