您的位置:首页 > 其它

List (迭代器)

2016-03-04 15:05 204 查看

1 List的迭代

遍历是List中最常用的操作。说到遍历,就不得不提Java里的迭代器了。由于Java中数据容器众多,而对数据容器的操作在很多时候都具有极大的共性,于是Java采用了迭代器为各种容器提供公共的操作接口。使用Java的迭代器iterator可以使得对容器的遍历操作完全与其底层相隔离,可以到达极好的解耦效果。所以,我们不光要学会怎么使用迭代器,更重要的是,它是一个很好的例子,可以去体会Java的编程思想。拿List当一个切入点,先看看下面几段源码:

public interface List<E> extends Collection<E> {……}


public interface Collection<E> extends Iterable<E> {……}


public interface Iterable<T> {
Iterator<T> iterator();
}


public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}


总结一下就是List接口继承了Collection接口,Collection接口继承了Iterable接口,Iterable接口返回一个Iterator,Iterator中只有三种方法:hasNext、next、remove。而在List接口中有一个对应的方法:

Iterator<E> iterator();


所以接下来我们就要看看ArrayList和LinkedList怎么实现iterator这个方法的。





所以,ArrayList返回迭代器的方法就是它本身的一个方法,而LinkedList返回迭代器的方法继承至抽象类AbstractSequentialList。而ArrayList的iterator()来至于它本身的方法。那么上面有提到两个抽象类,AbstractList和AbstractSequentialList,干啥子的咧? 我们一起来扒一扒。

2 AbstractCollection

public abstract class AbstractCollection<E> implements Collection<E> {……}


此类提供 Collection 接口的骨干实现,以最大限度地减少了实现此接口所需的工作。

要实现一个不可修改的 collection,编程人员只需扩展此类,并提供 iterator 和 size 方法的实现。(iterator 方法返回的迭代器必须实现 hasNext 和 next。)

要实现可修改的 collection,编程人员必须另外重写此类的 add 方法(否则,会抛出 UnsupportedOperationException),iterator 方法返回的迭代器还必须另外实现其 remove 方法。

按照 Collection 接口规范中的建议,编程人员通常应提供一个 void (无参数)和 Collection 构造方法。

此类中每个非抽象方法的文档详细描述了其实现。如果要实现的 collection 允许更有效的实现,则可以重写这些方法中的每个方法。



3 AbstractList

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {……}


此类提供 List 接口的骨干实现,以最大限度地减少实现“随机访问”数据存储(如数组)支持的该接口所需的工作。对于连续的访问数据(如链表),应优先使用 AbstractSequentialList,而不是此类。

要实现不可修改的列表,编程人员只需扩展此类,并提供 get(int) 和 size() 方法的实现。

要实现可修改的列表,编程人员必须另外重写 set(int, E) 方法(否则将抛出 UnsupportedOperationException)。如果列表为可变大小,则编程人员必须另外重写 add(int, E) 和 remove(int) 方法。

按照 Collection 接口规范中的建议,编程人员通常应该提供一个 void(无参数)和 collection 构造方法。

与其他抽象 collection 实现不同,编程人员不必 提供迭代器实现;迭代器和列表迭代器由此类在以下“随机访问”方法上实现:get(int)、set(int, E)、add(int, E) 和 remove(int)。

此类中每个非抽象方法的文档详细描述了其实现。如果要实现的 collection 允许更有效的实现,则可以重写所有这些方法。

4 AbstractSequentialList

public abstract class AbstractSequentialList<E> extends AbstractList<E> {……}


此类提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作。对于随机访问数据(如数组),应该优先使用 AbstractList,而不是先使用此类。

从某种意义上说,此类与在列表的列表迭代器上实现“随机访问”方法(get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index))的 AbstractList 类相对立,而不是其他关系。

要实现一个列表,程序员只需要扩展此类,并提供 listIterator 和 size 方法的实现即可。对于不可修改的列表,程序员只需要实现列表迭代器的 hasNext、next、hasPrevious、previous 和 index 方法即可。

对于可修改的列表,程序员应该再另外实现列表迭代器的 set 方法。对于可变大小的列表,程序员应该再另外实现列表迭代器的 remove 和 add 方法。

按照 Collection 接口规范中的推荐,程序员通常应该提供一个 void(无参数)构造方法和 collection 构造方法。

5 ArrayList

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{……}


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

/**
* An optimized version of AbstractList.Itr
*/
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() {
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];
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

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


所以,ArrayList是在抽象类AbstractList上创建的。虽然AbstractList中已经实现了iterator(),但是ArrayList还是对其进行了重写,也就是优化。

6 LinkedList

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{……}


LinkedList并没有对iterator()进行重写,所以它的iterator()来至AbstractSequentialList,而AbstractSequentialList中的iterator()实际上也是继承于AbstractList的listIterator()。233333……这里又引出另一个原先被忽略的接口ListIterator。

7 ListIterator

列表迭代器,允许程序员按任一方向遍历列表、迭代期间修改列表,并获得迭代器在列表中的当前位置。ListIterator 没有当前元素;它的光标位置 始终位于调用 previous() 所返回的元素和调用 next() 所返回的元素之间。长度为 n 的列表的迭代器有 n+1 个可能的指针位置。

注意,remove() 和 set(Object) 方法不是 根据光标位置定义的;它们是根据对调用 next() 或 previous() 所返回的最后一个元素的操作定义的。



8 总结

上面看似讲了很多,实际上只是将Java Iterator的由来和几个接口和类之间的继承关系列了一下,主要是为了满足博主自己的好奇心。在使用上,迭代器是非常简单的,Java作为一个美丽的高级编程语言,使很多细节透明化了。使用时要注意一下两点:

8.1 遍历

在这里,我想强调的一点是:效率问题。ArrayList是数组,LinkedList是双向链表,get对于前者需O(1),对于后者是O(N/2);那么对于这样一段反复get的代码:

public static int sum1(List<Integer> list) {
int total = 0;
for(int i=0;i<list.size();i++){
total += list.get(i);
}
return total;
}


前者需O(N),对于后者是O(N^2),因为LinkeList的每一次get都需要沿着链表遍历一部分到指定index。(对于这一点如果不是很理解,可以参考博主的上一篇博客:/article/9915730.html)但是如果改用Iterator遍历,则不管是ArrayList还是LinkedList都只会遍历一次,都是O(N):

public static int sum2(List<Integer> list) {
int total = 0;
int i = 0;
for (Iterator<Integer> iterator = list.iterator(); iterator.hasNext();) {
Integer integer = (Integer) iterator.next();
i = integer.intValue();
total += i;
}
return total;
}


或者这样写:

public static int sum3(List<Integer> list) {
int total = 0;
int i=0;
for (Integer integer : list) {
i = integer.intValue();
total += i;
}
return total;
}


foreach的原理也是利用Collection的迭代器,所以sum3实际上和sum2等价,而且代码更加简洁,所以推荐使用foreach写法。所以,在对容器进行遍历的时候,推荐使用迭代器。

8.2 迭代器的remove与容器的remove

ArrayList的remove:一次shift而已

public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}


LinkedList的remove:通过部分遍历找到节点然后调整引用而已

public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}


ArrayList的Iterator的remove:调用ArrayList的remove,每次只能删除last element returned,而不是任意删除。删除之后会对expectedModCount进行维护,可以简单地理解为一种防止迭代器混乱的机制。

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}


LinkedList的Iterator的remove

//AbstractSequentialList中
public E remove(int index) {
try {
ListIterator<E> e = listIterator(index);
E outCast = e.next();
e.remove();
return outCast;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}


//LinkedList中
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}

private class ListItr implements ListIterator<E> {
private Node<E> lastReturned = null;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;

ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
…………………………
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
//ListIterator 没有当前元素;它的光标位置 始终位于调用 previous() 所返回的元素和调用 next() 所返回的元素之间。长度为 n 的列表的迭代器有 n+1 个可能的指针位置
Node<E> lastNext = lastReturned.next;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
……………………
}


同样的,LinkedList的迭代器的remove会调用LinedList的unlink方法,然后会维护expectedModCount。

所以,关于迭代的一个需要注意的问题就是,在跌代器跌代的过程中不能用集合的remove,因为他会改变modCount这个指标,提示迭代器,集合已经发生改变,可能造成混乱,系统会报错。示例:



但是,如果用普通的for循环是可以的,只是效率上很低而已。不论是ArrayList还是LinkedList的remove的时间复杂度都是O(N),如果进行removeEvensVer这样的操作,就要花费O(N^2)。这是不可取的。

如果使用迭代器的remove方法,则一次遍历即可。对于LinkedList,由于是找到了偶数节点才删除,所以一次remove花费常数时间,总过程花费线性时间。对于ArrayList,由于每次remove都需要shift,所以最好情况(一个为偶数的节点都没有)则花费O(N);最坏情况(每个节点的值都是偶数),则花费O(N^2);所以,就变为了一个性能与输入数据相关的算法,但总的来讲,比for循环遍历然后调用Collection的remove方法的效率要高。

综上所述

遍历的List的时候建议使用foreach(迭代器);

如果使用迭代器,在迭代过程中不能调用Collection的remove方法;

遍历情况下的删除操作建议使用迭代器的remove方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: