Java中的forin語句

forin的原理

forin語句是JDK5版本的新特性,在此之前,遍歷數(shù)組或集合的方法有兩種:通過下標(biāo)遍歷和通過迭代器遍歷。先舉個例子:

@Test
public void demo() {
    String arr[] = { "abc", "def", "opq" };
    for (int i = 0; i < arr.length; i++) {//通過下標(biāo)遍歷數(shù)組
        System.out.println(arr[i]);
    }
    System.out.println("----------");
    List<String> list = new ArrayList<String>();
    list.add("abc");
    list.add("def");
    list.add("opq");
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {//通過迭代器遍歷集合
        System.out.println(iterator.next());
    }
}

用JUnit進(jìn)行單體測試,兩種方法的輸出結(jié)果是一樣的:

demo()運(yùn)行效果

JDK5以后引入了forin語句,目的是為了簡化迭代器遍歷,其本質(zhì)仍然是迭代器遍歷。forin語句的寫法很簡單:

for(數(shù)據(jù)類型 對象名 : 數(shù)組或集合名){
    ...
}

這里的數(shù)據(jù)類型是數(shù)組或集合中的數(shù)據(jù)類型,接著聲明一個該數(shù)據(jù)類型的對象,用于代替數(shù)組或集合中的每一個元素(因此forin語句又稱為foreach語句),最后便是對該對象也就是數(shù)組或集合中元素的操作了。
修改上面的代碼,用forin語句遍歷剛才的數(shù)組和集合:

System.out.println("----------");
for (String s1 : arr) {
    System.out.println(s1);
}
System.out.println("----------");
for (String s2 : list) {
    System.out.println(s2);
}

用JUnit進(jìn)行單體測試,輸出的結(jié)果與之前相同:

demo()運(yùn)行效果

需要注意的是,通過forin語句遍歷和通過迭代器遍歷是完全等價(jià)的。另外,在使用Eclipse進(jìn)行編程的時(shí)候,可以使用alt+/進(jìn)行快捷輸入生成下標(biāo)遍歷的for循環(huán)語句或forin語句,十分方便。
下面講一個關(guān)于數(shù)組內(nèi)存的問題,在上面的代碼中再添加一段:

System.out.println("----------");
for (String s3 : arr) {
    s3 = "rst";
}
System.out.println(arr[0]);

如果按照常規(guī)的思維去理解,數(shù)組中的三個元素應(yīng)該都被修改為了rst,因此最后輸出的結(jié)果也應(yīng)全部為rst。然而并不是這樣的,用JUnit進(jìn)行單體測試:

demo()運(yùn)行效果

結(jié)果很明顯,輸出的是abc、def、opq而非三個rst,也是說數(shù)組中的三個元素并沒有被rst替換。要解釋這個問題就要從Java中的內(nèi)存講起,在Java中,方法中的引用位于堆空間,而對象則實(shí)例化在??臻g。數(shù)組{ "abc", "def", "opq" }屬于方法中的引用,因此存儲在堆空間中,而s3arr屬于實(shí)例化的對象,則應(yīng)存儲在??臻g中。在String arr[] = { "abc", "def", "opq" };這句代碼中,=的作用就是將??臻g中的arr指向堆空間中的數(shù)組,而forin語句的作用則是每循環(huán)一次就將堆空間中數(shù)組元素的值賦給??臻g中的s3,而這些元素的值實(shí)際上不會發(fā)生改變。因此遍歷并輸出數(shù)組所有元素得到的結(jié)果與之前完全一樣。下圖可以幫助理解這個問題:

數(shù)組內(nèi)存

forin的實(shí)現(xiàn)

如果一個對象想使用forin語句進(jìn)行遍歷,則對象類必須滿足兩個條件:實(shí)現(xiàn)Iterable接口和實(shí)現(xiàn)Iterator方法。之所以ArrayList集合類能夠?qū)崿F(xiàn)forin語句遍歷,就是因?yàn)槠錆M足上述兩個條件:

Collection接口繼承Iterable接口

Collection接口實(shí)現(xiàn)Iterator方法

由于ArrayList集合類繼承AbstractList類,AbstractList類繼承AbstractCollection類,AbstractCollection類又實(shí)現(xiàn)Collection接口,因此ArrayList集合類間接地實(shí)現(xiàn)了Iterable接口和Iterator方法。
現(xiàn)在我們試著編寫一個Phone類,然后讓Phone類對象能夠?qū)崿F(xiàn)forin語句遍歷:

public class Phone implements Iterable<String> {//實(shí)現(xiàn)Iterable接口
    String[] names = { "蘋果", "三星", "華為", "小米", "魅族" };
    public Iterator<String> iterator() {//實(shí)現(xiàn)Iterator方法同時(shí)自定義迭代器
        Iterator<String> iterator = new MyIterator();
        return iterator;
    }
    class MyIterator implements Iterator<String> {
        int index = 0; 
        public boolean hasNext() {
            if (index >= names.length) { 
                return false;
            }
            return true;
        }
        public String next() {
            String name = names[index];
            index++;
            return name;
        }
        public void remove() {
        }
    }
}

創(chuàng)建新的方法用于測試:

@Test
public void demo1(){
    Phone phone = new Phone();//實(shí)例化Phone類對象
    for (String s : phone) {//forin語句遍歷Phone類對象phone
        System.out.println(s);
    }
}

用JUnit進(jìn)行測試,結(jié)果是正確的:

demo1()運(yùn)行結(jié)果

forin刪除元素

再創(chuàng)建一個方法,這次對集合的元素進(jìn)行一些改動,然后用兩種方法刪除包含字符a的字符串。首先是通過下標(biāo)遍歷集合:

@Test
public void demo2(){
    List<String> list = new ArrayList<String>();
    list.add("abc");
    list.add("ade");
    list.add("afg");
    list.add("def");
    list.add("opq");
    for (int i = 0; i < list.size(); i++) {
        String s = list.get(i);
        if (s.contains("a")){
            list.remove(s);
        }
    }
    System.out.println(list);
}

這段代碼看起來再正確不過,然而輸出結(jié)果卻是錯誤的:

demo2()運(yùn)行效果

這是因?yàn)楫?dāng)刪除完第一個字符串abc后,第二個字符串ade會自動成為第一個字符串,因此當(dāng)下標(biāo)變成1時(shí),得到的字符串就不是ade而是afg了,字符串ade并沒有被刪除掉,便會出現(xiàn)錯誤的結(jié)果。
為了防止通過下標(biāo)刪除集合元素時(shí)產(chǎn)生類似的錯誤,每次刪除完元素后應(yīng)將下標(biāo)減一,即i--。改正代碼后再次測試,結(jié)果就正確了:

demo2()運(yùn)行效果

接著是用forin語句遍歷,很簡單地想到代碼應(yīng)該為:

for (String s : list) {
    if(s.contains("a")){
        list.remove(s);
    }
}
System.out.println(list);

然而事與愿違,程序報(bào)錯了,拋出了一個異常:

程序報(bào)錯

這個異常為并發(fā)修改異常。我們將關(guān)注的焦點(diǎn)放在第三行錯誤信息上,可以發(fā)現(xiàn)是ArrayList類中Itr類(迭代器類)的next()方法出現(xiàn)了異常,查看方法的聲明,會發(fā)現(xiàn)調(diào)用了checkForComodification()方法,繼續(xù)查看聲明:

checkForComodification()方法聲明

這里出現(xiàn)了兩個參數(shù):modCountexpectedModCount,并且如果這兩個參數(shù)不等,則會拋出并發(fā)修改異常。expectedModCount參數(shù)是集合的初始化長度,而modCount參數(shù)則是集合的當(dāng)前長度。回到ArrayList類中Itr類的聲明,會有這么一段代碼:

集合長度初始化

也就是說,在集合初始化的時(shí)候,expectedModCountmodCount是相等的,但是一旦向集合中添加或者刪除了元素,兩者就不等了,也就會拋出異常。
要想解決拋出異常的問題,可以使用Itr類中的remove()方法,先查看方法的聲明:

remove()方法聲明

有一句代碼十分關(guān)鍵:expectedModCount = modCount;。顯然調(diào)用remove()方法能夠?qū)?code>expectedModCount與modCount置為相等,因此這樣能夠避免程序拋出并發(fā)修改異常。
用集合迭代器的remove()方法刪除集合的元素:

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String s = iterator.next();
    if (s.contains("a")) {
        iterator.remove();
    }
}
System.out.println(list);

用JUnit進(jìn)行單體測試,結(jié)果自然是正確的:

demo2()運(yùn)行效果

如果只需要刪除集合中的一個元素例如刪除字符串afg,這時(shí)候就可以使用集合的remove()方法進(jìn)行刪除,但前提是刪除完之后必須用break語句跳出循環(huán):

for (String s : list) {
    if (s.equals("afg")) {
        list.remove(s);
        break;
    }
}   
System.out.println(list);
demo2()運(yùn)行效果

原理也很簡單,還記得之前介紹過forin語句就是迭代器遍歷嗎?用break語句跳出循環(huán)使得迭代器無法調(diào)用next()方法,從而也不會拋出并發(fā)修改異常了。
還有一種方法,拋出異常是由集合自身性質(zhì)所決定的,如果采用不會拋出這類異常的集合不就能解決問題了嗎?JDK5版本引入了Copy-On-Write容器的概念,CopyOnWrite機(jī)制的理念就是:當(dāng)我們往一個容器添加或刪除元素的時(shí)候,不直接往當(dāng)前容器添加或刪除,而是先將當(dāng)前容器進(jìn)行Copy,復(fù)制出一個新的容器,然后新的容器里添加或刪除元素,在這之后再將原容器的引用指向新的容器。目前有CopyOnWriteArrayListCopyOnWriteArraySet兩個實(shí)現(xiàn)類,因此我們可以采用CopyOnWriteArrayList類:

List<String> list = new CopyOnWriteArrayList<String>();
list.add("abc");
list.add("ade");
list.add("afg");
list.add("def");
list.add("opq");
for (String s : list) {
    if (s.contains("a")){
        list.remove(s);
    }
}
System.out.println(list);

用JUnit進(jìn)行測試,結(jié)果是正確的:

demo2()運(yùn)行效果
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容