注目イベント!
春の新人向け連載企画開催中
新人エンジニアの皆さん、2024春、私たちと一緒にキャリアアップの旅を始めませんか?
IT業界への最初の一歩を踏み出す新人エンジニアをサポートする新連載スタート!
mameyose

Effective Java 第3版を読んで

| 6 min read
Author: ryo-nakagaito ryo-nakagaitoの画像

これは豆蔵デベロッパーサイトアドベントカレンダー2023第25日目の記事です。

はじめに

#

BS第二グループの中垣内と申します。今回は、先輩社員からおすすめしていただいた書籍「Effective Java 第3版」読み、自分の現場での開発経験と照らし合わせて考えたことを寄稿させていただきます。本書に書いてあることはどれもJavaで開発を行う上でためになることばかりなのですが、今回はその中でも個人的に特にためになった・学びになったと感じたことをピックアップして紹介させていただきます。

Effective Javaについて

#

Javaプログラマにとって必読の定番書であり、第3版ではJava8から導入されたラムダとストリームに関する章が追加されたそうです。初心者向けの書籍と言うよりは、Javaの基本文法を習得している人がターゲットとなっており、より良いJavaコードを書くためのノウハウやデザインパターンが示されている本だと私は捉えています。

Effective Java 第3版 / Joshua Bloch(著), 柴田 芳樹(訳) Amazonのリンク

第2章 オブジェクトの生成と消滅

#

項目1 コンストラクタの代わりにstaticファクトリメソッドを検討する

#

staticファクトリメソッドとは、特定のクラスのインスタンスを提供するための、コンストラクタ以外の手段として用いられるものであり、以下のようなメリットがあることを学びました。

  • プログラマにとって分かりやすい名前を付けることができる。

コンストラクタの場合メソッド名はクラス名に限定されてしまいますが、以下のような分かりやすい名称をつけることができます。

fromofvalueOfgetInstancecreatenewInstance

  • 新たなオブジェクトを生成する必要がなく、同じオブジェクトを使いまわせる。

ここがまさに「オブジェクトの生成を管理できる」という点で最も大きなメリットであると感じました。

ここまでの2項を読み、「IntegerクラスのvalueOfメソッドがこれに該当する?」と思い調べてみたところ、Integerクラスでは内部的に-128~127の範囲のインスタンスをキャッシュして保持しており(頻繁に使われる数値だから)、この範囲内のint型のパラメータを受け取った場合、新たにインスタンスを生成するのではなく、キャッシュの中から該当するインスタンスを返すのだということを知りました。Boolean.valueOfなども同様の考え方ですね。

標準ライブラリでは不必要に重複したインスタンスを生成することを避け、使いまわせるものは使いまわすように上手く作られており、今後自分がクラスを設計する際も、別々にインスタンスを生成する必要がない / インスタンスの論理的な意味合いが同じになるような場合にstaticファクトリメソッドの実装を検討しようと思いました。

  • コンストラクタと異なり、戻り値の型のサブタイプのオブジェクトを返せる。
  • 入力するパラメータに応じて返却するオブジェクトの型を変えられる

例えば、渡されるパラメータに応じて、それに適したコレクションインタフェースの実装クラスのインスタンスを返却することができ、呼び出し側は戻り値の型のインタフェースの規約を把握していれば、実際に返却されるインスタンスの具体的な型は知らなくてもよく(実装を隠蔽できる)、よりオブジェクト指向的な実装ができるという点で優れているのかな、と思いました。

項目2 多くのコンストラクタパラメータに直面したときにはビルダーを検討する

#

インスタンス変数の数がたくさんあるクラスのインスタンスを生成する時、コンストラクタやstaticファクトリメソッドの場合は多くのパラメータを決められた順番通りに与えなければならず、そういった場合にビルダーパターンでの実装が役に立つことを学びました。

本書に記載されていた呼び出し側のコードを以下に示します。

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
    calories(100).sodium(35).carbohydrate(27).build();

(Effective Java 第3版 14頁から引用)

最初に必須パラメータを渡し、後は必要なパラメータを流れるように設定してゆくという記述が非常に分かりやすく便利だと感じました。このような設計だと、NutritionFactsクラスに新しいフィールドを追加する必要が出てきたとしても既存ソースの修正は最小限に抑えられ、保守性も良いと感じました。業務でコンストラクタの引数の並びが煩雑な時やJavaBeansパターンでSetterを使用して多くのフィールドを設定しなければならずソースが汚くなることが多々あったので、このビルダーパターンを活かしたいと思いました。

また本書には記載されていないことですが、Lombok@Builderアノテーションを利用すればこのビルダーパターンを楽に可読性良く実装できることも調べて分かりました。積極的に活用したいです。

第7章 ラムダとストリーム

#

第3版から追加された章ですね。私は豆蔵に入社してから初めてまともにStream APIを使ったコーディングをできるようになったのですが「なんとなく」で書いていた部分も多く、この章によって改めて体系的に学ぶことができました。

項目42 無名クラスよりもラムダを選ぶ

#
// 関数オブジェクトとしてのラムダ式(無名クラスを置換)
Collection.sort(words,
        (s1, s2) -> Integer.compare(s1.length(), s2.length()));

ラムダの型(Comparator)、そのパラメータの型(s1とs2はどちらもString)、その戻り値の型(int)はコードには書かれていないことに注意してください。コンパイラは文脈からこれらの型を、型推論type inference)と呼ばれる処理を用いて推論します。場合によっては、コンパイラは型を決定できず、その場合には型を明示しなければなりません。

~中略~

型を明示することでプログラムが明瞭になるのでなければ、すべてのラムダのパラメータで型を省略してください。コンパイラがラムダのパラメータを推論できない旨のエラーを表示したら、そのとき、型を明示してください。ときには、戻り値あるいはラムダ式全体をキャストしなければならないかもしれませんが、それはまれです。

(Effective Java 第3版 196頁から引用)

本項を読み、今までIDEの自動コンパイル機能を利用しながらラムダを記述しておりましたが、確かに型を省略して記述していたこと、コンパイラが型を推論してくれていたことに気づきました。型推論の具体的な仕組みはまだ理解できていない部分が多いのですが、ラムダの良さは記述の簡潔さであり、可能な限り型を省略して記述するのが良いということを学びました。

項目43 ラムダよりもメソッド参照を選ぶ

#

メソッド参照も実際の業務のソースコードで使用していましたが、理解が曖昧な部分があり、本書を読んでいくつか分類があることを学びました。

メソッド参照の種類 同等のラムダ
static Integer::parseInt str -> Integer.parseInt(str)
バウンド Instant.now()::isAfter Instant then = instant.now();
t -> then.isAfter(t)
アンバウンド String::toLowerCase str -> str.toLowerCase()
クラスコンストラクタ TreeMap<K,V>::new () -> new TreeMap<K,V>()
配列コンストラクタ int[]::new len -> new int[len]

(Effective Java 第3版 200頁から引用)

インスタンスを返す式::メソッド名で表すバウンド参照クラス名::メソッド名でインスタンスのメソッドを呼び出すアンバウンド参照の違いがあることを学びました。メソッド参照を使用するとコードがすっきりするので、こういった分類があることを頭の片隅に置きつつ、これからも積極的に利用してゆこうと思います。

本章を読んで総合的に思ったことは、ストリームとラムダを使用したコードは簡潔だが、可読性を担保できる場合だけ使用するようにするべきであるということでした。ストリームの中間操作が何行も続く場合や操作が複雑な場合は、for分の方が分かりやすく書けるのではないかと検討したり、ラムダのパラメータ名も一目で何を表しているのか分かるようにしなければ、かえって可読性が落ち、結果として保守性も悪いコードになり得ます。ストリームとラムダを、可読性の良さという観点で注意深く考えながら書いてゆこうと思いました。

第10章 例外

#

項目71 チェックされる例外を不必要に使うのを避ける

#

チェックされる例外を過剰に使った場合、呼び出し側のコードではcatchブロックで例外を処理したり、例外を伝播させたりすることを強制されることになるため、呼び出し側で例外から回復する処理が本当に必要な場合のみ、チェックされる例外を投げるべきであるということを学習しました。

そして、チェックされる例外を投げることを避ける簡単な方法としてOptionalが使用可能であることを併せて学びました。メソッドの戻り値をオプショナルにし、例外を投げる代わりに空のオプショナルを返すようにすれば、呼び出し側の負荷は下がります。呼び出し側で例外の詳細情報を受け取って回復処理を実装する必要があるのかそうでないのかを吟味し、例外を使用するのかオプショナルを使用するのかを検討すべきであると考えました。

項目76 エラーアトミック性に努める

#

エラーアトミック性という言葉を初めて知りました。以下、本書の引用です。

一般的に言えば、失敗したメソッド呼び出しは、オブジェクトをそのメソッド呼び出しの前の状態にしておくべきです。このような性質を持つメソッドは、エラーアトミックfailure atomic)であると呼ばれます。

(Effective Java 第3版 308頁から引用)

メソッドが失敗した際にオブジェクトの状態が変わらないことが望ましいということですね。これを実現するために、以下のようないくつかの手段があることを学びました。

  • そもそもオブジェクトが不変となるようにクラスを設計する(項目17 可変性を最小限にする に記載)
  • 可変オブジェクトを操作する場合、操作を行う前にパラメータの正当性チェックを行う
  • 失敗する可能性がある処理を、オブジェクトを変更する処理よりも前に行う
  • 元オブジェクトを一時的にコピーしそれに対し操作を行う。操作が完了したらコピーしたオブジェクトの内容で置き換える
  • 失敗した場合、操作が始まる前の時点までオブジェクトの状態を戻す回復コードを書く(あまり一般的ではない方法)

このようにエラーアトミック性を担保する手段はいくつかあり、一般的にオブジェクトを不変にすることが望ましいですが、コストがかかりすぎるような時は本当に必要かどうかを見極め、エラーアトミックな作りにするのか、オブジェクトの状態を変えてしまう場合があることをJavadocコメントで明記するのかを決めなければならないのだということを学びました。

おわりに

#

今回はEffective Java 第3版の中からいくつか項目をピックアップして気づいたことや考えたことを書かせていただきましたが、Javaに限らず一般的にプログラミングで遵守すべきことや、Java特有の注意すべき事項、私にとって個人的に発展的な内容(並行処理等)など、他にも興味深くためになることがたくさん書かれている書籍でした。

書かれている内容の中で深く理解できた部分は実際の業務での設計・コーディングに積極的に活かし、まだ理解が足りない部分は少し期間を空けてから繰り返し読んで理解し、長期に渡って今後の業務に活かしてゆきたいと思える一冊でした。

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。