JavaRush /Java Blog /Random-JA /For および For-Each ループ: どのように反復したのか、反復されたのか、反復されなかったのかの物語
Viacheslav
レベル 3

For および For-Each ループ: どのように反復したのか、反復されたのか、反復されなかったのかの物語

Random-JA グループに公開済み

導入

ループはプログラミング言語の基本構造の 1 つです。たとえば、Oracle の Web サイトには「レッスン: 言語の基礎」セクションがあり、ループには別のレッスン「for ステートメント」があります。基本を復習しましょう。ループは、初期化(初期化)、条件(終了)、およびインクリメント(増分) の 3 つの式 (ステートメント) で構成されます。
For および For-Each ループ: どのように反復し、反復したが、反復しなかったかについての物語 - 1
興味深いことに、これらはすべてオプションです。つまり、必要に応じて次のように記述できます。
for (;;){
}
確かに、この場合、無限ループが発生します。ループを終了するための条件は指定しません。初期化式は、ループ全体が実行される前に 1 回だけ実行されます。サイクルには独自のスコープがあることを常に覚えておく価値があります。これは、initializationterminationincrementおよびループ本体が同じ変数を参照することを意味します。スコープは中括弧を使用すると常に簡単に決定できます。括弧内のすべては括弧の外側には表示されませんが、括弧の外側のすべては括弧の内側に表示されます。 初期化は単なる式です。たとえば、変数を初期化する代わりに、通常は何も返さないメソッドを呼び出すことができます。または、最初のセミコロンの前に空白スペースを残してスキップします。終了条件は次の式で指定します。trueである限り、ループが実行されます。falseの場合、新しい反復は開始されません。下の図を見ると、コンパイル中にエラーが発生し、IDE が「ループ内の式に到達できません」と警告します。ループ内に単一の反復がないため、すぐに終了します。間違い:
For および For-Each ループ: 繰り返し、繰り返したが、繰り返しなかった方法の物語 - 2
終了ステートメントの式に注目する価値があります。これは、アプリケーションが無限ループになるかどうかを直接決定します。 インクリメントは最も単純な式です。これは、ループの反復が成功するたびに実行されます。また、この式は省略することもできます。例えば:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
例からわかるように、ループの各反復では 2 ずつ増分しますが、値がouterVar10 未満である場合に限ります。また、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 loopOracle サイトのガイドに記載されている「For-Each Loop」またはバージョン1.5.0と呼ばれる設計を提供してくれました。一般に、次のようになります。
For および For-Each ループ: どのように反復し、反復したが、反復しなかったかについての物語 - 4
Java 言語仕様 (JLS) でこの構造の説明を読むと、それが魔法ではないことが確認できます。この構造については、「 14.14.2. 拡張された for ステートメント」の章で説明します。ご覧のとおり、for each ループは配列およびjava.lang.Iterableインターフェイスを実装する配列で使用できます。つまり、本当に必要な場合は、java.lang.Iterableインターフェイスを実装し、各ループをクラスで使用できます。あなたはすぐに、「これは反復可能なオブジェクトですが、配列はオブジェクトではありません。そのようなものです。」と言うでしょう。そしてあなたは間違っているでしょう、なぜなら... Java では、配列は動的に作成されるオブジェクトです。言語仕様には、「Java プログラミング言語では、配列はオブジェクトである」と記載されています。一般に、配列はちょっとした JVM マジックです。なぜなら... 配列が内部的にどのように構成されているかは不明であり、Java 仮想マシン内のどこかに配置されています。興味のある人は、stackoverflow で「Java で配列クラスはどのように機能しますか?」という回答を読むことができます。配列を使用していない場合は、Iterable を実装するものを使用する必要があることがわかります。例えば:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
ここで、コレクション ( java.util.Collection ) を使用すると、そのおかげで正確にIterable が得られること を覚えておいてください。オブジェクトに Iterable を実装するクラスがある場合、iterator メソッドが呼び出されたときに、そのオブジェクトの内容を反復処理する Iterator を提供する必要があります。たとえば、上記のコードは次のようなバイトコードになります (IntelliJ Idea では、「表示」 -> 「バイトコードの表示」を実行できます:
For ループと For-Each ループ: どのように反復し、反復したが、反復しなかったかについての物語 - 5
ご覧のとおり、実際にはイテレータが使用されています。for each ループがなかった場合は、次のように記述する必要があります。
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メソッド)、​​そしてnextを通じて受け取った最後の要素を削除するRemoveメソッド を実行できるという事実に帰着します。Removeメソッドはオプションであり、実装されることは保証されていません。実際、Java が進化するにつれて、インターフェースも進化します。したがって、Java 8 には、イテレータが訪問しない残りの要素に対して何らかのアクションを実行できるメソッドもありました。イテレータとコレクションの何が興味深いのでしょうか? たとえば、 クラス があります。これは、およびの親である抽象クラスです。modCountなどのフィールドがあるため、これは私たちにとって興味深いものです。変更するたびにリストの内容も変わります。それで、それは私たちにとって何の意味があるのでしょうか?そして、反復子は、反復処理の対象となるコレクションが操作中に変更されないことを確認します。ご存知のとおり、リストの反復子の実装はmodcountと同じ場所、つまり class 内にあります。簡単な例を見てみましょう。 forEachRemainingAbstractListArrayListLinkedListAbstractList
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独自の特別なものArrayList( java.util.Arrays.ArrayList ) を返します。追加メソッドは実装されていないため、変更できません。これについては、JavaDoc「fixed-size」に記載されています。しかし実際には、それは固定サイズ以上のものです。また、不変、つまり変更不可能です。削除しても機能しません。エラーも発生します。なぜなら... イテレータを作成したら、その中の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.ConcurrentModificationExceptionは、 truehasNext()を返した場合にのみ発生します。つまり、コレクション自体を通じて最後の要素を削除しても、イテレータは落ちません。詳細については、「#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 つだけあることを意味します。これにより、ラムダなどの魔法のようなものを使用できるようになります。この記事はそれについて説明するものではありませんが、なぜそれを使用できるのかを理解する必要があります。したがって、ラムダを使用すると、上記の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. Iterator.remove。当然のことながら、これについてはすでに議論されています。なぜ iterator.forEachRemaining は Consumer ラムダの要素を削除しないのでしょうか? もう 1 つの方法は、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