您的位置:首页 > 移动开发 > Android开发

Android Gems - Java源码分析之List

2016-11-25 17:41 239 查看
最近突发奇想,写个Java源码分析系列。开发过程中,总会使用各种类库,如ArrayList,LinkedList等,用得虽多,但对于其实现细节却了解甚少,所以专卖开辟个系列,从源码的角度分析一下各种Java类库的实现细节。就先从最简单的List入手。

List是给接口,其继承自Collection<?>,标准接口咱就不说了,List的实现有两个,ArrayList和LinkedList,前者是数组实现,后者是链表。

ArrayList只有两个成员变量array,size。array是个Object[] 表示List的元素数组,size是元素个数,这里可能有人要问,Object[]不是自带length吗,为什么还需要个size呢?这是为了提高ArrayList的add性能,数组的内存空间会比实际List的元素的个数大,这样在add的时候,不用每次都重新分配内存,并进行arraycopy。

@Override
public boolean add(E object) {
Object[] a = array;
int s = size;
if (s == a.length) {
Object[] newArray = new Object[s +
(s < (MIN_CAPACITY_INCREMENT / 2) ?
MIN_CAPACITY_INCREMENT : s >> 1)];
System.arraycopy(a, 0, newArray, 0, s);
array = a = newArray;
}
a[s] = object;
size = s + 1;
modCount++;
return true;
}


以上就是ArrayList的add函数源码,如果当前的元素的个数已经达到数组内存的大小,那么当前这个待add的object就没有空间了,所以需要扩容,ArrayList的扩容的算法是当前的size 的 2倍,为了避免元素少的时候增长太慢,于是设置了一个最小的扩容值12(MIN_CAPACITY_INCREMENT),也就是说如果当前size如果是1的话,那么ArrayList add之后,array的内存大小就扩大到12,而不是1 × 2。这是add到最后的情况,那么如果是add到某个指定位置 add(int
index, E object)呢?

@Override
public void add(int index, E object) {
Object[] a = array;
int s = size;
if (index > s || index < 0) {
throwIndexOutOfBoundsException(index, s);
}

if (s < a.length) {
System.arraycopy(a, index, a, index + 1, s - index);
} else {
// assert s == a.length;
Object[] newArray = new Object[newCapacity(s)];
System.arraycopy(a, 0, newArray, 0, index);
System.arraycopy(a, index, newArray, index + 1, s - index);
array = a = newArray;
}
a[index] = object;
size = s + 1;
modCount++;
}


从上面的代码可以看出,如果当前元素的size还未达到内存的上限(s < a.length),那么要做的就是arraycopy,其时间复杂度可是O(n),越往前插越慢。而如果当前元素的size达到上限,那么先需要扩容,然后再挪内存。由此看来,如果你的需求有很多的往数组中间插入的操作,那么ArrayList的性能会比较差,应尽量避免这种操作。

这是add单个元素的情况,对于addAll添加一个数组的情况也是一样的逻辑,添加之后元素的个数是newSize = s + newPartSize,如果newSize没有超过内存上限,就只需要arrayCopy先挪内存元素,以便新的Collection的元素都能插入进来。否则先扩容,新的size为newCapacity函数给出,这时候的算法并不是翻倍,而是newSize的基础上增加50%,这主要是避免扩容扩太快。

@Override
public boolean addAll(int index, Collection<? extends E> collection) {
int s = size;
if (index > s || index < 0) {
throwIndexOutOfBoundsException(index, s);
}
Object[] newPart = collection.toArray();
int newPartSize = newPart.length;
if (newPartSize == 0) {
return false;
}
Object[] a = array;
int newSize = s + newPartSize; // If add overflows, arraycopy will fail
if (newSize <= a.length) {
System.arraycopy(a, index, a, index + newPartSize, s - index);
} else {
int newCapacity = newCapacity(newSize - 1);  // ~33% growth room
Object[] newArray = new Object[newCapacity];
System.arraycopy(a, 0, newArray, 0, index);
System.arraycopy(a, index, newArray, index + newPartSize, s-index);
array = a = newArray;
}
System.arraycopy(newPart, 0, a, index, newPartSize);
size = newSize;
modCount++;
return true;
}
private static int newCapacity(int currentCapacity) {
int increment = (currentCapacity < (MIN_CAPACITY_INCREMENT / 2) ?
MIN_CAPACITY_INCREMENT : currentCapacity >> 1);
return currentCapacity + increment;
}
可见ArrayList的容量是动态扩展的,有时为了避免扩容造成的性能损失,可以先调用ensureCapacity来提前分配内存,这种方法主要适用于对自己未来的内存占用量有个提前预估。

以上是add的情况,remove的操作则会更加简单一些,remove由于内存没有扩容,因此和add比,没有扩容部门,只有挪元素部分。

@Override
public E remove(int index) {
Object[] a = array;
int s = size;
if (index >= s) {
throwIndexOutOfBoundsException(index, s);
}
@SuppressWarnings("unchecked") E result = (E) a[index];
System.arraycopy(a, index + 1, a, index, --s - index);
a[s] = null;  // Prevent memory leak
size = s;
modCount++;
return result;
}


先把[index + 1, s)的元素前移。a[s] = null的目的是避免hold住前移之前的最后一个元素的对象,从而后续造成内存泄漏。同理,ArrayList的clear函数,除了只把size置0之外,还需要把array的元素置空,从这个角度看,clear的时间复杂度其实是O(n)的。

以上便是ArrayList的实现细节,是不是很简单。

LinkedList是链表实现的,他也有两个成员变量,voidLink,size。voidLink是个Link对象,表示链表的表头,size则是元素大小,size的目的主要是优化LinkedList.size函数,不用每次都遍历链表来数个数。

LinkedList的Link是个双链表,有next也有previous,双链表比单链表好处是删除方便,单链表的删除需要找到他的previous,不好的地方就是多了一个previous对象,多占了内存。LinkedList的链表还是个循环列表,voidLink表头的previous是指向最后一个元素的,目的是了add操作,链表没法随机访问,如果不是循环链表的话,那么就需要遍历整个链表,找到最后一个节点,才能插入。

我们看一下LinkedList的几个基本函数:

@Override
public E get(int location) {
if (location >= 0 && location < size) {
Link<E> link = voidLink;
if (location < (size / 2)) {
for (int i = 0; i <= location; i++) {
link = link.next;
}
} else {
for (int i = size; i > location; i--) {
link = link.previous;
}
}
return link.data;
}
throw new IndexOutOfBoundsException();
}
get函数是随机访问,这个不是链表的长处。如果是前半部分,那么就从表头开始往后找,如果是后半部分,就从表头往前找。所以如果你的使用场景是随机访问比较多的话,建议用ArrayList,而不是LinkedList,LinkedList适合那种插入操作比较多的场景。另外一个注意的地方就是,遍历的时候,LinkedList千万别用for + get实现,这样复杂度非常高,需要用iterator来实现。

@Override
public boolean add(E object) {
return addLastImpl(object);
}

private boolean addLastImpl(E object) {
Link<E> oldLast = voidLink.previous;
Link<E> newLink = new Link<E>(object, oldLast, voidLink);
voidLink.previous = newLink;
oldLast.next = newLink;
size++;
modCount++;
return true;
}
这是LinkedList添加元素到最后的实现,链表很方便,O(1),同理addFirst也一样。往中间插就不一样了,如下,需要先找到location所在的节点。虽然都是O(n),但LinkedList这个复杂度比ArrayList的System.arraycopy要高,ArrayList挪内存用的arraycopy是native实现的,并且是连续内存,是有优化的。

@Override
public void add(int location, E object) {
if (location >= 0 && location <= size) {
Link<E> link = voidLink;
if (location < (size / 2)) {
for (int i = 0; i <= location; i++) {
link = link.next;
}
} else {
for (int i = size; i > location; i--) {
link = link.previous;
}
}
Link<E> previous = link.previous;
Link<E> newLink = new Link<E>(object, previous, link);
previous.next = newLink;
link.previous = newLink;
size++;
modCount++;
} else {
throw new IndexOutOfBoundsException();
}
}


LinkedList的remove需要先找到节点,比如remove(int location),需要先找到这个元素,然后删除。remove(Object object)也是一样的,两种case都是随机删除,这都不是LinkedList擅长的,如果如果你的使用场景,如果是这种随机插入、删除的场景特别多的话,那么LinkedList是不适合的。

@Override
public E remove(int location) {
if (location >= 0 && location < size) {
Link<E> link = voidLink;
if (location < (size / 2)) {
for (int i = 0; i <= location; i++) {
link = link.next;
}
} else {
for (int i = size; i > location; i--) {
link = link.previous;
}
}
Link<E> previous = link.previous;
Link<E> next = link.next;
previous.next = next;
next.previous = previous;
size--;
modCount++;
return link.data;
}
throw new IndexOutOfBoundsException();
}


细心的读者可能发现了,不管是ArrayList还是LinkedList都有一个modCount字段,这个是用来干什么的呢?
LinkedList和ArrayList都不是线程安全的,modCount在每次元素有改动的时候都会++,这个就是为了检查是否有改动,而使用的地方就是在Iterator里,不管是ArrayList还是ArrayList都实现自己的Iterator,用来遍历数组的元素。在每次Interator实例化的时候,也就是准备遍历数组元素的时候,都会保存当前的modCount到迭代器的expectedModCount字段,然后在做遍历操作的时候,会检查expectedModCount和modCount的值,如果这个值不一样,就说明有其他线程在这次遍历期间,对List的元素做过改动,就会抛出ConcurrentModificationException异常,用来提示多线程竞争了。ConcurrentModificationException是不是很熟悉,如果你遇到这个异常,就说明在多个线程里对一个线程不安全的容器做了改动,很可能会造成数据的不一致,不止是ArrayList、LinkedList,HashMap、Hashtable都一样,都是在使用了迭代器的时候会碰到这个异常。

作者简介:

田力,网易彩票Android端创始人,小米视频创始人,现任roobo技术经理、视频云技术总监

欢迎关注微信公众号 磨剑石,定期推送技术心得以及源码分析等文章,谢谢

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