ユーザーの手動入力をループ処理する場合に使用するループ構造、そこには多くの微妙な違いが…

著者:Simon Roberts、Mikalai Zaikin

2019年8月26日

過去にこのクイズの設問に挑戦したことがある方なら、どれ1つとして簡単な問題はないことをご存じでしょう。クイズの設問は、認定試験の中でも難しめのものに合わせています。「中級者向け」「上級者向け」というレベルは、設問の難易度ではなく、対応する試験による分類です。しかし、ほとんどすべての場合において、「上級者向け」の方が難しくなります。設問は認定試験対策として作成しており、認定試験と同じルールの適用を意図しています。文章は文字どおりに解釈してください。回答者を引っかけようとする設問ではなく、率直に言語の詳細な知識を試すものだと考えてください。

ループ構造を比較したいと思います。

以下のことを行うインタラクティブなコンソール・アプリケーションを書いているものとします。

  1. プロンプトを表示する。
  2. 入力としてコマンドを読み取る。
  3. 入力がQだった場合、終了する。そうでない場合は、指定されたコマンドを実行し、手順1に戻る。
 

次のコード構造のうち、このシナリオを実装するために最適な選択肢はどれですか。1つ選んでください。

  1. break文があるforループ
  2. 拡張forループ
  3. continue文があるwhileループ
  4. do/whileループ
 

解答:この設問では価値判断を求めています。多くの場合、こういった選択問題は悩ましいものです。しかし、この設問で求められているのはかなり一般的な判断です。すべての説明が終わるころには、皆さんにもそう思っていただけるはずです。

設問から2つの重要なポイントが読み取れます。この2つが、適切なループを選ぶ際の手がかりになります。1つ目は、手順1と2を少なくとも1回実行する必要があることです。つまり、少なくとも1回コマンドを読み取るまで、コードは終了しないはずであるということです。そのこととも密接に関連していますが、もう1つわかることは、コマンドを読み取ってからでなければ、プログラムを終了する判断はできないということです。

whileループを使おうとする場合、以上の要件が実装にどう影響するかを考えてみます。まずわかるのが、whileループの条件判定はループに入るときに行われるということです。つまり、条件判定はループ本体より先に実行されます。そのため、このループを使ってプログラムの実行と終了を制御する場合の1つの可能性としては、次の疑似コードに示すように、ループの開始前にプロンプトを表示してコマンドを読み取ることが考えられます。 

プロンプトを表示する
コマンドを読み取る
While (コマンドが"Q"でない)
  コマンドを実行する
  ...

現時点では、このアプローチに問題はないように思えるかもしれません。しかし、さらに、次のプロンプトを表示して次のコマンドを読み取る必要があります。この処理はループごとに行う必要があるため、関連するコードをループの中に入れる必要があります。すなわち、同じコードを2か所に書かなければなりません。この条件は、コードに重複する部分が必要だと間接的に述べているのと同じことです。このコードは次のようになります。

プロンプトを表示する
コマンドを読み取る
While (コマンドが"Q"でない)
  コマンドを実行する
  プロンプトを表示する // 重複
  コマンドを読み取る // 重複
End-while

コードの重複は美しくなく、メンテナンスの際にエラーが起きやすくなります。別のアプローチとして、ループの前にダミーの値をコマンドに設定し、そのコマンドを実行しても何も行わないようにする方法も考えられます。こうすれば重複は回避されますが、実質的に動作を「ハッキング」する特殊な値を使っているため、やはりコードが複雑になり、理解しにくくなる可能性があります。

この問題は、whileループの使用にとって確かに打撃と考えられますが、価値判断を行う前に、他の可能性について考えてみます。

次は、標準forループについて見てみます。実は、forループは、whileループに飾り付けをしただけのものにすぎません。その飾りによって、ループ構造でよくある2つの状況に対処します。1つはループ内で使われる変数の初期化(同時に宣言が行われることもあります)、もう1つは反復に伴う、変数の更新です。しかし、forループの実体は、飾りが付いたwhileループにすぎません。その飾りのいずれかが今回のシナリオに役立つ可能性もありますが、プロンプトと入力読取りのコードが重複するという根本的な問題があることは変わりません。

次に、拡張forループについて見てみます。拡張forループは、Iterableオブジェクトのすべての要素を処理する際に、よりクリーンな手段を提供します(対象となるIterableは、Listなどのコレクションによって提供されるのが一般的ですが、常にそうであるとは限りません)。この問題では、コンソールから入力として項目を読み取る必要があり、入力値がQであればループは終了します。それに対して、拡張forループでは、Iterableから項目を取得し、利用できる項目がなくなったときに終了します。 ユーザーにプロンプトを表示して1行のテキストを読み取り、ユーザーの入力がQでなかった場合にStringを返し、ユーザーの入力がQだった場合には反復を終了するIterableオブジェクトを作ることはできますが、今回のシナリオで使うにはあまりに遠回りで複雑すぎます。この状況で拡張forループが推奨される理由が多いとは考えられないため、最後の選択肢について考えてみます。

残る選択肢は、do/whileループです。このループを繰り返すかどうかを決める条件判定は、do/while構造の最後に置かれます。これによって、いくつかの結果が生まれます。do/whileループは、常に少なくとも1回は実行されます。ループの条件判定に到達するためには、その本体を実行しなければならないからです。別の言い方をすれば、条件判定が行われるにループの本体が実行されるということです。今回のシナリオには、こちらの方がはるかに適しています。ループ本体でプロンプトを表示し、コマンド入力を読み取ることができるからです。その結果、コードを重複させることなく、条件判定の前にプロンプトの表示と入力の読取りが行われることが保証され、クリーンでシンプルな構造が実現します。

この時点で、今回のシナリオに最適なのは選択肢Dであり、他の選択肢よりもはるかに優れていることは明らかでしょう。そのため、選択肢Dが正解で、A、B、Cは誤りであることがわかります。

いくつか補足しておきます。選択肢Aにはbreak文を使うとあり、選択肢Cにはcontinueを使うとあります。forbreakの構造を代わりに使用することもできますが、いずれにしても、エレガントな解決策にはなりません。選択肢Aは、次のようにコーディングすることもできるでしょう。

for (;;) { // 事実上の無限ループ
    // プロンプトを表示する
    String input = // ユーザーの入力を読み取る
    if (input.equals("Q"))
        break; // ループの外に出る
    executeCommand(input);
}

しかし、do/whileループを使用したものに比べれば、やはり美しくありません。同じように、whileループでcontinueを使っても、クリーンな解決策に直結することはありません。

正解は選択肢Dです。

 

Java Magazine December 2019の他の記事

プロパティベース・テストを習得する
Arquillian:簡単なJakarta EEテスト
ArchUnitでアーキテクチャの単体テストを行う
新しいJava Magazine
作ってみよう:自分だけのテキスト・エディタ(パート1)
クイズに挑戦:Collectorsの使用(上級者向け)
クイズに挑戦:スレッドとExecutor(上級者向け)
クイズに挑戦:ラッパー・クラス(中級者向け)
書評:Core Java, 11th Ed. Volumes 1 and 2

Simon Roberts

Simon Roberts:Sun Microsystemsがイギリスで初めてJavaの研修を行う少し前にSunに入社し、Sun認定Javaプログラマー試験とSun認定Java開発者試験の作成に携わる。複数のJava認定ガイドを執筆し、現在はフリーランスでPearson InformITにおいて録画やライブによるビデオ・トレーニングを行っている(直接またはO’Reilly Safari Books Onlineサービス経由で視聴可能)。OracleのJava認定プロジェクトにも継続的に関わっている。

 

Mikalai Zaikin

Mikalai Zaikin:ベラルーシのミンスクを拠点とするIBA IT ParkのリードJava開発者。OracleによるJava認定試験の作成に携わるとともに、複数のJava認定教科書のテクニカル・レビューを行っている。Kathy Sierra氏とBert Bates氏による有名な学習ガイド『Sun Certified Programmer for Java』では、3版にわたってテクニカル・レビューを務めた。