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

Effective Java 第 3 版の個人的メモ

  1. 項目 1 コンストラクタの代わりに static ファクトリーメソッドを検討する
  2. 項目 2 数多くのコンストラクタパラメータに直面した時にはビルダーパターンを検討する。
  3. 項目 3 private のコンストラクタか enum 型でシングルトン特性を強制する
  4. 項目 4 private のコンストラクタでインスタンス化不可能を強制する
  5. 項目 5 資源を直接結び付けるよりも依存性注入を選ぶ
  6. 項目 6 不必要なオブジェクトの生成を避ける
  7. 項目 7 廃れたオブジェクト参照を取り除く
  8. 項目 8 finalizer と cleaner の使用は避けるべし
  9. 項目 9 try-finally よりも try-with-resources を使うべし

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

1.1. 結論

コンストラクタの代わりに static ファクトリーメソッドを使用すると様々なメリットがある。 そのメリットが必要ない時はコンストラクタを使用すればよい。

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

static ファクトリーメソッドとは、下記の例のHuman#create()のようなインスタンスを返す static メソッドである。

Human.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Human {
    private int age;
    private String name;

    Human(int age, String name){
        this.age = age;
        this.name = name;
    }

    public static Human create(int age, String name){
        var human = new Human(age, name);
        return human;
    }
}

1.3. static ファクトリーメソッドのメリット

  1. 分かりやすいメソッド名を付けられる
  2. オブジェクト生成のために新しいインスタンスを毎回生成する必要がない。
  • コンストラクタの場合、新しいインスタンスを生成する必要がある。
  • static ファクトリーメソッドの場合、新しいインスタンスを生成しなくてもよい。(シングルトンパターンにできる。)
  1. サブクラスのインスタンスでも生成できる(柔軟性が高く、自由に処理を入れることができる。)
  2. 引数によって、異なるインスタンスを生成できる
  • 下記の例のように、引数によって異なるインスタンスを生成することができる。

Human.java

1
2
3
4
5
6
7
public static Human create(int age, String name, boolean isMan) {
    if(isMan){
        return new Man(age,name);
    } else {
        return new Woman(age,name);
    }
}

1.4. static ファクトリーメソッドの使いどころ

  1. 上記 4 つのメリットを享受したい場合は static ファクトリーメソッドの使用を検討する。
  • 例:Logger
  1. 上記 4 つのメリットがない場合は必要ない。
  • 例:DTO

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

2.1. Telescoping constructor

Telescoping =「順番にあてはめる」=1つ1つ順番にあてはめられるように、受け取れる引数が1つずつ増えていくようにコンストラクタを作成し、それらを順番に呼び出そう。

  • 欠点:コンストラクタの引数の何番目に何を設定すれば良いのかがわからなくなる。

2.2. JavaBeans パターン

メンバー変数があり、各メンバー変数のセッターがあるもの。

  • 利点:set メソッドで何をセットしているのかはわかりやすい。
  • 欠点:インスタンス生成時が、完成した状態ではない。生成後に変更(セット)しないと使えない。

2.3. Builder パターン

値を設定していき、最後にビルドしてインスタンスを生成する方法のこと

  • 利点:メンバ変数に final 修飾子をつけておけば、生成後にメンバ変数を変更できないため、インスタンス生成時の状態であることを保証できる。

また下記のような Builder パターンは、fluent API として知られており、assertThat("hoge").isEqualTo("Hoge");のように、左から右へ流し読み(一読)できるようになっている。

Builder パターンのクラス例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Dog {
    private final String name;
    private final Integer age;
    private final String hobby;
    public static class Builder {
        //必須パラメータ
        private final String name;
        //オプションパラメータ デフォルト値に初期化
        private Integer age = 0;
        private String hobby = "Run";

        public Builder(String name) {
            this.name = name;
            return this;
        }
        public Builder age(int age) {
            this.age = age;
            return this;
        }
        public Builder hobby(String hobby) {
            this.hobby = hobby;
            return this;
        }
        public Dog build() {
           return new Dog(this);
        }
    }

    private Dog(Builder builder) {
        name = builder.name;
        age = builder.age;
        hobby = builder.hobby;
    }
}

Builder パターンの利用方法例

1
Dog chihuahua = new Dog.Builder("Chibi").age(3).hobby("Dance").build();

参考サイト:Builder パターンについて

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

3.1. シングルトンなクラス設計の方法について

  1. private のコンストラクタ
  • public finalのフィールドによるシングルトン
    • 欠点:リフレクションにより、privateのコンストラクタを呼び出せるので、シングルトンが破綻すること

Elvis.java

1
2
3
4
5
6
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}
  • staticファクトリーメソッドによるシングルトン
    • 利点 1:final を利用しているので、リフレクションを使ったとしても防止できる。
    • 利点 2:API を変更せずに、シングルトンか否かを変更できる。

Singleton.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public final class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

参考サイト:正しいシングルトンクラスの実装の仕方

3.2. ENUM 型を使ったシングルトン

Elvis.java

1
2
3
4
5
public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}
  • Java の世界でインスタンスが 1 つであることを保証
  • 並列処理の場合でも必ず1つ
  • シリアライズについて考慮しなくて良いから推奨

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

4.1. 結論

ユーティリティクラスは private のコンストラクタでインスタンス化不可能を強制するとよい。

4.2. ユーティリティクラスとは

ユーティリティクラスとは、static メソッドをまとめたクラス

  • 例:java.util.Math や java.util.Arrays)

実装例

1
2
3
4
5
public final class UtilityClass {
    private UtilityClass() {
        throw new AssertionError();
    }
}
  • 利点
    • インスタンス化する意味がないクラスをインスタンス化不可能にできる点。
    • 副次的に、サブクラスを作成することを防げる点。
    • 防げる理由は、すべてのクラスはスーパークラスのコンストラクタを呼び出さなければならないが、呼び出せるアクセス可能なコンストラクタがないため。

public finalとすることで、継承をさせる隙すら与えない。

4.3. インスタンス化不可能にする理由

  • インスタンス化する必要がないのであれば、使う人に考えさせないようにインスタンス化不可能にするべき
  • java.util.Math のようなユーティリティクラスは 1996 年からあり、メモリが少ない時代であったという時代背景もありそう。

5. 項目 5 資源を直接結び付けるよりも依存性注入を選ぶ

5.1. 結論

DI(Dependency Injection)を使うとテストがしやすくなる。

5.2. 資源を直接結び付ける例

下記サンプルコードは、DB が Mysql であることが前提となっていて、Mysql が起動していないとテストできない。 (昔はそのようにテストしていた。)

DaoBLogic.java

1
2
3
4
public class DaoBLogic {
    DAO dao = new MysqlDao();
    ...
}

5.3. 依存性注入の例

下記サンプルコードは、DB が何であるかは、依存性注入時に決めることができる。 つまり、Mock の DB を注入してもテストできるため、柔軟である。

DaoBLogic.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 依存性注入は柔軟でありテストできる
public class DaoBLogic {
    private final Dao dao;

    public DaoBLogic(Dao dao) {
        this.dao = Objects.requireNonNull(dao);
    }
}

インジェクションには以下の3つの方法がある。

  • コンストラクタインジェクション
  • セッターインジェクション
  • フィールドインジェクション(Spring なら@Autowired)

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

6.1. 結論

一度生成されたら変更されないオブジェクトは再利用したり、自動ボクシングに注意したりすることで、パフォーマンスを落とさないようにしよう。

6.2. 生成に時間がかかる処理について

  • java.util.Calendarクラス
    • デフォルトロケール取得等の処理が必要であるため。
  • 例外クラス
    • stacktrace を収集するため。
  • DB コネクションクラス
    • 接続コスト(TCP コネクション確立~ DB の認証)のため。
  • 正規表現のパース処理
    • 正規表現と検査文字列の長さに対して、O(n^2)のオーダーがかかるため。

自動ボクシングにも時間がかかる。 インスタンス生成が走るため。int -> Integer など。

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

7.1. 参照について

  • 直接参照
    • 直接値を見ている参照
  • 関節参照
    • アドレスから値を取得している参照

7.2. 強参照と弱参照

  • 強参照
    • 変数によりアドレスが参照されている状態
  • 弱参照, (ソフト参照、ファントム参照)
    • アドレスがどの変数からも参照されていない状態

強参照が無くなったオブジェクトは、GC の対象となる。

7.3. 気をつけること

MAP などは、強参照で保持し続けるため、個数の上限を決めて、LRU(Least Recently Used)や FIFO などのアルゴリズムで対処しましょう。

以下のライブラリを使うという方法もある。

  • Ehcache
    • Java のキャッシュライブラリの代表で高機能。
  • Guava Cache
    • Google のライブラリである Guava に、キャッシュ用のパッケージも含まれており、基本的なことは可能。
  • Apache DirectMemory
    • JavaVM ヒープ外のメモリをキャッシュに使用。
    • 利点:ヒープ内のキャッシュと比べて、GC の影響を受けない。
    • 欠点:シリアライズ/デシリアライズのコストがかかる。

参考サイト:Java で使えるオープンソース・キャッシュライブラリ

8. 項目 8 finalizer と cleaner の使用は避けるべし

8.1. Finalizer/Cleaner

  • finalizer : オブジェクトが GC で回収されるときに、 java.lang.Object#finalizeが実行される。
    • 実行する人:Finalizer スレッド
    • GCスレッド -(put)-> (Finzelizerキュー) -(get)-> Finalizerスレッド
  • finalize でやりたいこと
    • リソースの解放(DB コネクションのクローズ、ファイルの接続)
  • 問題 1
    • ファイナライザいつまでたっても終わらない問題
    • tomcat スレッド数が 10-20 に対して、GC を行う Finalizer スレッドが 1 スレッドなので、1 対多となり、GC が間に合わなくなる時がある。

参考:Finalizer スレッドは 1 つで、http-nioスレッドは複数ありますね。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MacBook-Air:~ takuto-no$ jps
18372 SpringBootApplication
.
.
.
MacBook-Air:~ takuto-no$ jstack 18372 | grep '"'
"http-nio-8080-AsyncTimeout" #29 daemon prio=5 os_prio=31 tid=0x00007fdac6037000 nid=0x6203 waiting on condition [0x000070000219f000]
"http-nio-8080-Acceptor-0" #28 daemon prio=5 os_prio=31 tid=0x00007fdac2a3f800 nid=0x6103 runnable [0x000070000209c000]
"http-nio-8080-ClientPoller-1" #27 daemon prio=5 os_prio=31 tid=0x00007fdac1c30000 nid=0x5f03 runnable [0x0000700001f99000]
"http-nio-8080-ClientPoller-0" #26 daemon prio=5 os_prio=31 tid=0x00007fdac1c2d800 nid=0xa303 runnable [0x0000700001e96000]
"http-nio-8080-exec-10" #25 daemon prio=5 os_prio=31 tid=0x00007fdac2a3e800 nid=0xa403 waiting on condition [0x0000700001d93000]
"http-nio-8080-exec-9" #24 daemon prio=5 os_prio=31 tid=0x00007fdac2a3e000 nid=0x5b03 waiting on condition [0x0000700001c90000]
"http-nio-8080-exec-8" #23 daemon prio=5 os_prio=31 tid=0x00007fdac2a3d000 nid=0xa503 waiting on condition [0x0000700001b8d000]
"http-nio-8080-exec-7" #22 daemon prio=5 os_prio=31 tid=0x00007fdac1a97000 nid=0x5903 waiting on condition [0x0000700001a8a000]
"http-nio-8080-exec-6" #21 daemon prio=5 os_prio=31 tid=0x00007fdac50fc800 nid=0x5703 waiting on condition [0x0000700001987000]
"http-nio-8080-exec-5" #20 daemon prio=5 os_prio=31 tid=0x00007fdac50fb800 nid=0xa803 waiting on condition [0x0000700001884000]
"http-nio-8080-exec-4" #19 daemon prio=5 os_prio=31 tid=0x00007fdac2a34000 nid=0x5603 waiting on condition [0x0000700001781000]
"http-nio-8080-exec-3" #18 daemon prio=5 os_prio=31 tid=0x00007fdac1ad0800 nid=0x3f03 waiting on condition [0x000070000167e000]
"http-nio-8080-exec-2" #17 daemon prio=5 os_prio=31 tid=0x00007fdac2a33800 nid=0x4103 waiting on condition [0x000070000157b000]
"http-nio-8080-exec-1" #16 daemon prio=5 os_prio=31 tid=0x00007fdac50f8000 nid=0x3e03 waiting on condition [0x0000700001478000]
"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007fdac1859000 nid=0x2e03 in Object.wait() [0x0000700000a5a000]
.
.
.
  • 問題 2
    • ファイナライザ内で例外おきたらどうなるの問題
    • Finalizer スレッドが、A -> B の順でクローズすべきところを、B -> A の順でしてしまい例外など。
    • なお、Finalizer スレッド内の順序制御は出来ない。

8.2. 結論

try-finally の finally 句内で正しい順序でクローズしましょう。

Spring にも終了時の処理を挟む API があるので、使いましょう。

9. 項目 9 try-finally よりも try-with-resources を使うべし

9.1. 結論

基本的には、closeしなければならないリソースを扱う場合、try-finallyではなく、try-with-resourcesを使いましょう。

9.2. try-finally の場合

TryFinally.javaのように書く必要があり、以下の欠点がある。

  • finally 句で、非 null チェックする必要がある。
  • finally 句の中で例外が発生したらスローすることになる。

TryFinally.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.
.
.

} finally {
    if (br != null)
        try {
            br.close();
        } catch (IOException e) {
            // 例外処理
        }
}
.
.
.

豆知識

try 句内で例外が発生し、finally 句でも例外が発生すると、finally 句の例外だけがエラーメッセージとして出る。
以下のように、finally 句で出た例外を揉み消せば、try 句内で出た例外がスローされるよ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    void execute(){

        var r = new TestResource();
        try {
            r.highLayerClose();
        } finally {
            try {
                r.lowLayerClose();
            } catch (Throwable th) {
                System.out.println("揉み消す");
            }

            try {
                r.highLayerClose();
            } catch (Throwable th) {
                System.out.println("揉み消す");
            }
        }
    }

9.3. try-with-resources の場合

TryWithResources.javaのように書きましょう。以下のような利点がある。

  • finally句で close しなくても、自動的で close をしてくれる。
  • close 処理中に、例外が発生しても try 文の中の例外がスローされる。

TryWithResources.java

1
2
3
4
5
6
7
8
9
public void getResource(String src, String dest) throws IOException {
    try (InputStream inputStream = new FileInputStream(src);
         OutputStream outputStream = new FileOutputStream(dest))
    {
        .
        .
        .
    }
}

ただし、try-with-resources でリソース解放されないパターンで紹介されているように、「リソース解放の対象クラスをネストさせてインスタンス生成した場合、コンストラクタで例外が発生するとリソース解放されません。」ので、この場合は、[ 個別にフィールドを宣言し、それぞれインスタンス生成]して対処するようにしましょう。