2015/03/06

JJUG ナイトセミナ 「至極の Java Quiz 傑作選」 続き

このエントリーをはてなブックマークに追加

ナイトセミナの後、寺田さんにパズルの解説を blog で書かないのと聞いたら、Java Day Tokyo の準備でそれどころではないというので、代わりに寺田さんのパズルの解説をしようと思います。

資料はこちら。

 

寺田さんの問題は 2, 5, 8 問目です。

 

#2 TriTrial

問題はこちら。

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

public class TriTrial {
    public static void main(String... args) {
        List<Integer> fifthElement = Stream.of(1, 2, 3, 4, 5)
                                           .collect(Collectors.toList());
        
        IntStream.rangeClosed(1, 5)
                 .filter(n -> n % 3 == 0)
                 .forEach(n -> fifthElement.remove(n));
        
        fifthElement.forEach(System.out::print);
    }
}

選択肢は

  1. 1235
  2. 1345
  3. 1245
  4. それ以外

この問題では、[1, 2, 3, 4, 5] を要素に持つリストがあります。それに対し、3 で割り切れる要素を削除しようというものです。順当に考えれば、3 の 1245 になると思うわけですが、答えは 1 の 1235 です。

どうして、そうなってしまったのでしょう。

Stream API が散りばめられているので、なんとなくそこら辺がもんだいなのかと思われるかもしれませんが、違います。

とりあえず、はじめからコードをおってみましょう。

main メソッドのはじめの Stream.of メソッドは可変長引数のメソッドで、引数を要素に持つ Stream オブジェクトを生成するメソッドです。of メソッドの引数の型はジェネリクスの型パラメータで指定されるので、オートボクシングが働いて Stream<Intger> オブジェクトが生成されます。

次の collect メソッドはストリームに対する集約処理を行うメソッドですが、実際に行う集約処理は引数に指定する Collectors クラスのメソッドに依存します。ここでは toList メソッドを使用しているので、ストリームをリストに変換する処理を行います。

さて、次の IntStream クラスの rangeClosed メソッドは、引数で開始から終わりまでを指定して IntStream オブジェクトを生成するファクトリメソッドになります。同じようなメソッドに range メソッドがありますが、こちらは第 2 引数は含まないストリームを生成し、rangeClosed メソッドは第 2 引数を含むという違いがあります。

このため、生成した IntStream オブジェクトは、要素として [1, 2, 3, 4, 5] を保持します。

次の filter メソッドは引数のラムダ式の返り値が true の要素だけを残すフィルタリングのメソッドです。ここでは、3 で割ったあまりが 0 のものだけ取り出すということなので、結果として [3] だけを要素に持つ IntStream オブジェクトとなります。

問題は次の forEach メソッドに指定したラムダ式です。ここでは、fifithElement オブジェクトの remove メソッドをコールしています。

remove メソッドの引数は 3 なので、fifthElement オブジェクトの 3 が削除されてと考えるかもしれません。ところが、ここに落とし穴があるのです。

List インタフェースの remove メソッドは、2 つのオーバーロードがあります。

  • E remove(int index)
  • boolean remove(Object o)

なんとなく、オブジェクトを引数にとる remove がコールされるような気がしますが、実際には IntStream なのでラムダ式の引数の n は int 型であり、remove(int index) がコールされるのです。

インデックスだとすると、0 から始まるので要素の 4 が削除されます。

これを修正するには、明示的に n を int 型から Integer クラスに変換する必要があります。そのためには、n をキャストするか、IntStream オブジェクトを Stream<Integer> に変換する boxed メソッドを使用します。

boxed メソッドを使用すると、次のようになります。

        IntStream.rangeClosed(1, 5)
                 .filter(n -> n % 3 == 0)
                 .boxed()
                 .forEach(n -> fifthElement.remove(n));

この問題の教訓はメソッドのオーバーロードに気をつけることと、オートボクシングに気をつけるということがあります。

寺田さんの教訓は最もなのですが、櫻庭的にはもう 1 つ気をつけなくてはいけないところがあると思っています。

それはどこかというと、Stream API での副作用問題です。

Stream API を使用する時には、なるべく外部の変数にはアクセスしないようにすべきです。参照するだけであればまだしも、変数の状態を変更するようなことはなるべく避けるべきだと思います。

forEach メソッドは、Stream API の中では副作用を起こしてもしかたないメソッドの 1 つなのですが、それでもこのパズルのように、外部のリストの状態を変更するというのはちょっとやり過ぎのような気がします。

それだったら、次のように記述すればいいと思います。

public class TriTrial {
    public static void main(String... args) {
        List<Integer> fifthElement = Stream.of(1, 2, 3, 4, 5)
                                           .filter(n -> n % 3 != 0)
                                           .collect(Collectors.toList());
        
        fifthElement.forEach(System.out::print);
    }
}

この方が分かりやすいですよね。

 

さて、BGM です。

前回ちょっと書いたのですが、 Peter, Paul and Mary の All My Trial を使いました。問題では試行の意味の Trial なのですが、試練の意味の Trial をかけています。

櫻庭が PPM を聞き始めたのは、彼らのピークだった 60 年代とはかけ離れているわけです。

聞き始めたころは、60 年代でアコギ主体のアーティストと、どうしても Bob Dylan や Simon & Garfunkel などになってしまうわけです。PPM はちょっとまじめすぎて、なんとなく敬遠していた感もあります。

ほんとに PPM がいいなぁと感じられたのは、おとなになってからでしたね。コーラスはバツグンにキレイだし、3 人のテクニックがすごい。なんでこんなにコロコロとメインパートを入れ替わりながら歌えるのかほんと不思議です。ギターもむちゃくちゃうまいし。

PPM はキリスト教に関係する歌をよく歌っているのですが、この All My Trial もそのうちの一曲。この曲もほんとキレイな曲です。

 

#5 Kana Catalog

5 問目は Kana Catalog です。

問題はこちら。

import java.util.stream.IntStream;

public class KanaCatalog {
    public static void main(String... args) {
        IntStream.range('あ', 'わ')
                 .mapToObj(n -> (char)n)
                 .forEach(System.out::write);
    }
}

選択肢は

  1. あ ~ わ の表示
  2. あ ~ ゎ の表示
  3. 何も表示されない
  4. 文字化け発生

答えは 3. の 何も表示されない です。

これも Stream API の問題のように見えますが、違います。

ここでも、IntStream クラスの range メソッドを使用しています。'あ' から 'わ' までを作成しているわけですが、char 型のストリームはないので、int 型に暗黙の型変換が行われています。

次の mapToObj メソッドはプリミティブに対応した IntStream オブジェクトから Stream<T> に変換するメソッドです。ここではラムダ式の引数の int 型の変数を char 型にキャストしています。ここでは明示されていませんが、オートボクシングによって Character クラスに変換されます。

そして、最後の forEach メソッドで標準出力に出力されるわけです。

引っかかるとしたら、range メソッドが第 2 引数を含めないので、"わ" になるか "ゎ" になるかというところぐらいですね。

でも、そこではないのです。

どこかというと、write メソッドにあります。普通、こういうところでは write メソッドではなく、print メソッドか println メソッドを使いますよね。なぜ、print メソッドか println メソッドを使用するかご存じですか。

それは、print/println メソッドが最後にバッファをフラッシュするからです。ストリームやライタは通常バッファを持っており、バッファに対して書き込みを行い、ある程度まとまってから本来の出力先に対して出力します。この処理のことをフラッシュ (flush) と呼びます (トイレを流すことも flush ですね)。

write メソッドでも、条件が合致すれば flush するのですが、ここでコールされる write(int b) は改行文字、もしくはオートフラッシュモードにないかぎりフラッシュをしません。

これに対し、print/println メソッドでは毎回フラッシュを行うという違いがあります。

この問題の教訓は

  • 慣れ親しんだ API でも、必ず Javadoc をチェックしよう

ということです。

 

さて、BGM です。

とりあえず、 Catalog を扱った曲を探してみたのですが、手持ちの CD にはない ><

しかたないので、Catalog といえば Book だろうということで、Miles Davis Quintet の I Could Write a Book です。

この曲は Miles の Relaxin' に入っています。Relaxin' を含めた Streamin', Workin', Cookin' の 4 枚のアルバムはマラソンアルバムと呼ばれて、2 日間でアルバム 4 枚分を録音してしまったというアルバムなのです。レーベルを移籍に伴い、こういうことになったわけですが、この 4 枚のアルバムはほんとにカッコいい。Miles のベストといっても過言ではないと思います。

この頃の Miles バンドのメンバはサックスが John Coltrane、ピアノが Red Garland、ベースが Paul Chambers、ドラムが Philly Joe Jones です。ほんとすごいメンバなわけです。

コロコロと転がるような Garland のピアノで始まるこの曲も、Miles のミュートが冴え渡っています。Miles はどちらかというと暗いペットを吹くのですが、この曲のようにアップテンポで陽気な曲もいいものです。

 

#8 Superstar

8 問目は時間がなくて、セミナではできなかったのですが、Superstar という問題です。

import java.time.Year;

public class Superstar {
    public static final Superstar superstar = new Superstar();
    
    private static final int BIRTH_YEAR = Year.of(-4).getValue();
    private static final int THIS_YEAR = Year.now().getValue();
    
    private final int age;
    
    private Superstar(){
        age = THIS_YEAR - BIRTH_YEAR;
    }
    
    public int getAge() {
        return age;
    }
    
    public static void main(String... args) {
        int age = superstar.getAge();
        
        System.out.println("Superstar: " + age + " years old.");
    }
}

選択肢は次の通り。

  1. Superstar: 2019 years old.
  2. Superstar: 2015 years old.
  3. 例外発生
  4. それ以外

答えは 4 のそれ以外です。実際に出力されるのは "Superstar: 0 years old." になります。なんでだか全然分からないですよね。

この問題は、JVM が起動する時の、手順を知らないとちょっと難しいかもしれません。

JVM が起動して、main メソッドを実行するまでの手順は 4 つの段階に分かれます。

  1. JVM 起動
  2. クラスロード
  3. リンク
  4. 初期化

JVM の起動はその名の通り、JVM を起動するわけです。そして、必要とするクラスをロードするクラスロードを行います。

この時点ではロードしただけで、クラスを使えるところまではいきません。

その後、リンクを行います。リンクではベリフィケーション、準備、解決の 3 つの処理を行います。

ベリフィケーションはクラスファイルが正しいかどうかをチェックする処理です。

そして、準備で static フィールドの初期化を行います。ただし、クラス間の関係は次の解決で決められるので、クラスのメソッドをコールすることはできません。したがって、オブジェクトであれば null が代入されます。これは final な変数であったとしても同じです。

同様に数値を扱うプリミティブ型では 0 になります。

次の解決でクラス間の関係を構築したり、定数プールの解決を行います。

ここでやっと Java のコードを実行する準備が整います。

この時点で、superstar 変数は null、BIRTH_UYEAR と THIS_YEAR は 0 になっています。

そして、初期化で static フィールドの初期化と static イニシャライザが実行されます。これは記述順に行われるので、まず superstar の初期化が行われます。

superstar の右辺は new Superstar() なので、コンストラクタがコールされます。

コンストラクタでは THIS_YEAR と BIRTH_YEAR の引き算が行われるのですが、この時点でもまだ THIS_YEAR と BIRTH_YEAR の値は 0 なのです。

つまり、age は 0 になります。

superstar が初期化された後、BIRTH_YEAR、THIS_YEAR の順に初期化されます。

しかし、age が更新されることはなく、その値は 0 のままです。

このため、main メソッドで superstar.getAge() が返す値は 0 になってしまうわけです。

これを解消するには、static フィールドの記述順序を変更して、superstar を BIRHT_YEAR、THIS_YEAR の後に書けば OK。

でも、こんなことが起こるなんて、気がつかないですよね。だからこそ、気をつけて欲しいわけです。

 

さて、この問題の BGM ですが、ゴスペルの名曲、John The Revelator を使いました。

問題を見れば分かると思うのですが、Superstar といっているのはあの人のことです。

ほんとはミュージカルの Jesus Christ Superstar の曲を使いたかったのですが、さすがに CD を持ってなかったのでした。

キリスト教に関する曲だけはいっぱいあるので、その中のどれかにしようと思ったわけです。で、John The Revelator。

この曲はいろいろな人が歌っていますが、ここで使うつもりだったのは映画 Blues Brothers 2000 の Sam Moore のバージョン。Sam Moore といえば、Sam & Dave の Sam。

Sam Moore といえば高音でのシャウト。この曲でもそれが存分に発揮されてます。映画の中では、Blues Brothers の最後のメンバがこの曲で解脱 (?) してしまうわけです。


この Java Puzzlers の資料がとうとう 1 万 View に達してしまいました。こんな短い期間で 1 万に達したのは初めてです。

それだけ、注目されたということなのでしょう。

そうすると、やっぱり次回もということを考えてもいいかもしれません。まぁ、そんなにすぐにやるわけではないですが、頃合いを見て、考えようと思います。

0 件のコメント: