List (迭代器)

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。(对于这一点如果不是很理解,可以参考博主的上一篇博客:http://blog.csdn.net/dustin_cds/article/details/50788608)但是如果改用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方法。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值