Java中的forin語句

forin的原理

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

@Test
public void demo() {
    String arr[] = { "abc", "def", "opq" };
    for (int i = 0; i < arr.length; i++) {//通過下標遍歷數組
        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進行單體測試,兩種方法的輸出結果是一樣的:

demo()運行效果

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

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

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

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

用JUnit進行單體測試,輸出的結果與之前相同:

demo()運行效果

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

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

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

demo()運行效果

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

數組內存

forin的實現

如果一個對象想使用forin語句進行遍歷,則對象類必須滿足兩個條件:實現Iterable接口和實現Iterator方法。之所以ArrayList集合類能夠實現forin語句遍歷,就是因為其滿足上述兩個條件:

Collection接口繼承Iterable接口

Collection接口實現Iterator方法

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

public class Phone implements Iterable<String> {//實現Iterable接口
    String[] names = { "蘋果", "三星", "華為", "小米", "魅族" };
    public Iterator<String> iterator() {//實現Iterator方法同時自定義迭代器
        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() {
        }
    }
}

創建新的方法用于測試:

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

用JUnit進行測試,結果是正確的:

demo1()運行結果

forin刪除元素

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

@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);
}

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

demo2()運行效果

這是因為當刪除完第一個字符串abc后,第二個字符串ade會自動成為第一個字符串,因此當下標變成1時,得到的字符串就不是ade而是afg了,字符串ade并沒有被刪除掉,便會出現錯誤的結果。
為了防止通過下標刪除集合元素時產生類似的錯誤,每次刪除完元素后應將下標減一,即i--。改正代碼后再次測試,結果就正確了:

demo2()運行效果

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

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

然而事與愿違,程序報錯了,拋出了一個異常:

程序報錯

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

checkForComodification()方法聲明

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

集合長度初始化

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

remove()方法聲明

有一句代碼十分關鍵:expectedModCount = modCount;。顯然調用remove()方法能夠將expectedModCountmodCount置為相等,因此這樣能夠避免程序拋出并發修改異常。
用集合迭代器的remove()方法刪除集合的元素:

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

用JUnit進行單體測試,結果自然是正確的:

demo2()運行效果

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

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

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

demo2()運行效果
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容