Javaでコレクションを反復処理する

物のコレクションがあるときはいつでも、そのコレクション内のアイテムを体系的にステップスルーするための何らかのメカニズムが必要になります。日常の例として、さまざまなテレビチャンネルを繰り返すことができるテレビのリモコンを考えてみましょう。同様に、プログラミングの世界では、ソフトウェアオブジェクトのコレクションを体系的に反復するメカニズムが必要です。Javaには、インデックス(配列を反復処理するため)、カーソル(データベースクエリの結果を反復処理するため)、列挙(Javaの初期バージョン)、イテレータ(Javaの最新バージョン)など、反復のためのさまざまなメカニズムが含まれています。

Iteratorパターン

反復子は、いくつかの動作は各要素に対して実行されると、順次アクセスされるコレクションのすべての要素を可能にする機構です。基本的に、イテレータは、カプセル化されたオブジェクトのコレクションを「ループ」する手段を提供します。イテレータの使用例には、次のものがあります。

  • ディレクトリ(別名フォルダ)内の各ファイルにアクセスして、その名前を表示します。
  • グラフ内の各ノードにアクセスして、特定のノードから到達可能かどうかを判断します。
  • 待ち行列にいる各顧客を訪問し(たとえば、銀行の列をシミュレートする)、その顧客がどれだけ待っているかを調べます。
  • コンパイラの抽象構文ツリー(パーサーによって生成される)の各ノードにアクセスし、セマンティックチェックまたはコード生成を実行します。(このコンテキストでは、Visitorパターンを使用することもできます。)

イテレータの使用には特定の原則が当てはまります。一般に、複数のトラバーサルを同時に進行させることができるはずです。つまり、イテレータはネストされたループの概念を考慮に入れる必要があります。イテレータは、反復の動作自体がコレクションを変更してはならないという意味でも非破壊的である必要があります。もちろん、コレクション内の要素に対して実行されている操作によって、一部の要素が変更される可能性があります。イテレータがコレクションからの要素の削除またはコレクションの特定のポイントでの新しい要素の挿入をサポートすることも可能かもしれませんが、そのような変更はプログラム内で明示的であり、反復の副産物ではありません。場合によっては、さまざまなトラバーサル方法のイテレーターが必要になることもあります。たとえば、ツリーのプレオーダーとポストオーダーのトラバーサル、またはグラフの深さ優先と幅優先のトラバーサルです。

複雑なデータ構造の反復

私は最初、データ構造化機能が配列だけであったFORTRANの初期バージョンでプログラミングすることを学びました。インデックスとDOループを使用して配列を反復処理する方法をすぐに学びました。そこから、レコードの配列をシミュレートするために複数の配列に共通のインデックスを使用するというアイデアへの短い精神的な飛躍でした。ほとんどのプログラミング言語には配列と同様の機能があり、配列の単純なループをサポートしています。しかし、最新のプログラミング言語は、リスト、セット、マップ、ツリーなどのより複雑なデータ構造もサポートしています。これらの機能はパブリックメソッドを介して利用できますが、内部の詳細はクラスのプライベート部分に隠されています。プログラマーは、イテレーターの目的である内部構造を公開せずに、これらのデータ構造の要素をトラバースできる必要があります。

イテレータと4つのギャングのデザインパターン

Gang of Four(以下を参照)によると、イテレーターのデザインパターンは動作パターンであり、その重要なアイデアは「リスト[ ed。thinkcollection ]オブジェクトからアクセスとトラバーサルの責任を取り除いてイテレーターに入れることです。オブジェクト。」この記事は、イテレーターが実際にどのように使用されるかについてではなく、イテレーターパターンについての記事です。パターンを完全にカバーするには、イテレータの設計方法、設計の参加者(オブジェクトとクラス)、可能な代替設計、およびさまざまな設計代替のトレードオフについて話し合う必要があります。イテレータが実際にどのように使用されるかに焦点を当てたいと思いますが、イテレータパターンとデザインパターンを一般的に調査するためのいくつかのリソースを紹介します。

  • デザインパターン: Erich Gamma、Richard Helm、Ralph Johnson、およびJohn Vlissides(Gang of Fourまたは単にGoFとしても知られている)によって書かれた再利用可能なオブジェクト指向ソフトウェアの要素(Addison-Wesley Professional、1994)は、学習のための決定的なリソースですデザインパターンについて。この本は1994年に最初に出版されましたが、40を超える印刷があったことからも明らかなように、古典的なままです。
  • メリーランド大学ボルチモア郡の講師であるBobTarrは、Iteratorパターンの紹介を含む、デザインパターンに関するコースの優れたスライドセットを持っています。
  • David GearyのJavaWorldシリーズのJavaデザインパターンでは、シングルトン、オブザーバー、コンポジットパターンなど、Gang ofFourのデザインパターンの多くが紹介されています。また、JavaWorldでは、Jeff Friesenによる最近の3部構成のデザインパターンの概要に、GoFパターンのガイドが含まれています。

アクティブイテレータとパッシブイテレータ

誰が反復を制御するかに応じて、反復子を実装するための2つの一般的なアプローチがあります。以下のためのアクティブなイテレータ(としても知られている明示的なイテレータまたは外部イテレータ、)、クライアントは、クライアントがイテレータを作成するという意味で反復を制御し、次の要素に進めるためにときにそれを伝え、すべての要素が訪問されているかどうかを確認するためのテスト等々。このアプローチはC ++のような言語で一般的であり、GoFの本で最も注目されているアプローチです。 Javaのイテレータにはさまざまな形式がありますが、Java 8より前の実行可能なオプションは、基本的にアクティブなイテレータを使用することだけでした。

以下のために受動イテレータ(としても知られている暗黙イテレータ内部イテレータ、またはイテレータをコールバック)、反復子自体が反復を制御します。クライアントは基本的にイテレータに「コレクション内の要素に対してこの操作を実行する」と言います。このアプローチは、無名関数またはクロージャを提供するLISPのような言語で一般的です。Java 8のリリースにより、この反復へのアプローチは、Javaプログラマーにとって合理的な代替手段になりました。

Java8の命名スキーム

Windows(NT、2000、XP、VISTA、7、8、...)ほど悪くはありませんが、Javaのバージョン履歴にはいくつかの命名スキームが含まれています。まず、Java Standard Editionを「JDK」、「J2SE」、または「JavaSE」と呼ぶ必要がありますか? Javaのバージョン番号は最初は非常に単純で(1.0、1.1など)、Java(またはJDK)5というブランドのバージョン1.5ですべてが変更されました。Javaの初期バージョンを指すときは、「Java1.0」や「Java 1.1」ですが、Javaの5番目のバージョンの後、「Java5」や「Java8」などのフレーズを使用します。

To illustrate the various approaches to iteration in Java, I need an example of a collection and something that needs to be done with its elements. For the initial part of this article I'll use a collection of strings representing names of things. For each name in the collection, I will simply print its value to standard output. These basic ideas are easily extended to collections of more complicated objects (such as employees), and where the processing for each object is a little more involved (like giving each highly rated employee a 4.5 percent raise).

Other forms of iteration in Java 8

I'm focusing on iterating over collections, but there are other, more specialized forms of iteration in Java. For example, you might use a JDBC ResultSet to iterate over the rows returned from a SELECT query to a relational database, or use a Scanner to iterate over an input source.

Iteration with the Enumeration class

In Java 1.0 and 1.1, the two primary collection classes were Vector and Hashtable, and the Iterator design pattern was implemented in a class called Enumeration. In retrospect this was a bad name for the class. Do not confuse the class Enumeration with the concept of enum types, which didn't appear until Java 5. Today both Vector and Hashtable are generic classes, but back then generics were not part of the Java language. The code to process a vector of strings using Enumeration would look something like Listing 1.

Listing 1. Using enumeration to iterate over a vector of strings

 Vector names = new Vector(); // ... add some names to the collection Enumeration e = names.elements(); while (e.hasMoreElements()) { String name = (String) e.nextElement(); System.out.println(name); } 

Iteration with the Iterator class

Java 1.2 introduced the collection classes that we all know and love, and the Iterator design pattern was implemented in a class appropriately named Iterator. Because we didn't yet have generics in Java 1.2, casting an object returned from an Iterator was still necessary. For Java versions 1.2 through 1.4, iterating over a list of strings might resemble Listing 2.

Listing 2. Using an Iterator to iterate over a list of strings

 List names = new LinkedList(); // ... add some names to the collection Iterator i = names.iterator(); while (i.hasNext()) { String name = (String) i.next(); System.out.println(name); } 

Iteration with generics and the enhanced for-loop

Java 5 gave us generics, the interface Iterable, and the enhanced for-loop. The enhanced for-loop is one of my all-time-favorite small additions to Java. The creation of the iterator and calls to its hasNext() and next() methods are not expressed explicitly in the code, but they still take place behind the scenes. Thus, even though the code is more compact, we are still using an active iterator. Using Java 5, our example would look something like what you see in Listing 3.

Listing 3. Using generics and the enhanced for-loop to iterate over a list of strings

 List names = new LinkedList(); // ... add some names to the collection for (String name : names) System.out.println(name); 

Java 7 gave us the diamond operator, which reduces the verbosity of generics. Gone were the days of having to repeat the type used to instantiate the generic class after invoking the new operator! In Java 7 we could simplify the first line in Listing 3 above to the following:

 List names = new LinkedList(); 

A mild rant against generics

The design of a programming language involves tradeoffs between the benefits of language features versus the complexity they impose on the syntax and semantics of the language. For generics, I am not convinced that the benefits outweigh the complexity. Generics solved a problem that I did not have with Java. I generally agree with Ken Arnold's opinion when he states: "Generics are a mistake. This is not a problem based on technical disagreements. It's a fundamental language design problem [...] The complexity of Java has been turbocharged to what seems to me relatively small benefit."

Fortunately, while designing and implementing generic classes can sometimes be overly complicated, I have found that using generic classes in practice is usually straightforward.

Iteration with the forEach() method

Before delving into Java 8 iteration features, let's reflect on what's wrong with the code shown in the previous listings–which is, well, nothing really. There are millions of lines of Java code in currently deployed applications that use active iterators similar to those shown in my listings. Java 8 simply provides additional capabilities and new ways of performing iteration. For some scenarios, the new ways can be better.

The major new features in Java 8 center on lambda expressions, along with related features such as streams, method references, and functional interfaces. These new features in Java 8 allow us to seriously consider using passive iterators instead of the more conventional active iterators. In particular, the Iterable interface provides a passive iterator in the form of a default method called forEach().

A default method, another new feature in Java 8, is a method in an interface with a default implementation. In this case, the forEach() method is actually implemented using an active iterator in a manner similar to what you saw in Listing 3.

Collection classes that implement Iterable (for example, all list and set classes) now have a forEach() method. This method takes a single parameter that is a functional interface. Therefore the actual parameter passed to the forEach() method is a candidate for a lambda expression. Using the features of Java 8, our running example would evolve to the form shown in Listing 4.

Listing 4. Iteration in Java 8 using the forEach() method

 List names = new LinkedList(); // ... add some names to the collection names.forEach(name -> System.out.println(name)); 

Note the difference between the passive iterator in Listing 4 and the active iterator in the previous three listings. In the first three listings, the loop structure controls the iteration, and during each pass through the loop, an object is retrieved from the list and then printed. In Listing 4, there is no explicit loop. We simply tell the forEach() method what to do with the objects in the list — in this case we simply print the object. Control of the iteration resides within the forEach() method.

Iteration with Java streams

次に、リスト内の名前を単に印刷するよりも少し複雑なことを行うことを検討しましょう。たとえば、文字Aで始まる名前の数を数えたいとします。より複雑なロジックをラムダ式の一部として実装することも、Java8の新しいStreamAPIを使用することもできます。後者のアプローチを取りましょう。