「Javaエンジニア養成読本」読んだ

献本して頂きました。 私を推薦してくれたのざきひろふみさん、ありがとうございます!

この本は経験の浅いJavaエンジニアがふつうのJavaエンジニアになるために必要な技術的な知識 (と技術以外の知識) を一度に取り入れる事ができるような内容になっています。

「特集1」ではJava入門という事で誰もが通る道について解説されています。 ただしよくある入門書のように制御文やintの取りうる範囲、演算子の優先順位などをちまちま解説したりはしません。

その代わりJava言語の要であるクラスについて2章をまるごと使って解説しています。 トップレベルのクラス、インナークラス、staticなネストしたクラス、匿名クラスの違いを説明できますか? この本を読めば説明できるようになります。

また、例外の扱いかたについても実践的な視点で書かれています。 しっかり読んで e.printStackTrace() から卒業しましょう。

「特集2」ではラムダ式とStream APIについてコンパクトながらも分かりやすく解説しています。 文法や使い方だけでなくラムダ式がなぜ必要になったのかもよく分かります。

「特集3」はJava EEです。 広大なJava EEの仕様群からServlet、JSF、JAX-RS、JPA、CDI、EJBをピックアップして解説しています。 Java EEの主要技術の雰囲気を掴めるのではないでしょうか。

「特集4」ではJavaの技術ではなくチーム開発に必要な知識として Git、Maven、JUnit、Jenkinsについてそれらが必要となる背景や導入方法、 使用上の注意点などが解説されています。 もはやEclipseでWAR作ってるなどと言う現場はないと思いますが、 これら(あるいはこれらに類する他のツール)を導入していない場合はこの特集が役立つでしょう。

ページは前後しますが、「巻頭記事」ではJavaの歴史とJavaにまつわる(しょーもない)ネタを解説しています。 これを一読しておくとTwitterをより楽しめるようになりますたぶん。

そして一番後ろに記載されている「一般記事」は少々趣が変わり、 受託開発の現場で必要となるスキルが簡潔にまとめられています。 Excelにスクリーンショットを貼付けるといった (´;ω;`)ウッ… となるスキルにも触れていますが、 Java以外にもこういうスキルが必要なんだなと理解できる内容になっています。

細かい感想・気になった点など

というわけで細かい部分を挙げていきます。

  • 29ページ。

    通常の業務システムを開発する中でアノテーションを作成することは少ないので”

    CDIというものがあってだな(ry

  • 30ページ。

    インナークラスに名前付き定数を持てるの知らなかった。 勉強になりました!

  • 34ページ、リスト19。

    複数コンストラクタを用意する場合、 オプション引数は後ろの引数(ここでいうと b )にする方が一般的と思います。

  • 35ページ。

    例外オブジェクトには例外が発生した時の状態が詰め込まれてます。

    「状態」よりも「情報」の方がしっくりくるかなー。 前章で可変と不変について言及しているのも相まって分かりにくい気がしました。

  • 38、39ページ。

    InvocationTargetException は他のリフレクション例外と意味合いがちょっと違うと思っています。 他のリフレクション例外が「指定されたメソッドを実行できなかった」のに対して InvocationTargetExceptionは「指定されたメソッドを実行中に例外が発生した」という状況です。 それによってハンドリングが変わるかどうかはケースバイケースとは思いますが。 あとここはマルチキャッチについて書いてて、たまたまリフレクション使っただけなので InvocationTargetExceptionの扱いとか関係無いか。 まあ気になったということで。

    気になったと言えば例外の中では InterruptedException の扱いにも注意が必要と思うので 言及しておいたほうが良いのではと一瞬思いましたが、 後のほうでマルチスレッドはうかつに触るなって書いてあるしまあいいか。

  • 43ページ。

    Java 1.7のtry-with-resources構文のために追加された Throwable#addSuppressed(Throwable) も使えそうですね。

  • 68ページ、リスト6。69ページ。

    Files.lines() をtry-with-resourcesでクローズしてるのは流石だと思いました。 ここはハマりどころになりそうな気がしますねー。

  • 87ページ、リスト5。

    ボタンの押下時にAjaxで処理

    “押下”の現場感すごい。

  • 95、96ページ。

    JAXB経由のJSONプロバイダはJAX-RSの標準仕様ではない(ですよね?)から注意が必要ですね。

  • 98ページ、リスト3

    JPAにおけるReadとUpdate。 mergeはupdateではなくて永続コンテキスト外のエンティティを 永続コンテキストに突っ込む行為と思います。 em.find(clazz, id) したエンティティは永続コンテキストで管理されているので mergeする必要はないですよね? ……とか思っていたら100ページでmergeの説明があった。

  • 105ページ。

    モックによってテスト容易性を高めたり、

    具体例が欲しい気がしました。 CDIのalternativeはbeans.xmlを編集しなきゃいけなかったりして面倒い。 個人的にはCDIを使う場合はユニットテストにはGuiceを使うのが楽で良いと思っています。

あと、JVMへの言及が見当たらないのが気になりました。 -Xmx とか。

Java EEは、範囲が広くて仕様がでかいので仕方ないのですが、 内容が薄めだと感じてしまいました。 いっそJAX-RSやCDIなどの各仕様の説明はもっとシンプルにして、 チュートリアルを手厚くするという手もあったのかもかなー、などと無責任に思いました。

特にServletとEJBの説明はなくても良かったかも知れませんね。 すっぴんのサーブレットは使わないしEJBはトランザクション境界が欲しいってのが殆どと思いますし。 デカい案件やったことないので見当違いの意見かもしれませんが。

まとめ

というわけで、色々と細かいことも書きましたが総合的には間違い無く良書だと思っています。 冒頭にも書きましたがこの本は経験の浅いJavaエンジニアの皆様へオススメです。 この本を読んで学んで楽しいJavaライフを送りましょう!

あなたと!

(続きははてなブックマークのコメントでお願いします)

執筆陣のブログエントリ

まだ買ってないひとはキクタローさんのブログのアフィリンクから買おう!

セミコロンレスJavaで末尾再帰の最適化

前回 はセミコロンレスJavaで再帰ができる事が分かりました。

ただし再帰しすぎるとスタックオーバーフローでしにます。

再帰による 1 + 2 + ... + n を見てみましょう。

public class SemicolonlessRecursion {

    public static void main(String[] args) {
        if (java.util.stream.Stream
            .of(Integer.parseInt(args[0]))
            .flatMap(n -> java.util.stream.Stream
            .<F<Integer, Integer>> of((f, m) -> m < 1 ? 0 : m + f.apply(f, m - 1))
            .map(sum -> sum.apply(sum, n)))
            .peek(System.out::println)
            .count() > 0) {
        }
    }

    interface F<P, R> extends java.util.function.BiFunction<F<P, R>, P, R> {}
}

とっても分かりやすいコードですが n10000 程度を与えただけでスタックオーバーフローになります。

この再帰を末尾再帰にして最適化を行うのが今回の目的です。

普通のJavaで末尾再帰最適化

最初からセミコロンレスJavaで考えてもしんどいだけなので、 まずは普通のJavaで末尾再帰最適化版のコードを書いてみます。 実装するに当たって 「Javaによる関数型プログラミング」 の7章を参考にしました。

import java.util.Optional;
import java.util.stream.Stream;

public class TailCallOptimization {

    public static void main(String[] args) {
        int n = Integer.parseInt(args[0]);

        F sum = (f, p, r) -> p < 1 ? done(r) : call(() -> f.apply(f, p - 1, r + p));

        TailCall t = sum.apply(sum, n, 0);

        Integer result = Stream.iterate(t, TailCall::get)
                               .map(TailCall::result)
                               .filter(Optional::isPresent)
                               .map(Optional::get)
                               .findFirst()
                               .get();

        System.out.println(result);
    }

    interface F {

        TailCall apply(F f, Integer p, Integer r);
    }

    static TailCall call(TailCall t) {
        return t;
    }

    static TailCall done(Integer result) {
        return new TailCall() {

            @Override
            public TailCall get() {
                throw new UnsupportedOperationException();
            }

            @Override
            public Optional<Integer> result() {
                return Optional.of(result);
            }
        };
    }

    interface TailCall {

        TailCall get();

        default Optional<Integer> result() {
            return Optional.empty();
        }
    }
}

多少セミコロンレスJavaへの変換を意識していますが普通のJavaです。 これをセミコロンレスJavaにしていきます。

セミコロンレス化の布石

Java 8時代におけるセミコロンレスJavaの鍵はラムダ式だと思っています。 値を返すメソッドの定義が出来ないセミコロンレスJavaですが、 ラムダ式を使う事でセミコロンレスに関数を定義する事が可能です。

//ふたつのintを足して返す関数を定義して2, 3に適用する
if (java.util.stream.Stream
    .<java.util.function.BinaryOperator<Integer>> of((a, b) -> a + b)
    .map(add -> add.apply(2, 3))
    .peek(System.out::println)
    .count() > 0) {
}

ラムダ式を使う為に必要となるのは関数型インターフェースです。 セミコロンレスJavaではインターフェースの定義は出来ますが、その中でメソッド定義が出来ません。 ただし、幸いにもJavaの標準APIには関数型インターフェースが豊富に用意されているので それらをextendsすることで用途に特化した関数型インターフェースを手に入れる事ができます。

まず TailCall を関数型インターフェースにする事から始めましょう。 ここでの課題は get()result() の一本化です。 今のままではどうしても匿名クラスを導入する必要があります。

TailCallOptional<Integer>Pair を返す Supplier とすることで TailCall を関数型インターフェースにできました。

interface TailCall extends Supplier<Pair<TailCall, Optional<Integer>>>{}

これにより done(Integer) が返す値を匿名クラスではなくラムダ式で書けるようになりました。

static TailCall done(Integer result) {
    return () -> new Pair<>(null, Optional.of(result));
}

また call(TailCall) は次のように変更します。

static TailCall call(Supplier<TailCall> t) {
    return () -> new Pair<>(t.get(), Optional.empty());
}

こうすることで関数 sum は次のように書けます。

F sum = (f, p, r) -> p < 1 ? done(r) : call(() -> f.apply(f, p - 1, r + p));

それから結果を求める Stream 操作ですが、 普通の再帰版では TailCallget() を呼び出すことで Stream を構築していましたが get()Pair<TailCall, Optional<Integer>> を返すようにしたので、 Pair<TailCall, Optional<Integer>>Stream を構築するようにします。

Stream.iterate(new Pair<>(t, Optional.<Integer> empty()),
               p -> p.getKey().get())
      .map(Pair::getValue)
      .filter(Optional::isPresent)
      .map(Optional::get)
      .findFirst()
      .get();

ここまでのコード全体を次に記載します。

import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;

import javafx.util.Pair;

public class TailCallOptimization {

    public static void main(String[] args) {
        int n = Integer.parseInt(args[0]);

        F sum = (f, p, r) -> p < 1 ? done(r) : call(() -> f.apply(f, p - 1, r + p));

        TailCall t = sum.apply(sum, n, 0);

        Integer result = Stream.iterate(new Pair<>(t, Optional.<Integer> empty()),
                                        p -> p.getKey().get()).map(Pair::getValue)
                               .filter(Optional::isPresent)
                               .map(Optional::get)
                               .findFirst()
                               .get();

        System.out.println(result);
    }

    interface F {

        TailCall apply(F f, Integer p, Integer r);
    }

    static TailCall call(Supplier<TailCall> t) {
        return () -> new Pair<>(t.get(), Optional.empty());
    }

    static TailCall done(Integer result) {
        return () -> new Pair<>(null, Optional.of(result));
    }

    interface TailCall extends Supplier<Pair<TailCall, Optional<Integer>>> {}
}

そしてセミコロンレスへ……

あとはちょっとずつまとめたりなんやかんやしてセミコロンレスJavaに変更していきます。

というわけでセミコロンレスJavaで末尾再帰最適化を行ったコードが次になります。

public class SemicolonlessTailCallOptimization {

    public static void main(String[] args) {
        if (java.util.stream.Stream
            .of(Integer.parseInt(args[0]))
            .flatMap(n -> java.util.stream.Stream
            .<F> of((f, pr) -> pr[0] < 1
                ? () -> new javafx.util.Pair<>(null, java.util.Optional.of(pr[1]))
                : () -> new javafx.util.Pair<>(f.apply(f, new int[] { pr[0] - 1, pr[1] + pr[0] }), java.util.Optional.empty()))
            .<TailCall> map(sum -> sum.apply(sum, new int[] { n, 0 })))
            .map(t -> java.util.stream.Stream
            .iterate(new javafx.util.Pair<>(t, java.util.Optional.<Integer> empty()), p -> p.getKey().get())
            .map(javafx.util.Pair::getValue)
            .filter(java.util.Optional::isPresent)
            .map(java.util.Optional::get)
            .findFirst()
            .get())
            .peek(System.out::println)
            .count() > 0) {
        }
    }

    interface F extends java.util.function.BiFunction<F, int[], TailCall> {}

    interface TailCall extends java.util.function.Supplier<javafx.util.Pair<TailCall, java.util.Optional<Integer>>> {}
}

まとめ

セミコロンレスJavaでも末尾再帰の最適化が出来る事が分かりました。 これによりセミコロンレスJavaがまた一歩、実用的な言語へと近づいたと思われます。

なお、今回は javax.util.Pair を使用しましたが、これが大変便利でした。 特にふたつの値を返す場合に今までは配列あたりを使用していたのでキャストが必須になっていましたが、 Pair があればキャストも不要でコードがすっきりしました。 また、ふたつ以上の値を返す場合は Pair<T, Pair<U, V>> などとすれば良いですね。

というわけでこれからもセミコロンレスJavaの可能性を探って行きたいと思います。