torutkのブログ

ソフトウェア・エンジニアのブログ

Java読書会BOF「Effective Java第3版」を読む会(第5回)を開催して

Java読書会開催のデータ

昨日4月13日(土)は、Java読書会BOF主催の「Effective Java第3版」を読む会(第5回)を開催しました。

1998年12月にJava読書会が始まってから通算241回目、36冊目の書籍となります。
Effective Javaについては、2002年に第1版を、2008年に第2版をJava読書会で読んでいます。

今回の読書範囲からのメモ

オーバーロード

同じパラメータ数の二つのオーバーロードされたメソッドを提供しないこと

これに尽きますね。オートボクシングがあるので、引数の型がObjectとintでオーバーロードしていると落とし穴に陥りがちです。既存APIにあるので注意が必要です。

可変長引数

引数の個数が0個のことも考慮して実装する必要があります。引数を最低1個必須とする場合は、必須の引数と可変長引数と2つの引数を要求するようメソッドを定義します。

nullをメソッドの戻り値として返さない

nullを返すメソッドは呼び出し側にnullに対応する余分なコードが必要となる上、利用者がそのコードを書き忘れてプログラムのエラーを招く原因となります。

配列またはコレクションを戻り値型とする場合は、nullではなく空コレクションか空配列を返すようにします。

Java SE 8から導入されたOptionalを戻り値型とすると、メソッドが値を返さなかったときにデフォルト値を使う(orElse)、例外をスローする(orElseThrow)、別な処理を呼び出す(orElseGet)を簡潔に実装できます。isPresentはOptionalが提供する他のメソッドでは対応できないときの「安全弁」として使うという解説があり、なるほど~と思いました。

Optionalの使い方は読書会で議論となり、Serializableではないのでフィールドには使うべきではないねとなりました。

ドキュメントコメント

いわゆるJavadocコメントですが、Java SE 8以降も進化していました。

  • @implSpec
  • @index
  • @summary
  • 検査有効(-Xdoclintがデフォルトで有効)
  • HTML5を生成させる-html5オプション

@throwsに、可能性のある非チェック例外を書くのはよいが、メソッド内で外部ライブラリを呼び出しているとすべての非チェック例外を把握しているわけではないのでどうしよう?と議論に。

ライブラリを知り、ライブラリを使う

車輪を再発明しないでください

で、どのようにライブラリを探して使うかについて議論となりました。

  • 検索スキルが必要
  • こういうライブラリがあるはず(必要)、という意識がないとライブラリを探せない
  • ライブラリの更新頻度、ダウンロード数を参考にする
  • 大きなライブラリは避ける
  • 複数のライブラリを使うと、それらのライブラリがたまたま同じライブりに依存するがバージョンが違うときにはまる
  • google検索エンジンでトップに出てくる、内容が薄いQiita記事は何だろうね
  • 類似ライブラリを横並べて比較する、APIの違いを見る
Randomクラスについて

Java 7の時点で、Randomをもはや使うべきではありません。今日ではほとんどの場合、選択すべき乱数生成器はThreadLocalRandomです。それは高品質な乱数を生成し、きわめて速いです。

InputStreamにtransferToメソッドが追加

InputStreamに、long transferTo(OutputStream out) メソッドが追加され、入力ストリームをそのまま出力ストリームに流し込むことが簡単にできるようになりました。

    public static void main(String[] args) throws IOException {
        try (InputStream in = new URL(args[0]).openStream()) {
            in.transferTo(System.out);
        }
    }

おおー、これは便利。java.io.Readerとjava.nio.channels.FileChannelにもtransferToメソッドが追加されています。

toArrayのパフォーマンス

Effective Javaで次の記事が参照されていました。
Arrays of Wisdom of the Ancients

これは、ListインタフェースのtoArrayメソッドを呼ぶ際に指定する引数の値を、長さ0の配列を生成するか、Listの要素数に相当するサイズの配列を生成するかでどちらが速いかを調査した内容です。

List<Cheese> cheeseInStock = ...
return cheeseInStock.toArray(new Cheese[0]);  // (1)
return cheeseInStock.toArray(new Cheese[cheeseInStock.size()]); // (2)

Effective Javaでは、参照記事に基づいて(1)を用い、(2)はパフォーマンスに悪影響があるので使わないことを推奨しています。

(1)はコレクションcheeseInStockの要素数が1以上のときは新しい配列を生成します。(2)は渡した配列が使われ、新しい配列の生成はありません。そこで、(2)の方が性能がよいように見えます。

参照記事ではパフォーマンスを計測した結果、(1)の方がArrayListでは顕著に早く、HashSetではほぼ同じとのデータが得られ、それを考察しています。パフォーマンス計測にはJMHを使っているのでそこは信頼に足ります。

記事をざっと読んだところ次のとおり

  • 配列の生成にかかるコストよりも要素を配列に詰めるコストが支配的
  • ArrayListの場合は、内部の配列の要素をtoArrayで返却する配列にSystem.arraycopyでコピー
  • (1)ではネイティブコードのcheckcast_arraycopy_uninit関数が実行されている
  • (2)ではネイティブコードのcheckcast_arraycopy関数が実行されている。(1)のコードにはないrepz stosb命令(memset to zero)に時間を要している

ちなみに、引数なしのtoArray()では、AVX命令を使ったさらに高速なコピーが行われています。