集合的遍历律化娜无敌

一、集合的遍历

集合的遍历可以有多种方式

1.普通的for循坏(两种)

①for(int i = 0; i < list.size(); i++){
    System.out.println(list.get(i));
}
②int length = list.size();
for(int i = 0; i < length; i++){
    System.out.println(list.get(i));
}

2.写成foreach形式的for

for (String str:list){
    System.out.println(str);
}

3.迭代器

for(Iterator iterator = list.iterator();iterator.hasNext();){
    System.out.println(iterator.next());
}

4.集合的forEach方法

list.forEach(s -> {
    System.out.println(s);
});

5.迭代器的forEachRemaining

Iterator iterator = list.iterator();
iterator.forEachRemaining(str->{
    System.out.println(str);
});

有关于这几种遍历方式的效率问题看具体集合而定,这里不作介绍

这次主要介绍的是第2、第3、第4和第5的本质。

二、foreach相关遍历的本质

1.第2也就是写成foreach形式的for,很多人都知道它其实是调用了迭代器,这其实是一种语法糖,可以通过反编译来验证,由于缺乏反编译工具,这里只验证其使用了迭代器,我们验证使用的集合是Vector,这是一个历史遗留的类,采用它作测试的原因下文会讲到。

首先,我们在Vector迭代器上设置断点:

 

然后在主程序进行debug,得到结果:

 

我们可以发现程序跳到了迭代器上,由此可以得出写成foreach形式的for实际调用了迭代器进行遍历。

2.第3就是迭代器,这个就是直接调用迭代器进行遍历

3.第4就是集合的forEach方法,这个方法在Iterable接口就定义成了默认方法:

 

而在有些集合里面会对这个方法进行重写。

在这里我们测试两种集合,第一种还是前面的Vector,我们可以发现在Vector里对forEach方法进行了重写:

 

第二种集合是LinkedList,在LinkedList里没有对forEach方法进行重写。

在集合Vector上的测试

先在Iterable上的forEach方法上作断点:

 

在主程序上debug:

 

可以发现程序直接停止,并没有跳到Iterable上的forEach断点,说明在Vector集合上的forEach方法不是Iterable上的forEach方法

把在Iterable上的forEach方法的断点取消,在Vector上重写的forEach方法上作断点:

 

在主程序上debug:

 

可以发现跳到Vector上的forEach方法,说明在Vector集合上的forEach方法是Vector上的forEach方法,其实这很明显,作验证只是为了证明“编译看左边,运行看右边”

List<String> list = new Vector<>();

虽然List类里没有重写forEach方法,运行看右边,运行时是调用的是Vector集合上的forEach方法。然后我们取消在Vector上重写的forEach方法上的断点。

我们看下Vector上重写的forEach方法的源码

public synchronized void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int elementCount = this.elementCount;
    for (int i=0; modCount == expectedModCount && i < elementCount; i++) {
        action.accept(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

就是对elementData依次填到action.accept方法,由于调试无法进去accept方法,只是和lambda表达式结合在一起,所以我们通过别的方式推断accept执行了什么。

接下来验证在Vector上重写的forEach方法(accept)有没有调用迭代器,我们先在Vector迭代器上加断点:

 

然后在主程序debug:

 

可以发现程序正常停止,没有调用Vector的迭代器,说明forEach方法并没有调用Vector的迭代器。

在集合LinkedList上的测试

由于LinkedList没有重写forEach方法,所以调用的是Iterable上的forEach方法,下面进行验证

Iterable上的forEach方法上加断点:

 

debug:

 

可以发现调用的确实是Iterable上的forEach方法

再在LinkedList迭代器上添加断点(因为LinkedList的Iterator方法没有重写,所以在AbstractSequentialList的迭代器方法上加断点):

 

结果:

 

我们可以发现Iterable上的forEach方法调用了迭代器,我们再看Iterable上的源码:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

发现调用的是一个foreach循环上的for,前面我们已经证明这个形式的遍历其实是通过迭代器来遍历的,然后再调用action.accept方法。

因此,Vector迭代器里的forEachRemaing方法也是同理,没重写的调用迭代器,有重写的不调用,可以查看下面源码得出:

Iterator的forEachRemaining方法

default void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    while (hasNext())
        action.accept(next());
}

Vector迭代器重写的forEachRemaining方法

public void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    synchronized (Vector.this) {
        final int size = elementCount;
        int i = cursor;
        if (i >= size) {
            return;
        }
@SuppressWarnings("unchecked")
        final E[] elementData = (E[]) Vector.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            action.accept(elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }
}

三、遇到的问题

本来这是能直接看源码就能理解的事,为什么要debug来验证呢?

因为在我们不能进去的accept方法里,有一个问题,这也是我们测试之所以不采用ArrayList的原因。

我们查看ArrayList源码:

public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

public void forEachRemaining(Consumer<? super E> consumer) {
    Objects.requireNonNull(consumer);
    final int size = ArrayList.this.size;
    int i = cursor;
    if (i >= size) {
        return;
    }
    final Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length) {
        throw new ConcurrentModificationException();
    }
    while (i != size && modCount == expectedModCount) {
        consumer.accept((E) elementData[i++]);
    }
    // update once at end of iteration to reduce heap write traffic
    cursor = i;
    lastRet = i - 1;
    checkForComodification();
}

可以发现和Vector基本类似,那么ArrayList的forEach方法和迭代器里的forEachRemaining方法应该不会调用迭代器才对,我们先看测试:

ArrayList迭代器上设置断点:

 

debug:

 

居然跳转到迭代器上。

再测试迭代器上的forEachRemaining方法,同样ArrayList迭代器上设置断点,debug:

 

也跳转到迭代器上。

这和我们前面Vector的测试结果完全不一样,我们前面得到的结论是:没重写的调用迭代器,有重写的不调用。ArrayList的这两个方法却调用了迭代器,但是ArrayList里重写的forEach方法和forEachRemaining方法确实没有调用迭代器的代码啊,这时,只剩下一个我们进不去的accept方法了,只能看怎么找出结果,但是因为进不去实在毫无头绪,于是我在debug界面里面疯狂乱跳代码,结果跳到一处:

 

可以发现此处new了两个ArrayList并把buf加进去result,往下

 

我们又发现了迭代器的使用,原来迭代器是在这使用的,为了验证确实是在accept方法里使用的ArrayList的迭代器,我们会到Vector的测试,ArrayList上的迭代器上断点保留,debug:

 

结果不出所料,真的跳到ArrayList的迭代器上去了。

所以,accept方法和lambda结合,原理实在复杂又不能debug,只能勉强得出这里面用到ArrayList和它的迭代器,可能是拿来迁移数据和遍历输出的,当然这只是我的猜测。

四、新的问题

集合里还有HashMap,但HashMap的遍历是通过Set进行的,于是我开始着眼于Set,先测试,代码:

public class Test {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap();
        map.put("kirito", "asuna");
        map.put("taki", "mitsuha");
        Set<String> set = map.keySet();
        set.forEach(a->{
            System.out.println(a);
        });
    }
}

依然是在迭代器上作断点

 

debug:

 

主程序停止,并没有调转到迭代器,这符合我们之前的结论,毕竟HashMap的KeySet里有对forEach方法进行重写

 

然后我们换成forEachRemaining方法,结果跳转了:

 

因为HashMap的KeySet迭代器没有对forEachRemaining方法进行重写,所以调用Iterator的forEachRemaining方法,所以用到了迭代器,在我心满意足的时候,一切都向着我希望的方式发展时,我发现KeySet的迭代器是KeyIterator

 

因此我决定把断点设置深一点看看

 

debug:

 

仍然停止,就在我要放弃的时候,发现

KeyIterator继承自HashIterator,然后我再把断点设置到了HashIterator上

 

debug:

这回竟然跳转到了这个迭代器,我思前想后,应该还是accept的问题,因为确实forEach方法里是没有调用到迭代器的,然后我还是疯狂debug:来了

 

这里用了HashMap里values的迭代器,所以刚刚不会跳到KeySet的迭代器而是跳到HashIterator。

values的迭代器设置断点:

 

debug:

 

不出所料

五、结论

经过了这么一折腾,我们可以发现默认的forEach方法(Iterable)和forEachRemaining方法(Iterator)都会调用对应集合的迭代器,集合没重写调用的就是默认的这两种方法,集合重写后的这两种方法一般都是自己遍历然后交给lambda表达式输出,不会调用迭代器,但是lambda表达式的原理里面,存在ArrayList和HashMap的values这两个集合的使用以及迭代器的使用,虽然用处不知,但这使用这两种集合作测试时会误认为重写过后的forEach方法(Iterable)和forEachRemaining方法是调用自身迭代器进行遍历,所以forEach方法和forEachRemaining方法的时间复杂度,不是重写的要看自身迭代器的效率还有ArrayList和HashMap的效率,是重写的则是自己重写遍历效率和ArrayList和HashMap的效率,相当复杂,因为lambda的原理完全不懂。所以,对于网上各种遍历方式的时间效率测试,没有绝对的对错,这不仅要看集合,还要看各种实现,总之,时间复杂度不是绝对的,要看具体代码,代码级别的时间复杂度才足够客观。

猜你喜欢

转载自www.cnblogs.com/takirito/p/10032304.html