Effective Javaを勉強します【第2章】

入社3年目になりましたが、全然Javaの基礎できてないなと思いまして、 effective javaの学習を始めました。まず第1章から。

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

staticファクトリーメソッドとは

クラスのインスタンスを返す単なるstaticのメソッド。

  • 例) 基本データ型booleanに対応するボクシングされた基本データクラスBooleanを返す
    • ボクシング:基本データ型の変数をそのラッパークラスのインスタンスに変換(逆はアンボクシング)
public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}

長所

コンストラクタと異なり名前を持つ

同じシグニチャを持つコンストラクタが複数必要な時は、シグニチャの順番を変えるしかない。

  • Hoge(int num, String str)とHoge(String str, int num)
    • 外部からはコードが何をしているのか分からない
  • Hoge.methodA(int num, String str)とHoge.methodB(int num, String str)
    • メソッド名で内部処理の違いを区別できる

メソッドが呼び出されるごとに新たなオブジェクトを生成する必要がない

あらかじめメンバーとして生成しておいたインスタンスや、オブジェクト生成時にキャッシュしておいたインスタンスを返すことで 不必要に重複したオブジェクトの生成を回避可能。

→オブジェクト生成のコストが高く、かつ頻繁に要求される場合は、パフォーマンスの大幅な向上につながる

メソッドの戻り値型の任意のサブタイプのオブジェクトを返却可能

  • staticファクトリーメソッドの戻り値型を、とか抽象インタフェースとかにする
  • 上記メソッドから返すクラスの実装を内部クラスとして持つ
    • 実装の隠蔽が可能
    • 複数のクラスを返却可能なので、柔軟なAPIの設計が可能になる
// java.util.Collectionsの抜粋
...
public class Collections {
    // インスタンス化不可能
    private Collections {
    ...
    // staticファクトリ〜メソッド
    public static <K,V> Map<K,V> More ...unmodifiableMap(Map<? extends K, ? extends V> m) {
        return new UnmodifiableMap<K,V>(m);
    }
    // 返却するクラスの実装
    private static class More ...UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
}

パラメータ化された型のインスタンス生成の面倒さを低減する

Java7からはダイヤモンド構文が導入されたので、おそらく解決した

// ダイヤモンド構文
Map<String, List<String>> m = newHasMap<>();

以前は以下。

// 冗長で面倒
Map<String, List<String>> m = newHasMap<String, List<String>>();
public static <K, V> HashMap<K, V> newInstance() {
    return new HashMap<K, V>();
}
// 簡潔になる
Map<String, List<String>> m = HashMap.newInstance();

短所

publicあるいはprotectedのコンストラクタを持たないクラスのサブクラスを作れない

容易に他のstaticメソッドと区別がつかない

  • コンストラクタほど目立たない(その他のメソッドと同列扱い)
    • valueOfなどstaticファクトリーメソッドでよく利用される命名法に従うことで軽減する

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

数多くのコンストラクタパラメータが必要な時、実現方法としては以下の3つが考えられるが、 読みやすさ、書きやすさ、安全性の観点からビルダーパターンを検討すべきである。

  • テレスコーピングコンストラクタ・パターン
  • JavaBeansパターン
  • ビルダーパターン

テレスコーピングコンストラクタ・パターン

いくつかの必須パラメータとn個のオプションパラメータが存在する時 以下のように複数のコンストラクタをオーバーロードする。

  • 必須パラメータだけを受け取るコンストラクタ
  • 必須パラメータ+0個のオプションパラメータを受け取るコンストラクタ
  • 必須パラメータ+1個のオプションパラメータを受け取るコンストラクタ
  • 必須パラメータ+n個のオプションパラメータを受け取るコンストラクタ

上記のうち、設定したいパラメータを全て含む最も短いパラメータリストを持つ コンストラクタを利用していくことになるが、大抵不要なパラメータも含まれることになる。

→可読性の低下、パラメータの順序を間違える恐れ

JavaBeansパターン

パラメータなしのコンストラクタを呼び出し、その後必要なパラメータと対応したセッターを呼び出し パラメータを設定する。

→生成が複数の呼び出しに分割されるので、その生成過程の途中で不整合な状態にある恐れ

ビルダーパターン

  1. 対象のオブジェクトを直接生成する代わりに、必須パラメータをすべてもつビルダーオブジェクトを生成する。
    • ビルダーは生成するクラスのstaticメンバークラス
  2. オプションパラメータはビルダーオブジェクトの持つセッターのようなメソッドから追加する。
  3. ビルダーオブジェクトの持つbuild()メソッドから生成するクラスのコンストラクタを呼び出し、対象のオブジェクトを生成する。
// 例: Member member = new Member.Builder("Taro").age(25).build();
public class Member {
    private final String name;
    private final int age;
    public static class Builder {
        private final String name; //必須パラメータ
        private int age = 20; //オプションパラメータはデフォルト値で初期化
        public Builder(String name) {
            this.name = name;
        }
        public Builder age(int age) {
            this.age = age;
            return this;
        }
        public Member build() {
            return new Member(this);
        }
    }
    private Member(Builder builder) {
        this.name = Builder.name;
        this.age = Builder.age;
    }
}

欠点

  • オブジェクト生成時に必ずビルダーオブジェクトの生成が必要
    • シビアなパフォーマンスが要求される場合は注意する
  • テレスコーピングコンストラクタ・パターンより長くなりがち
    • 例えばパラメータ数4以上など、パラメータが多い場合にだけ利用すべき
    • 途中からビルダーパターンへ変更は難しいため、将来的にパラメータが増えそうな場合は 最初からビルダーパターンを検討すべき

項目3. privateのコンストラクタかenum型でシングルトンを強制する

シングルトンとは

一度しかインスタンスが作成されないクラス

→コード中の同じクラスのインスタンスは全て同一であることが保証される

実現方法1:privateなコンストラクタ + finalのフィールド (+ staticファクトリーメソッド)

以下ではfinalのフィールド初期化時にのみ、コンストラクタが呼び出される

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    ...
}

または、staticファクトリーメソッドから返す。

public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance { return INSTANCE; }
    ...
}

上記はAccessibleObject.setAccessibleメソッドを使用して、リフレクションにより privateなコンストラクタを呼び出すことが可能。

  • コンストラクタを修正して2つ目のインスタンス生成時は例外を返すなどして回避

また、シリアライズ可能にする場合、ディシリアライズ毎に新たなインスタンスが生成されることを防ぐために 次のメソッドを追加する。

private Object readResolve() throws ObjectStreamException {
    // ディシリアライズ時にINSTANCEを返却して、重複生成を防ぐ
    return INSTANCE;
}

リフレクションとは

「実行時にプログラム自身の情報を取得/振る舞いを変更する」仕組み。 (リフレクションがどうしても必要なケースがいまいち分からない)

Class cl = Class.forName("Foo"); //オブジェクト取得
Method method = cl.getMethod("hello"); //メソッド取得
method.invoke(cl.newInstance()); //メソッド実行
  • JPCERTのセキュアコーディングスタンダードには 「リフレクションを使ってクラス、メソッド、フィールドのアクセス範囲を広げない」という項目がある

実現方法2:Enum

実現方法1よりこちらを選ぶべき。

public enum Elvis {
    INSTANCE;
    ...
}

項目4. privateのコンストラクタでインスタンス化不可能を強制する

1つの明示的なprivateなコンストラクタを含むことでインスタンス化を防ぐ。

  • 明示的にコンストラクタを書かないと、コンパイラによりデフォルトコンストラクタが 追加されてしまう
public class UtilityClass {
    private UtilityClass() {
        throw new AssertionError();
    }
    ...
}

項目5. 不必要なオブジェクトの生成を避ける

機能的に同じオブジェクトが必要なときは、1つのオブジェクトを再利用するほうが大抵適切である。

極端にだめな例

String s = new String("stringette") //bad
String s = "stringette"

bad:

“stringette"自体がStringオブジェクトであり、Stringコンストラクタで生成されるオブジェクトと同等

→二重にオブジェクトが生成されてしまう

good:

言語仕様上、同じ内容の文字列リテラルで生成されるインスタンスは再利用することが保証されている。(同一JVM上のみ)

→"stringette"は一度しか生成されず、再利用される

不変クラスの再利用

  • staticファクトリーメソッドを利用すれば大抵、不必要なオブジェクト生成は回避可能
    • 例) Boolean.valeOf(String)

変更されない可変オブジェクトの再利用

  • static初期化子で回避可能

オートボクシングに注意

オートボクシングにより、基本データ型が自動で対応するラッパークラスに変換される

→不要なオブジェクトが増える可能性

Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
    sum += i; //iがLong型にオートボクシングされる
}

防御的コピーは別

何でも再利用すればよいわけではない。 防御的コピーが求められる場合、オブジェクトの再利用が悪質なバグやセキュリティホールにつながる可能性があるので注意。(項目39)

項目6. 廃れたオブジェクト参照を取り除く

廃れた参照

今後それを通してオブジェクトが参照されることのない単なる参照

  • 本文中の例ではelements配列のsize外にある要素

→いらなくなったらnullを代入することで、スタックの利用クライアントで参照がなくなればGCされるようになる - 参照が間違っている場合、NullPointerExceptionでエラーを伝えることもできる

メモリリークを気をつけるべきとき

とにかくnullを入れればよいわけではない。廃れた参照に対する最善の方法は、参照が含まれていた変数を スコープの外に出すこと。

以下のような場合にメモリリークを注意すべき。

  • クラスが独自のメモリを管理しているとき
    • 本文中コードだとStackのelements
  • キャッシュ機構をもつとき
    • WeakHashMapでキャッシュを表現するのが望ましい
      • エントリーが廃れると(キーへの外部からの参照がなくなると)自動的に取り除かれる
  • リスナーやコールバックを持つクラス
    • キャッシュと同じくWeakHashMapで解決可能

項目7. ファイナライザを避ける

ファイナライザは予測不可能で、大抵危険であり、一般には必要ない

  • JPCERTのセキュアコーディングスタンダードには 「ファイナライザは使わない」という項目がある

ファイナライザ: GCによりインスタンスが破棄されるタイミングで実行されるメソッド

  • 即座にファイナライザが実行される保証がない
    • 時間的に制約のあることをファイナライザで行うべきではない
    • 実行タイミングはJVMごとのGCアルゴリズムに依存する
  • 実行されるかどうかも分からない
    • 重要な処理をファイナライザに頼ると実行されない可能性がある
    • 例1) データベースの共有ロックの解除
    • 例2) ファイナライザ中の例外は無視されて、そのままファイナライズは終了する
      • 他のオブジェクトへの悪影響を与える可能性もある
  • パフォーマンスも悪い
  • サブクラスのファイナライザは手作業でスーパークラスのファイナライザを呼び出す必要あり

→リソースの開放はファイナライザに頼らず、try-finallyのfinallyブロックで明示的に終了させる

ファイナライザが有効な場合

以下に対しては有効ではあるが、明示的終了のほうがよりよい。

  • 安全ネットとして
    • 明示的な終了を忘れた場合に備えて、保険的にファイナライザを用いる
      • ただし明示的終了がされずファイナライザが呼ばれたときは警告ログを出力すべき
  • ネイティブピアに対して

ファイナライズ連鎖とファイナライザガーディアン

サブクラスから、スーパークラスのファイナライザの呼び出しは手動で実施する必要がある。

→継承される可能性のあるクラスでファイナライズを用いる場合、 ファイナライザガーディアンと呼ばれる無名クラスを検討したほうがよい。

ファイナライザガーディアンとは

  • ファイナライザ処理を含み、スーパークラスとなりうるクラスの内部クラスとして記述する
  • ファイナライザガーディアンの中にスーパークラス自身のfinalize処理を書いておく
  • サブクラスでスーパークラスのfinalize処理を忘れたとしても、内部クラスとして定義されている ファイナライザガーディアンによってスーパークラスはfinalize処理される