天天看點

for 循環用了那麼多次,但你真的了解它麼?

其實我們寫代碼的時候一直都在使用for循環,但是偶爾還是會糾結用哪一個循環。

一、基礎的for循環

0、使用while也是一種循環方式,此處探究for相關的循環,就不做拓展了。

1、周遊數組的時候,初學時是使用的如下樣式的for循環:

for(int i=0;i<a.length;i++){
    System.out.println(n);
}      

2、而周遊集合的時候使用的都是Iterator疊代器:

給定一組人名,兩兩組隊(此處允許自己和自己組隊),實作如下:

enum Option {Tom, Jerry, Jack, Mary}      

想象中的寫法是:

Collection<Option> options = Arrays.asList(Option.values());
for(Iterator<Option> i = options.iterator(); i.hasNext();){
    for (Iterator<Option> j = options.iterator(); j.hasNext();) {
        System.out.println(i.next()+" "+j.next());
    }      

但是執行過後你會發現這段代碼是有瑕疵的,出現的結果隻有四組:

for 循環用了那麼多次,但你真的了解它麼?

那麼剩下的組合去哪裡了呢?

這裡程式并不會抛出異常,隻是單純的因為​

​i.next()​

​每次都會取下一個值,是以就出現了上圖的情況。

但是,如果外部集合的元素大于内部元素:

例如下面這段代碼:

enum OptionFirst {Tom, Jerry, Jack, Mary}
enum OptionSecond {Tom, Jerry, Jack, Mary, Mali, Tomsun, Lijie, Oolyyou}

public static void main(String[] args) {
    Collection<OptionFirst> optionFirstCollection = Arrays.asList(OptionFirst.values());
    Collection<OptionSecond> optionSecondCollection = Arrays.asList(OptionSecond.values());
    for (Iterator<OptionFirst> i = optionFirstCollection.iterator(); i.hasNext(); ) {
        for (Iterator<OptionSecond> j = optionSecondCollection.iterator(); j.hasNext(); ) {
            System.out.println(i.next() + " " + j.next());
        }
    }
}      

運作後,就會抛出​

​java.util.NoSuchElementException​

​異常,造成的原因就是因為外部循環調用了多次,而内部循環因為元素不足,導緻循環抛出了這樣的異常。

要想解決這種困擾隻需要在二次循環前添加一個變量來儲存外部元素;即可實作想要達到的效果。

Collection<Option> options = Arrays.asList(Option.values());
for(Iterator<Option> i = options.iterator(); i.hasNext();){
    Option option = i.next();
    for (Iterator<Option> j = options.iterator(); j.hasNext();) {
        System.out.println(option+" "+j.next());
    }
}      
for 循環用了那麼多次,但你真的了解它麼?

二、for-each循環

這種循環方式不論是數組還是集合都實用,而且效率更高;表達形式更加簡潔明了。

for(Element e:elements){
    System.out.println(e);
}      

當再次遇到上面的兩兩組隊問題時,根本不需要考慮元素不足的問題,而且代碼也簡潔多了:

for (Option option : options) {
    for (Option rank : options) {
        System.out.println(option + " " + rank);
    }
}      

《Effective Java》中是這樣子寫for-each循環的:

for 循環用了那麼多次,但你真的了解它麼?

三、for-each is not god

說了for-each那麼多好處,但是它也不是神,并非萬能的,有那麼三種情況是它需要注意的。

3.1、解構過濾的時候不能使用

如果需要周遊集合,并删除標明的元素,就需要使用顯式的疊代器,以便可以調用它的remove方法。不過在Java8中增加的Collection的removeIf方法常常可以避免顯式的周遊。

例如下面這段代碼:

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

list.add("1");
list.add("2");

for (String item : list) {
    if ("1".equals(item)) {
        list.remove(item);
        System.out.println("執行if語句成功");
    }
}      

直接運作這段代碼是沒什麼問題的,數組list能成功删除元素1,也能列印對應語句。

但是,我們進行如下任意一種操作:

  • 若把list.remove(item)換成list.add(“3”);操作如何?
  • 若在第6行添加list.add("3");那麼代碼會出錯嗎?
  • 若把if語句中的“1”換成“2”,結果你感到意外嗎?

如果都能正确執行當然就不需要問了,是以3個都會報ConcurrentModificationException的異常;

for 循環用了那麼多次,但你真的了解它麼?

而出現這些情況的原因稍稍解釋下就是:

首先,這涉及多線程操作,Iterator是不支援多線程操作的,List類會在内部維護一個modCount的變量,用來記錄修改次數。

舉例:ArrayList源碼

protected transient int modCount = 0;      

每生成一個Iterator,Iterator就會記錄該modCount,每次調用next()方法就會将該記錄與外部類List的modCount進行對比,發現不相等就會抛出多線程編輯異常。

為什麼這麼做呢?我的了解是你建立了一個疊代器,該疊代器和要周遊的集合的内容是緊耦合的,意思就是這個疊代器對應的集合内容就是目前的内容,我肯定不會希望在我冒泡排序的時候,還有線程在向我的集合裡插入資料對吧?是以Java用了這種簡單的處理機制來禁止周遊時修改集合。

至于為什麼删除“1”就可以呢,原因在于foreach和疊代器的hasNext()方法,foreach這個文法,實際上就是

while(itr.hasNext()){
    itr.next()
}      

是以每次循環都會先執行hasNext(),那麼看看ArrayList的hasNext()是怎麼寫的:

public boolean hasNext() {
    return cursor != size;
}      

cursor是用于标記疊代器位置的變量,該變量由0開始,每次調用next執行+1操作,于是:

是以代碼在執行删除“1”後,size=1,cursor=1,此時hasNext()傳回false,結束循環,是以你的疊代器并沒有調用next查找第二個元素,也就無從檢測modCount了,是以也不會出現多線程修改異常;但當你删除“2”時,疊代器調用了兩次next,此時size=1,cursor=2,hasNext()傳回true,于是疊代器傻乎乎的就又去調用了一次next(),是以也引發了modCount不相等,抛出多線程修改的異常。

當你的集合有三個元素的時候,你就會神奇的發現,删除“1”是會抛出異常的,但删除“2”就沒有問題了,究其原因,和上面的程式執行順序是一緻的。

是以,在《阿裡巴巴Java開發手冊中有這樣一條規定》:

for 循環用了那麼多次,但你真的了解它麼?

3.2、轉換

如果需要周遊清單或數組,并取代它的部分或者全部元素值,就需要使用清單疊代器或者數組索引,以便替換元素的值。

因為for-each是一循到底的,中間不做停留和位置資訊的顯示;是以要替換元素就不能使用它了。

3.3、平行疊代

如果需要并行的周遊多個集合,就需要顯式的控制疊代器或者索引變量,以便所有疊代器或者索引變量都可以同步前進(就像上面講述Iterator疊代器的時候提到的組合減少的情況,隻想出現下标一一對應的元素組合)。

4、總結

繼續閱讀