watermint.org - Takayuki Okazaki's note

Javaパズラーの問題ができるまで

以前日経ソフトウエア2013年9月号〜2015年3月号にて至極のJavaクイズという連載を、持ち回りにて執筆していました。この連載は2ページの分量に、十数行程度のコードを示し、実行結果を4択程度の選択肢、掛け合いの形式で問題を補足し、正解を示し、問題から得られる教訓までを説明するというもので、もとはJoshua BlockらによるJavaOneの人気セッションJava Puzzlersを参考にしています。ほかにはJava Day Tokyoなどのイベントでも過去に同様のセッションを行なっていました。このJavaパズラーの問題ができるまでを数年ぶりに振り返ってみます。

問題ができるまで

良問といえるのは、問題を正解できなかったとしても、なるほどと言える教訓が得られるものです。そのような問題は間違えたとしてもスッキリと気持ちよく教訓として読み進めることができます。実際、本家Java Puzzlersにはそのような良問がたくさん掲載されています。

逆に良問と言いづらいのは重箱の隅をつついたような違いを問題として表現したものです。そのような問題は、教訓が得られるというよりは「ふーん、そうなの。。」ぐらいの感想しか得られないものです。

また、知識だけを問う問題もあまり良問とは言えないでしょう。たとえばjavax.timeのようなパッケージは問題を作りやすいのですが、インタフェースやクラス、メソッドもかなり多いので知識を問う問題になりがちです。出題する際には解説などで工夫が必要になります。

出題範囲について、あまり一般的でないライブラリの仕様などを出題するのはためらわれます。たとえばJava SE標準ライブラリで、日常よく利用するであろう基本ライブラリです。ラムダ式など比較的新しい言語仕様も良いでしょう。 パッケージで言えばjava.langjava.utilあたりで出題できると良問の期待が高まります。 java.iojava.niojava.sqlなども範囲として良いですが直接これらのパッケージを利用するよりはフレームワークなどに処理を任せることが多いでしょう。出題する場合には多めに注釈を入れるなどで調整を進めます。

個人的には問題を考えるには教訓から考えていきます。Java言語仕様はもちろんのこと、新しいJava SEバージョンで変更された振る舞いや、FindBugsCheckstyleなどのルールもよく参考にしていました。

nullの扱いやequalsの振る舞い、評価順などにまつわる問題は作りやすい印象があります。ただ、それらにまつわる問題はすでに出題済みであったり本家Java Puzzlersでだいたいカバーされているので新規性のあるテーマを探すのはなかなか骨の折れる作業です。

選択肢ができるまで

回答は4択程度のなかから選ぶことになっています。必ず4択でなければならない訳ではありませんが、2択だと大抵の場合回答があからさますぎるので、4択程度になるよう調整します。 たとえばequals==の違いについて出題しようとした場合、ぱっと思いつく選択肢のパターンは次のようなものです。

  1. equals==が同じ振る舞いになる
  2. equals==が異なる振る舞いになる
  3. NullPointerExceptionが発生する
  4. その他

選択肢を考える上で、4つ目が「その他」となるのはできれば避けたいものです。ボツとなりやすいのは「その他」で全く別の例外が発生するようなケースです。equals==の違いから教訓を説明したいのに、別の例外が発生するというようでは出題意図がよくわからなくなってしまいます。時間の都合で妥協することもありますが、なるべくこだわりたい部分です。

どうしても詰まってしまった場合は、あえて出題意図を変えて選択肢をすべて例外発生にしてみるというのも面白いでしょう。たとえば下記のような選択肢ではどうでしょうか。

  1. NullPointerExceptionが発生する
  2. StackOverflowErrorが発生する
  3. ConcurrentModificationExceptionが発生する
  4. IndexOutOfBoundsExceptionが発生する

IndexOutOfBoundsExceptionConcurrentModificationExceptionがあるのでjava.util.Listなどに関連する問題と教訓にすれば面白そうです。StackOverflowErrorと迷うように再帰処理をいれてみるなどいろいろ試せそうです。

このように最初equals==の違いを出題しようと考えていたものが、別の教訓を説明するような問題にどんどん変わっていくこともあります。問題のコードはequals==の違いを説明しようとしてそうなのに、実はjava.util.Listの振る舞いについての教訓だった。となれば読みがいがありそうです。

問題コードができるまで

問題コードができるまでにも紆余曲折があります。日経ソフトウエアのように紙面であれば40行程度あっても入ることがあるのですが、やはりあまり長いとテンポが悪くなるのでなるべく短く済ませたいものです。セミナーなどでプレゼンテーションとして出題する際にはスライドに選択肢まで入れた上で一画面に収まり、会場後ろの方にも見えるようフォントサイズはできるだけ大きくする必要があります。スライドテンプレートやレイアウトによりますが15行程度までがおおよその限度となります。

このためあまり冗長な初期化処理はできませんし、教訓のコアとなる出題部分以外に必要となるクラス・メソッド定義については最低限に抑える必要があります。 読みやすさを考慮しある程度改行も入れていく必要があり、インデントも不自然にならないよう工夫する必要があります。Java言語仕様などの制限を考えると実際に使えるのは20行程度と考える必要があります。

どれだけ削っても収まりきらない場合は残念ながらボツ案となります。よりテーマを絞り込むなど教訓を考えるところまで手戻りとなります。

掛け合い部分ができるまで

イベントなどでプレゼンテーションする場合は2人のプレゼンターがあらかじめ出題者と模擬回答者として役割をきめておきます。問題がJava SEバージョンに依存する場合や新仕様などをテーマにしている場合、模擬回答者がうまく出題者に質問するような格好で補足していきます。

追加で模擬回答者は「これはひっかけですね」などと、いくつかのトリックを説明しつつ、あまり教訓の核心に触れないように打ち合わせておきます。理想的にはこのように掛け合いで説明できるようなトリックが教訓以外でも含まれていたほうが良いのですが、あまりいろいろ高望みすると締め切りがどんどん厳しくなります。。

教訓ができるまで

問題と選択肢ができあがえれば作業のほとんどは完成です。あとは教訓をわかりやすく説明するのですが、そうは言っても紙面に収まるようわかりやすく手短に伝えようとするとなかなか難しいものです。

タイトルができるまで

タイトルはなるべく教訓や選択肢の特徴を表現しつつも、ヒントになりすぎないよう微妙な調整が必要です。教訓を知った上でニヤリとできるようなタイトルとなれば大成功といっていいと思います。

問題を作ってみる

数年ぶりに問題を作ってみました。

オーはオリンピックのオー

import java.util.List;
import java.util.stream.Stream;

class City {
  private List<String> futureOlympics =
        List.of("Tokyo", "Paris", "Los Angeles");
  private String city;
  City(String name) { this.city = name; }
  boolean isFuture() { 
    return futureOlympics.contains(city.trim()); 
  }
}
public class Olympics {
  static long countO(String city) {
    return city.chars().filter(c -> c == 'o').count();
  }
  public static void main(String... args) {
    System.out.println(
      Stream.of("London", "Rio", "Tokyo", "Paris", "Los Angeles")
        .map(City::new)
        .filter(City::isFuture)
        .map(c -> c + " Olympics")
        .map(Olympics::countO)
        .reduce(0L, (sum, count) -> sum + count));
  }
}

プログラムを実行すると何が出力されるでしょうか。

  1. 「0」
  2. 「2」
  3. 「3」
  4. 「6」

.

.

.

.

.

.

.

.

.

.

.

解答

答えは「1. 「0」」です。”London”や”Rio”といった文字列を順番にCityクラスのインスタンスにマッピングし、isFutureで将来のオリンピックだけに限定したうえで、残ったオリンピック開催都市の名前から o (小文字のオー)が何文字あるか合計を計算するプログラムです。

isFuturecountOにはあまり不審なところはありませんが、いかにも怪しいのは次の行です。

                .map(c -> c + " Olympics")

Cityクラスのインスタンスに文字列を連結しています。オートキャストによりCity#toStringが呼び出され、その後に” Olympics”が連結されます。CityクラスはtoStringをオーバーライドしていないためObject#toStringが呼び出されCity@2ed94a8bのようなクラス名とハッシュ値を使った文字列表現になります。

このCity@2ed94a8bにはo (小文字のオー)は含まれませんから合計値は0となるというわけです。

教訓

toStringの実装は忘れずに。

.

.

.

問題のレビュー

さてではこのパズラー問題をレビューしてみましょう。先ほど怪しいとご紹介したこの行。

                .map(c -> c + " Olympics")

処理の文脈上、isFutureなどまでの処理は自然ですが、oをカウントする処理の前に” Olympics”と追加するというのはかなり不自然です。できればこの不自然さをうまく消したいところです。また、いくつかStream APIを利用しているのにあまり特徴を活かせていないにのも改善したいところです。

選択肢についても同様でうまく迷いを誘導できていないように見えます。「2.「2」」はTokyoだけがカウントされる、あるいはTokyoとLos Angelsで1ずつカウントされればそうなりますがちょっと考えにくい選択肢です。 「3. 「3」」はTokyoとLos Angelsでどちらもoがカウントされたと考えるなら選択される可能性があります。 「4. 「6」」はすべての都市名についてoがカウントされたと考えるなら選択される可能性があります(isFutureがうまく働かなかった)。ただちょっとそれも強引でしょう。

まとめ

このように、教訓を作る → 選択肢を作る → コードを作る → レビューというサイクルを何度も繰り返すことでより良い問題が作成できます。 ただ、本当にちゃんとした教訓となるような良問を作るのはなかなか難しいものです。(特に締め切りがある場合には。。)