java--迭代(二)for,foreach和迭代器详解
这篇文章会详解上篇关于迭代器中出现的问题,当然说是详解,其实我也只能在自己能力内对foreach,迭代器的机制进行了解。其中以arraylist为例子,包含了jdk的源代码。
首先,for是大家都很熟悉的循环语法,它的基础规则和使用为:
编程中用于循环处理的语句。Java的for语句形式有两种:一种是和C语言中的for语句形式一样,另一种形式用于在集合和数组之中进行迭代。有时候把这种形式称为增强的for(enhanced for)语句,它可以使循环更加紧凑和容易阅读。它的一般形式为: for(;;) 语句; 初始化总是一个赋值语句,它用来给循环控制变量赋初值;条件表达式是一个关系表达式,它决定什么时候退出循环;增量定义循环控制变量每循环一次后按什么方式变化。这三个部分之间用";"分开。
这一段是话百度的解释,但事实上这个地方有一点不太准确的地方,foreach确实只能针对iterator进行迭代,但是与我在开头所提到的迭代器遍历是由一定区别的,我在后面会通过csdn上某位同志遇到的问题进行详细解释。所以在这个时候,我更愿意将for,foreach和迭代器遍历着三种方法分开,而不是将foreach归为迭代器的一种遍历方法。
三种方式的区别:
1)形式区别:
对于for循环,我们采用:
for(int i=0;i<arr.size();i++){...}
对于foreach:
for(int i:arr){...}
(当然这里的int只是举例子,这里可以是所有实现iterator的接口对象);
在这里多说两句,foreach只能针对实现了iterable接口的对象,其中具体怎么实现的,我有做了部分了解。我去查看了一个foreach的测试程序的字节码。当然这不是今天的重点,我会重新开一个篇章讲述关于java汇编中字节码的事情。如果有兴趣的同学可以点击下面这个网站:
http://www.cnblogs.com/vinozly/p/5465454.html;
对于迭代器的形式:
Iterator it = arr.iterator(); while(it.hasNext()){ object o =it.next(); ...}
;
2)条件差别
for需要知道数组或者集合的大小,而且需要时有序的,不然无法遍历;
foreach和iterator不需要知道数组或者集合的大小,他们都是得到集合内的每一个元素然后进行处理;
3)多态差别
for和foreach都需要知道自己的集合类型,甚至要知道自己集合内的元素类型,不能实现多态。这个使用的语法上都可以表示出来。
Iterator是一个接口累心,它不关心集合的累心和集合内的元素类型,因为它是通过hasnext和next来进行下一个元素的判断和获取,这一切都是在集合类型定义的时候就完成的事情。迭代器统一了对容器的访问模式,这也是对接口解耦的最好表现。
4)用法差别
for一般可以用于简单的顺序集合,并且可以预测集合的大小;
foreach可以遍历任何集合或者数组,但是使用者需要知道遍历元素的类型。
iterator是最强大的,它可以随之修改元素内部的元素。可以在遍历的时刻用remove()!!!!我的意思是其他两个都不可以!!!
而且iterator不需要知道元素类型和元素大小,通过hasnext()判断是否遍历完所有元素。
而且在对范型的集合进行遍历的时候,iterator是不二的选择,就是因为不需要知道元素类型便可以遍历。
这里讨论完了三个具体的区别,但是我接触到了一个新的东西叫接口解耦。好吧,我会再写一个文章讲一下接口解耦是什么。(感觉坑越填愈多)
接下来讲一下iterator实现遍历的具体机制:
我们首先从一个问题程序入手,这个程序如下:
1 package iteratorTest; 2 import java.util.*; 3 4 public class TestIterator { 5 public static void main(String[] args){ 6 List<String> list=new ArrayList<String>(); 7 8 list.add("1"); 9 list.add("2"); 10 for(String temp : list){ 11 if("2".equals(temp)) 12 list.remove(temp); 13 } 14 15 for(String temp : list){ 16 System.out.println(temp); 17 } 18 19 Iterator<String> it=list.iterator(); 20 /*while(it.hasNext()){ 21 String temp=it.next(); 22 if("2".equals(temp)) 23 it.remove(); 24 } 25 26 for(String temp:list){ 27 System.out.println(temp); 28 }*/ 29 } 30 }
我们汇编的时候,程序会出现下面的错误:
当然,我如果更改程序把“1”删除,这个时候就没有问题。我当时也是一脸懵逼呀。
从错误中可以看到,是checForComodification出现了报错。。那我们现在就去看这个地方的源代码:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
这里设计一个java关于多线程的知识,因为iterator是禁止多线程操作的(当然vector的使用会在在后面写。- -!真的是坑越填越多),java用了一个非常简单的机制,就是modcount。记录对集合进行改变的数量,创建一个迭代器与当前集合是要紧耦合的。我不能再我遍历的时候同时去改变这个集合元素,不然会造成混乱。
好,这里我们可以确定是modcount不等于expectedmodecount造成的错误,那么及时在foreach内调用next()方法的时候出现了这个问题。我们深入去查看.next()方法。在看之前,我想先把JDK文档中arraylist的方法和数据结构给出来,这在后面理解的时候是非常重要的:
注意我所标记的fastremove方法上面的remove()方法和私有类Itr里面的remove()方法。很明显Itr是一个iterator接口类。注意了!!!但是在foreach的循环语句里面,我们使用的是上面的remove方法,因为我们并没有构造list的迭代器。但是报道的错误信息来源于这个私有类里面的next是foreach擅自为我们进行构造的迭代器,而我们没有使用这个迭代器里面的remove()!!!!!在研究源码的时候我真的是载在这里好久好久。
我们使用的list.remove()是使用的arraylist自身的remove()方法,而不是构造器的方法。在使用的时候我还是清醒的,在调查源代码的过程中整个人都懵逼了。我给出两个remove的具体代码,大家就会知道,arraylist自身方法的remove是不会去调整exceptedmodcount的值,但是却增加了modcount,所以在foreach语句中使用next的checkmodcount的时候发生了错误。
arraylist.remove():
public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } /* * Private remove method that skips bounds checking and does not * return the value removed. */ private void fastRemove(int index) { modCount++; 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 }
Itr私有迭代器的remove():
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(); } }
很明显迭代器中的remove有意识的改变expectedmodcount来使其不报错。而foreach使用的arraylist.remove,改变了modcount,所以才会出现报错现象。
然而删除“1”不会报错,是因为删除过后foreach会调用迭代器中的hasnext()进行判断,代码如下:
public boolean hasNext() { return cursor != size; }
这个时候没有下一个元素,所以不会在arraylist.remove()后调用Itr.next()方法,所以不会进行判断。
这里给出Itr.next()的代码:
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]; }
可以看到,在Itr.next()的一开始便检查modcount于expectedmodcount是否相同。
通过上面的了解,我们使用iterator为什么能够在遍历的时候进行删除操作也可以理解了:
在Itr.remove()方法内有意的将expectedmodcount赋值为modcount,所以不会产生checkformodcount的错误。
ps:真的,我在这里研究这么久是因为我看了一个评论,它就是将foreach的机制和arraylist.remove, Itr.remove弄混了,一直在分析迭代器内部是如何影响modcount和expectedcount的。还得到了很多很多赞。我在理解源码的过程中表示,这不是吧expectedmodcount也改变了的么。
所以有时候,大家最好能通过源码了解的,都自己去看源码吧,大家对问题有不同的看法,也会有错误,但是源码是不变的。
这篇文章写到这里,很多问题也没有深入,关于foreach的实现和字节码,有时间就会做的。
我还会另开一个数据库的篇章,以怀恋我drop掉的数据库课。不是我不想上,是印度教授真的弄的我头疼。
最后,我想说的就是:
开源大法好!!!!