🌌

Java達人への道:Effective Java 第3版からの学び

2023/12/13に公開

こんにちは、Javaのアドベントカレンダー13日目の記事です!
https://qiita.com/advent-calendar/2023/java

Javaでサーバーサイドを書いてお金をもらっている山下です。

この記事では私がEffective Java 第3版を読んで学びにつながったことを5つに絞って紹介したいと思います。
(全章学びがあるので読むことをお勧めします)

https://www.amazon.co.jp/dp/4621303252

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

簡単に言うと、オブジェクトに値を渡したいときに考えられるデザインパターンは以下の3つです。

  • Telescoping Constructorパターン
  • JavaBeansパターン
  • Builderパターン

その中でもBuilderパターンがおすすめ。

Telescoping Constructorパターン

いつものコンストラクタを使うとsetterを定義しない限り、イミュータブルにできスレッドセーフに作れます。
ただ、デメリットとしてパラメータが増えると可読性が下がります。
そのため、「パラメータの設定順序を知る必要があり、間違えたときに気付きにくい」「読み手はパラメータの意味を理解しにくい」ことがあげられます。
Lombokなら@~ArgsConstructorで設定可能です。
下記のようなクラスです。

public class StudentGrades {
    private int mathGrade;
    private int scienceGrade;
    private int historyGrade;
    private int languageArtsGrade;
    private int physicalEducationGrade;

    // コンストラクタ
    // mathGradeとscienceGradeを誤って逆に渡してもコンパイラは教えてくれない
    public StudentGrades(int mathGrade, int scienceGrade, int historyGrade, int languageArtsGrade, int physicalEducationGrade) {
        this.mathGrade = mathGrade;
        this.scienceGrade = scienceGrade;
        this.historyGrade = historyGrade;
        this.languageArtsGrade = languageArtsGrade;
        this.physicalEducationGrade = physicalEducationGrade;
    }

    // ゲッターとか
}

Java Beansパターン

Java Beansパターンでは可読性が高く、インスタンスの生成も容易です。
デメリットとしてはイミュータブルにできないことです。
オブジェクト生成後もsetterを使いメンバ変数が変更可能であるためスレッドセーフに作れません。

public class StudentGrades {
    private int mathGrade;
    private int scienceGrade;
    private int historyGrade;
    private int languageArtsGrade;
    private int physicalEducationGrade;

    // ゲッターとセッター
    public int getMathGrade() {
        return mathGrade;
    }

    public void setMathGrade(int mathGrade) {
        this.mathGrade = mathGrade;
    }

    public int getScienceGrade() {
        return scienceGrade;
    }

    public void setScienceGrade(int scienceGrade) {
        this.scienceGrade = scienceGrade;
    }

    public int getHistoryGrade() {
        return historyGrade;
    }

    public void setHistoryGrade(int historyGrade) {
        this.historyGrade = historyGrade;
    }
}

Builderパターン

Builderパターンを使うとTelescoping ConstructorパターンとJava Beansパターンの長所をいいところどりが出来ます。
Lombokなら@Builderで設定できます。

項目11 equalsをオーバーライドするときは、常にhashCodeをオーバーライドする

なぜequalsとhashCodeを同時に実装しなければならないか、ご存知ですか?
私はこの項目を読むまで知らずに生きていました...
一般的にhashCodeメソッドには下記のような規則があります。

  • Javaアプリケーションの実行中に同じオブジェクトに対して複数回呼び出された場合は常に、このオブジェクトに対する equalsの比較で使用される情報が変更されていなければ、hashCodeメソッドは常に同じ整数を返す必要があります。 ただし、この整数は同じアプリケーションの実行ごとに同じである必要はありません。
  • equals(Object)メソッドに従って 2つのオブジェクトが等しい場合は、2つの各オブジェクトに対するhashCodeメソッドの呼出しによって同じ整数の結果が生成される必要があります。
  • equals(java.lang.Object)メソッドに従って 2つのオブジェクトが等しくない場合は、2つの各オブジェクトに対するhashCodeメソッドの呼出しによって異なる整数の結果が生成される必要はありません。 ただし、プログラマは、等しくないオブジェクトに対して異なる整数の結果を生成すると、ハッシュ表のパフォーマンスが向上する可能性があることに注意するようにしてください。
    https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/lang/Object.html

equalsをオーバーライドしたが、hashCodeをオーバーライドしないと2番目の規則に違反してしまいます。
その結果、プログラムが正しく動作しなくなる可能性があります。
例えば、下記のようなコードです。

Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
m.get(new PhoneNumber(707, 867, 5309));// nullが返される

上記のコードでは、PhoneNumberクラスにはequalsが実装されていますが、hashCodeが実装されていない前提です。
そのため、2行目と3行目で異なるハッシュコードが生成されてしまい、2行目でputした値を取得することが出来ません。

項目18 継承よりもコンポジションを選ぶ

コードを再利用するときに継承を選ぶことが常に最善とは限りません。
継承を選ぶことのデメリットは「カプセル化を破ること」にあります。
サブクラスはスーパークラスの実装に依存するため、スーパークラスの修正にサブクラスが常に追従する必要があります。
また、スーパークラスが欠陥を持って居る場合、欠陥がサブクラスにも伝播されてしまいます。
コンポジションを使えば、上記の問題を解決できます。

コンポジションではなく、継承を選ぶのは下記の場合です。

  • サブクラスとスーパークラスの間に「is-a」関係が成り立つ場合
    • 全てのサブクラスはスーパークラスであるか
  • 拡張しようとしているクラスは欠陥を持っていない場合

項目28 配列よりもリストを選ぶ

配列とリストの違い、どちらを使った方がいいかご存知ですか?

配列は2つの重要な点で、ジェネリックス型と異なっています。

  1. 配列は共変、ジェネリックスは不変
  2. 配列は具象化されている

配列は共変、ジェネリックスは不変とは?

配列が共変であるとは、親クラスの配列に子クラスの要素を代入することが出来ることを指します。

  // StringはObjectのサブタイプなので、代入可能
  Object[] objectArray = new String[1];

ジェネリックスが不変であるとは、逆に親クラスの配列に、子クラスの要素を代入することが不可能ということです。

public class EffectifveJavaClass {
    // 原型 List<String>を渡せる
    private final List stringList = ... ;
    // 型パラメータにObjectを指定 List<String>を渡せない
    private final List<Object> stringList = ... ;
  }

ジェネリックスに欠陥があることを意味すると思われるかもしれませんが、欠陥があるのはほぼ間違いなく配列です。

なぜなら、コンパイル時に気付けるから。

  // 実行時に失敗する
  Object[] objectArray = new Long[1];
  // ArrayStoreExceptionが投げられる
  ObjectArray[0]= "I don't like Elasticsearch";
  // コンパイルがされない
  List<Object> ol = new ArrayList<Long>();
  ol.add("I don't like Elasticsearch");

配列は具象化されているとは?

そもそも、なぜ配列は実行時にならないと例外を出せないのか???
それは下記のような流れで実行されるからです。

  1. 配列は実行するときに、自分がどんな型を格納できるのか知る。
  2. 実行するときに格納された要素に強制する。
    これが「具象化されている」という性質。

ジェネリックスがどのような流れで実行されているかはEffective Javaを読んで確認してみてください。

項目61 ボクシングされた基本データよりも基本データ型を選ぶ

基本データ型とボクシングされた基本データ型には3つの違いがあります。
これらを認識しないと厄介な問題を引き起こす可能性があります。

  1. 基本データ型は値だけを持つが、ボクシングされた基本データ型は値とは別のアイデンティティを持つ
  2. ボクシングされた基本データ型はnullを持つことが出来る
  3. 基本データ型は時間と空間的に効率的

基本データ型は値だけを持つが、ボクシングされた基本データ型は値とは別のアイデンティティを持つ

下記のコードを実行すると異なるインスタンスの参照であるため、falseが表示されます。
そのため、ボクシングされた基本データ型に対して==演算子を適用するの危険です。

System.out.println(new Integer(42) == new Integer(42))

他の違いが引き起こす問題はEffective Javaを読んで確認してみてください。

Discussion