JavaRush /Java Blog /Random-TW /For 和 For-Each 迴圈:關於我如何迭代、被迭代、但未被迭代的故事
Viacheslav
等級 3

For 和 For-Each 迴圈:關於我如何迭代、被迭代、但未被迭代的故事

在 Random-TW 群組發布

介紹

循環是程式語言的基本結構之一。例如,在Oracle網站上有一個章節“ Lesson: Language Basics ”,其中循環有一個單獨的課程“ The for Statement ”。讓我們回顧一下基礎知識:循環由三個表達式(語句)組成:初始化(初始化)、條件(終止)和增量(增量):
For 和 For-Each 循環:一個關於我如何迭代、迭代但不迭代的故事 - 1
有趣的是,它們都是可選的,這意味著如果我們願意,我們可以寫:
for (;;){
}
確實,在這種情況下我們會得到一個無限循環,因為 我們沒有指定退出循環(終止)的條件。在執行整個循環之前,初始化表達式僅執行一次。始終值得記住的是,循環有其自己的範圍。這意味著初始化終止增量和循環體看到相同的變數。使用花括號總是可以輕鬆確定範圍。括號內的所有內容在括號外不可見,但括號外的所有內容在括號內可見。 初始化只是一個表達式。例如,您通常可以呼叫不傳回任何內容的方法,而不是初始化變數。或直接跳過它,在第一個分號之前留一個空格。以下表達式指定終止條件。只要為true,就會執行迴圈。如果為false,則不會開始新的迭代。如果你看下圖,我們在編譯過程中會遇到錯誤,IDE 會抱怨:循環中的表達式無法存取。由於循環中不會有單次迭代,因此我們將立即退出,因為 錯誤的:
For 和 For-Each 迴圈:關於我如何迭代、迭代但不迭代的故事 - 2
值得關注終止語句中的表達式:它直接決定您的應用程式是否會出現無限循環。 增量是最簡單的表達方式。它在循環的每次成功迭代後執行。而這個表達式也可以被跳過。例如:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
從範例中可以看出,循環的每次迭代我們都會以 2 為增量遞增,但前提是該值outerVar小於 10。此外,由於increment 語句中的表達式實際上只是一個表達式,因此可以包含任何東西。因此,沒有人禁止使用減量而不是增量,即 減少價值。您應該始終監視增量的寫入。+=首先執行增加,然後賦值,但是如果在上面的範例中我們編寫相反的內容,我們將得到一個無限循環,因為變數永遠outerVar不會收到更改的值:在這種情況下,它將在=+賦值之後計算。順便說一下,視圖增量也是一樣的++。例如,我們有一個循環:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
該循環有效且沒有任何問題。但後來重構人來了。他不理解增量,只是這樣做了:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
如果數值前面出現增量符號,則表示它會先增加,然後回到所指示的位置。在此範例中,我們將立即開始從陣列中提取索引 1 處的元素,跳過第一個元素。然後在索引 3 處我們將崩潰並出現錯誤「java.lang.ArrayIndexOutOfBoundsException」。正如您可能已經猜到的,這之前之所以有效,只是因為增量是在迭代完成後調用的。當將此表達式轉移到迭代時,一切都崩潰了。事實證明,即使在一個簡單的循環中,你也會弄得一團糟)如果你有一個數組,也許有一些更簡單的方法來顯示所有元素?
For 和 For-Each 循環:關於我如何迭代、迭代但不迭代的故事 - 3

對於每個循環

從 Java 1.5 開始,Java 開發人員for each loop在 Oracle 網站的指南中為我們提供了一種名為「The For-Each Loop」或版本1.5.0的設計。一般來說,它看起來像這樣:
For 和 For-Each 循環:關於我如何迭代、迭代但不迭代的故事 - 4
您可以閱讀 Java 語言規格 (JLS) 中對此構造的描述,以確保它並不神奇。這種結構在「 14.14.2. 增強的 for 語句」一章中進行了描述。正如您所看到的,foreach迴圈可以與陣列和實作java.lang.Iterable介面的陣列一起使用。也就是說,如果您確實需要,您可以實作java.lang.Iterable接口,並且foreach 循環可以與您的類別一起使用。你會立即說:“好吧,它是一個可迭代對象,但數組不是一個對象。算是吧。” 你會錯的,因為...... 在Java中,陣列是動態建立的物件。語言規範告訴我們:“在 Java 程式語言中,數組是物件。” 一般來說,陣列有點 JVM 的魔力,因為… 此陣列的內部結構是未知的,並且位於 Java 虛擬機器內部的某個位置。有興趣的可以閱讀stackoverflow上的答案:“ How does array class work in Java? ” 事實證明,如果我們不使用數組,那麼我們必須使用實作Iterable 的東西。例如:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
在這裡你可以記住,如果我們使用集合(java.util.Collection),多虧了這一點,我們得到了精確的Iterable。如果一個物件有一個實作 Iterable 的類,那麼當呼叫迭代器方法時,它有義務提供一個迭代器來迭代該物件的內容。例如,上面的程式碼將具有類似這樣的字節碼(在 IntelliJ Idea 中,您可以執行“檢視”->“顯示字節碼”:
For 和 For-Each 循環:一個關於我如何迭代、迭代但不迭代的故事 - 5
正如你所看到的,實際上使用了迭代器。如果不是foreach 循環,我們就必須寫如下內容:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (Iterator i = names.iterator(); /* continue if */ i.hasNext(); /* skip increment */) {
	String name = (String) i.next();
	System.out.println("Name = " + name);
}

迭代器

正如我們在上面看到的,Iterable介面表示,對於某個物件的實例,您可以獲得一個迭代器,用它可以迭代內容。同樣,這可以說是SOLID的單一職責原則。資料結構本身不應該驅動遍歷,但它可以提供應該驅動的遍歷。Iterator 的基本實作是,它通常被聲明為一個內部類,可以存取外部類別的內容並提供外部類別中包含的所需元素。ArrayList以下是迭代器如何返回元素的 類別的範例:
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];
}
正如我們所看到的,在迭代器的幫助下ArrayList.this存取外部類別及其變量elementData,然後從那裡返回一個元素。因此,取得迭代器非常簡單:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
它的工作歸結為這樣一個事實:我們可以檢查是否還有元素(hasNext方法),獲取下一個元素(next方法)和remove方法,該方法刪除透過next接收到的最後一個元素。刪除方法是可選的,不保證一定會實作。事實上,隨著 Java 的發展,介面也在發展。因此,在Java 8中,也出現了一種方法forEachRemaining,讓你對迭代器未存取到的剩餘元素執行一些操作。迭代器和集合有什麼有趣的地方?例如,有一個類別AbstractList。這是一個抽象類,是ArrayListand的父類LinkedList。我們對modCount這樣的字段很感興趣。每次更改都會更改清單的內容。那麼這對我們來說有什麼關係呢?事實上,迭代器確保在操作期間迭代它的集合不會改變。如您所知,列表迭代器的實作與modcount位於相同位置,即在類別中AbstractList。讓我們來看一個簡單的例子:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
names.add("modcount++");
System.out.println(iterator.next());
這是第一個有趣的事情,雖然不是主題。實際上Arrays.asList返回它自己的特殊的一個ArrayListjava.util.Arrays.ArrayList)。它沒有實作添加方法,因此它是不可修改的。JavaDoc 中對此進行了描述:固定大小。但事實上,它不僅僅是固定大小的。它也是不可變的,即不可改變的;刪除也不起作用。我們也會得到一個錯誤,因為...... 創建迭代器後,我們記住了其中的modcount。然後我們“從外部”(即不通過迭代器)更改集合的狀態並執行迭代器方法。因此,我們得到錯誤:java.util.ConcurrentModificationException。為了避免這種情況,迭代期間的變更必須透過迭代器本身執行,而不是透過存取集合來執行:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next();
iterator.remove();
System.out.println(iterator.next());
如您所知,如果iterator.remove()您以前不這樣做iterator.next(),那麼是因為。迭代器沒有指向任何元素,那麼我們會得到一個錯誤。在範例中,迭代器將轉到John元素,將其刪除,然後取得Sara元素。這裡一切都會好起來,但運氣不好,再次存在“細微差別”)java.util.ConcurrentModificationExceptionhasNext()僅當它返回true時才會發生。也就是說,如果透過集合本身刪除最後一個元素,迭代器不會掉落。有關更多詳細信息,最好查看“ #ITsubbotnik JAVA 部分:Java 謎題”中有關 Java 謎題的報告。我們開始如此詳細的對話的原因很簡單,當for each loop... 我們最喜歡的迭代器是在底層使用的。所有這些細微差別也適用於此。唯一的問題是,我們將無法存取迭代器,並且無法安全地刪除元素。順便說一下,正如您所理解的,狀態在創建迭代器時被記住。並且安全刪除僅在調用它的地方起作用。也就是說,這個選項將不起作用:
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
因為對於iterator2來說,透過iterator1的刪除是“外部的”,也就是說,它是在外部的某個地方執行的,而他對此一無所知。關於迭代器的話題,我也想指出這一點。專門為介面實作製作了一個特殊的擴展迭代器List。他們給他取了名字ListIterator。它不僅允許您向前移動,還可以向後移動,還允許您找出上一個元素和下一個元素的索引。此外,它還允許您替換當前元素或在當前迭代器位置和下一個迭代器位置之間的位置插入新元素。正如您所猜測的,由於實現了索引訪問, ListIterator所以允許這樣做。List
For 和 For-Each 迴圈:關於我如何迭代、迭代但不迭代的故事 - 6

Java 8 和迭代

Java 8 的發布讓許多人的生活變得更輕鬆。我們也沒有忽略物件內容的迭代。要理解它是如何工作的,您需要對此說幾句話。Java 8 引進了java.util.function.Consumer類別。這是一個例子:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
Consumer是一個函數式接口,這意味著接口內部只有 1 個未實現的抽象方法,需要在指定該接口的實現的那些類中強制實現。這讓你可以使用像 lambda 這樣神奇的東西。本文不是要討論這個,但我們需要了解為什麼我們可以使用它。因此,使用 lambdas,上面的Consumer可以這樣重寫: Consumer consumer = (obj) -> System.out.println(obj); 這意味著 Java 看到一個名為 obj 的東西將被傳遞到輸入,然後會針對這個 obj 執行 -> 之後的表達式。至於迭代,現在我們可以這樣做:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
如果你去看看這個方法forEach,你會發現一切都很簡單。這是我們最喜歡的for-each loop
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
也可以使用迭代器漂亮地刪除元素,例如:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
在本例中,removeIf方法所採用的輸入不是Consumer,而是Predicate。它返回布林值。在這種情況下,如果謂詞說“ true ”,那麼該元素將被刪除。有趣的是,這裡並不是一切都是顯而易見的))那麼,你想要什麼?會議上需要給人們創造謎題的空間。例如,我們使用以下程式碼來刪除迭代器在迭代後可以到達的所有內容:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
while (iterator.hasNext()) {
    iterator.next(); // Следующий элемент
    iterator.remove(); // Удалor его
}
System.out.println(names);
好的,這裡一切正常。但我們畢竟記得 Java 8。因此,我們嘗試簡化一下程式碼:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
iterator.forEachRemaining(obj -> iterator.remove());
System.out.println(names);
真的變得更美了嗎?但是,將會出現java.lang.IllegalStateException。原因是...Java 中的一個錯誤。事實證明它是固定的,但是在 JDK 9 中。這裡是 OpenJDK 中任務的連結:Iterator.forEachRemaining vs. 迭代器.刪除。當然,這已經討論過:Why iterator.forEachRemaining does not remove element in Consumer lambda? 那麼,另一種方法是直接透過 Stream API:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

結論

正如我們從上面的所有材料中看到的,循環for-each loop只是迭代器之上的“語法糖”。不過,現在很多地方都在使用它。此外,任何產品都必須謹慎使用。例如,一個無害的人forEachRemaining可能隱藏著令人不快的驚喜。而這也再次證明了單元測試的必要性。良好的測試可以識別程式碼中的此類用例。您可以觀看/閱讀有關該主題的內容: #維亞切斯拉夫
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION