目次


Java 8 のイディオム

従来の for ループに代わる関数型の手法

複雑な繰り返し処理でさえも、厄介な手間を省く新しいメソッド

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Java 8 のイディオム

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Java 8 のイディオム

このシリーズの続きに乞うご期待。

for ループは、その可変の構成要素のすべてを含め、非常に馴染み深い手法なので、多くの開発者は繰り返し処理の手段には、何も考えずに for ループを使用しようとします。Java 8 からは、複雑な繰り返し処理を単純にする、いくつかの強力な新しいメソッドを使えるようになっています。この記事では、IntStream クラスのメソッド rangeiteratelimit を使用して、範囲を繰り返し処理する方法、範囲内の値をスキップする方法を紹介します。さらに、リリース予定の Java 9 で導入される新しいメソッド、takeWhiledropWhile についても説明します。

for の問題

従来型の for ループは Java 言語の初回リリースで導入され、Java 5 では for ループを単純化した形の for-each バージョンも導入されました。ほとんどの開発者は、平凡な繰り返し処理には for-each を優先して使っていますが、範囲を指定した繰り返し処理や、範囲内の値をスキップする場合には、引き続き for ループを使用しています。

for ループは極めて有能ではあるものの、あまりにも多くの可変の構成要素が伴います。以下のような get set プロンプトを出力する極めて単純なタスクでさえも、可変の構成要素が必要になります。

リスト 1. 単純なタスクに対処する複雑なコード
 System.out.print("Get set...");
   for(int i = 1; i < 4; i++) {
     System.out.print(i + "...");
   }

リスト 1 では、ループ・インデックス変数 i を 1 で開始し、この変数の値を 4 未満に制限します。for ループを使用する場合、ループに増分処理を指示する必要があることに注意してください。この例では、前置インクリメントであるか後置インクリメントであるかの増分方法も指定しています。

リスト 1 に示されているのは大量のコードとは言えませんが、このコードには不要な部分が含まれています。Java 8 では、for ループより単純で、不要な部分が少ない代替手法を使用できるようになっています。それは、IntStream クラスの range メソッドです。リスト 1 の get set プロンプトを出力するコードは、range メソッドを使用して以下のように書き換えることができます。

リスト 2. 単純なタスクに対処する単純なコード
 System.out.print("Get set...");
   IntStream.range(1, 4)
     .forEach(i -> System.out.print(i + "..."));

リスト 2 では、コードの量が大幅に減っているわけではありませんが、複雑さは軽減されています。その理由は主に 2 つあります。

  1. for とは異なり、range では可変の変数を初期化する必要がありません。
  2. 繰り返し処理は自動的に行われるため、ループ・インデックスを使用する場合とは異なり増分方法を定義する必要はありません。

意味的には、元の for ループの変数 i は、状態が変化する変数です。range やこれと同様のメソッドの価値を知るは、設計によってどのような結果がもたらせるのかを理解することが役立ちます。

可変の変数とパラメーター

for ループの例で定義した変数 i は、単一の変数であるとは言え、ループの繰り返し処理ごとにその状態が変化します。range メソッドに含まれる変数 i は、ラムダ式に渡されるパラメーターであるため、繰り返し処理ごとに新たな変数になります。小さな違いではあるものの、この違いが、2 つのコードの世界をまったく別のものにします。その理由が明らかになるよう、これからいくつかの例を記載します。

まず、リスト 3 の for ループは、インデックス変数を内部クラス内で使用しようとしています。

リスト 3. 内部クラス内でインデックス変数を使用する例
ExecutorService executorService = Executors.newFixedThreadPool(10);

      for(int i = 0; i < 5; i++) {
        int temp = i;

        executorService.submit(new Runnable() {
          public void run() {
            //If uncommented the next line will result in an error
            //System.out.println("Running task " + i); 
            //local variables referenced from an inner class must be final or effectively final

            System.out.println("Running task " + temp); 
          }
        });
      }

      executorService.shutdown();

上記で使用している匿名内部クラスは、Runnable インターフェースを実装します。インデックス変数 i には run メソッド内でアクセスする必要がありますが、コンパイラーはそれを許しません。

この制約事項に対する次善策としては、インデックス変数のコピーとして、temp のようなローカル一時変数を作成するという方法があります。新しい繰り返し処理ごとに、変数 temp を作成するというわけです。このような変数は、Java 8 より前のバージョンでは final としてマークすることが要件となっていますが、Java 8 からは「実質的 final (effectively final)」と見なされます。それは、変数の状態は変更していないためです。いずれにしても、for ループでは、繰り返し処理によって単一の変数の状態が変化することのみを原因として、このような余計な変数が必要になります。

同じ問題を、今度は range 関数を使用して解決してみましょう。

リスト 4. 内部クラス内でラムダ式パラメーターを使用する例
 ExecutorService executorService = Executors.newFixedThreadPool(10);
                       
      IntStream.range(0, 5)
        .forEach(i -> 
          executorService.submit(new Runnable() {
            public void run() {
              System.out.println("Running task " + i); 
            }
          }));

      executorService.shutdown();

ラムダ式に渡されるパラメーターとして受け取られるインデックス変数 i は、ループ・インデックス変数と同じ意味を持つものではありません。リスト 3 で手作業で作成した temp とほぼ同じく、このパラメーター i は繰り返し処理ごとに新たな変数として出現します。変数の値はどの箇所でも変更していないため、この変数は実質的 final です。したがって、厄介な手間も混乱もなく、内部クラスのコンテキスト内から直接使用することができます。

Runnable は関数型インターフェースであるため、以下のように簡単に、匿名内部クラスをラムダ式で置き換えることができます。

リスト 5. 内部クラスをラムダ式で置き換える例
 IntStream.range(0, 5)
        .forEach(i -> 
          executorService.submit(() -> System.out.println("Running task " + i)));

比較的単純な繰り返し処理には、for を使用するのではなく range を使用することにメリットがあるのは明らかですが、for がとりわけ貴重な存在となるのは、複雑な繰り返し処理のシナリオに対処する場合です。その for の能力に匹敵する range とその他の Java 8 のメソッドを見ていきましょう。

閉範囲

for ループを作成する際は、以下のようにして、インデックス変数の変数が指定の範囲内になるよう指示できます。

リスト 6. 閉範囲を使用した for ループ
for(int i = 0; i <= 5; i++) {

上記の場合、インデックス変数 i は、05 の値を取ります。範囲を指定した繰り返し処理には、for ではなく、rangeClosed メソッドを使用して対処することもできます。その場合は、範囲の終了値を終えたときに繰り返し処理を終了するよう、IntStream に指示します。

リスト 7. rangeClosed メソッド
 IntStream.rangeClosed(0, 5)

この範囲を繰り返し処理する際には、境界値の 5 も範囲に含まれます。

値のスキップ

range メソッドと rangeClosed メソッドは、for の基本的なループ処理よりも単純で、円滑に for に置き換えることができますが、いくつかの特定の値をまたいで処理を行う必要がある場合はどうなるでしょうか?この場合、for では、事前に必要な作業があることで、比較的簡単な処理になっています。リスト 8 の for ループでは、繰り返し処理中に容易に値 2 つをスキップできます。

リスト 8. for を使用した値のスキップ
    int total = 0;
    for(int i = 1; i <= 100; i = i + 3) {
      total += i;
    }

ループ 8 のループは、1 から 100 までの値の範囲内で、3 つおきの値の合計を計算します。幾分複雑な処理ですが、for を使用すると比較的簡単になります。これと同じ問題を range を使って解決できないでしょうか?

まず考え付きそうなのは、IntStreamrange メソッドを filter または map と組み合わせて使用するという方法でしょう。けれどもそうすると、for ループを使用するよりも多くの作業が必要になってきます。よりもふさわしい解決方法としては、iteratelimit を組み合わせるという方法が考えられます。

リスト 9. limit を使用した繰り返し処理
    IntStream.iterate(1, e -> e + 3)
      .limit(34)
      .sum()

iterate メソッドを使用するのは、ごく簡単です。このメソッドは最初の引数として、繰り返し処理を開始する初期値を取ります。そして 2 番目の引数として渡されるラムダ式によって、繰り返し処理内での次の値が決まるという仕組みです。リスト 8 でもこれと同じように、式を for ループに渡してインデックス変数の値を増分しています。ただし、limit を使用する場合には落とし穴があります。それは、rangerangeClosed とは異なり、iterate メソッドに対して繰り返し処理の停止を指示する手段がないことです。値を制限しなければ、繰り返し処理は停止不可能になってしまいます。

この問題はどのように回避できるでしょうか?

ここで対象としている値は 1 ~ 100 の範囲であり、開始値の 1 を出発点に、値を 2 つスキップしながら処理する必要があります。簡単な計算で、この特定の範囲内には対象とする値が 34 個あることがわかります。したがって、この数値を limit メソッドに渡します。

このようなコードは機能しますが、そのプロセスはあまりにも複雑です。前もって計算するのは愉快なことではなく、作成するコードに制限が加わることになります。値を 2 つではなく、3 つスキップすることにした場合を考えてみてください。コードに変更を加えなければならないだけでなく、その結果、エラーも発生しやすくなります。これよりも有効な方法があるはずです。

takeWhile メソッド

リリース予定の Java 9 で新しく導入されることになっている takeWhile メソッドを使用すると、これまでより簡単に limit を使用して繰り返し処理を行えるようになります。takeWhile を使用することで、必要な条件を満たす限り繰り返し処理を続行するよう直接指定できます。リスト 9 の繰り返し処理は、takeWhile を使用すると以下のようなコードになります。

リスト 10. 条件付き繰り返し処理
 IntStream.iterate(1, e -> e + 3)
      .takeWhile(i -> i <= 100) //available in Java 9
      .sum()

上記では、事前に計算した回数に繰り返し処理を制限するのではなく、takeWhile に指定した条件を使用して、繰り返し処理から抜けるタイミングを動的に決定しています。繰り返し処理の回数を事前に計算するよりも、この手法のほうが遥かに簡単で、エラーが発生する可能性も低くなります。

takeWhile メソッドに対応するメソッドとして、特定の条件を満たすまで値をスキップする dropWhile というメソッドもあります。この 2 つは、JDK に追加されることが切望されていたメソッドです。takeWhile メソッドは中断命令のように機能する一方、dropWhile は続行命令のように機能します。Java 9 からは、この 2 つのメソッドをあらゆるタイプの Stream で使用できるようになります。

逆順の繰り返し処理

従来の for ループと IntStream のどちらを使用するかに関わらず、使用順方向の繰り返し処理に比べれば、逆順の繰り返し処理はずっと容易です。

以下に、for ループの逆順の繰り返し処理を示します。

リスト 11. for を使用した逆順の繰り返し処理
 for(int i = 7; i > 0; i--) {

range または rangeClosed の最初の引数には、2 番目の引数より大きい値を指定できないため、逆順の繰り返し処理には、この 2 つのメソッドをいずれも使用できません。使用できるのは、iterate メソッドです。

リスト 12. iterate を使用した逆順の繰り返し処理
 IntStream.iterate(7, e -> e - 1)
      .limit(7)

ラムダ式を引数として iterate メソッドに渡すと、指定の値が減少されて、繰り返し処理が逆順で進められます。逆順の繰り返し処理の間に確認したい値の総数を指定するために、ここでは limit 関数を使用しています。必要に応じて、takeWhile および dropWhile メソッドを使用して繰り返し処理のフローを動的に変更することもできます。

まとめ

従来の for ループは非常に強力な機能ではあるものの、あまりにも複雑です。Java 8 と Java 9 の新しいメソッドは、高度な繰り返し処理であっても、そのためのコードを単純化するために利用できます。rangeiterate、および limit メソッドには可変の構成要素が少ないことから、コードをより効率的に作成できるようになります。内部クラスからローカル変数にアクセスするには変数を final として宣言しなければならないという、Java の長年にわたる要件も、これらのメソッドによって解決されます。意味的には少々の違いがありますが、実質的 final パラメーターで単一の可変のインデックスを置き換えれば、ガーベッジ変数が大幅に減り、最終的に、より単純で、より簡潔なコードにすることができます。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=1047314
ArticleTitle=Java 8 のイディオム: 従来の for ループに代わる関数型の手法
publish-date=07062017