【Effective Java】使われなくなったオブジェクト参照を取り除く


Effective Javaの独自解釈です。
第3版の項目7について、自分なりにコード書いたりして解釈してみました。

概要

  • ロジック上、二度と使わないオブジェクトであっても、ガベージコレクタはそれを知る術を持たないことがある。
  • いつ参照がなくなるかを意識し、ガベージコレクタが判断できない場合は意図的にnullを参照させる。
  • 変数はなるべく使う直前に宣言し、スコープも最小限に留める。

前提知識

ガベージコレクション

Javaにはガベージコレクタによって、参照されることがなくなったオブジェクトが自動で削除される機能が備わっている。

例えば以下のコードではforループでString"hoge0", "hoge1", "hoge2" オブジェクトが作られていくが、参照変数hogeは参照対象を順にスライドさせていくため、 "hoge0", "hoge1" オブジェクトはガベージコレクタによって削除される。

String hoge = null;

for (int i = 0; i < 3 ; i++){
    hoge = "hoge" + i;
}

一方、参照が残っているオブジェクトは削除対象とならず、いつまでもメモリに残ったままとなる。
以下のコードでは配列にオブジェクトを格納しているが、 hoge[0], hoge[1], hoge[2]"hoge0", "hoge1", "hoge2" それぞれを参照可能なので、ガベージコレクションで削除対象とならず、オブジェクトはメモリに残り続ける。

String[] hoges = new String[3];

for (int i = 0; i < 3; i++) {
    hoges[i] = "hoge" + i;
}

アンチパターンと解決策

お手製スタック

スタックとは後入れ先出し(Last In First Out)のデータ構造のことである。
Dequeインタフェースを利用して簡単に実装できるが、ここでは敢えてお手製で実装する。

お手製スタッククラス

public class Stack {

    /**
     * スタック用配列
     */
    private String[] hoges;

    /**
     * 現在のスタック要素数
     */
    private int currentElementCount = 0;

    /**
     * コンストラクタ
     * スタックのサイズを定義
     */
    public Stack() {
        this.hoges = new String[3];
    }

    /**
     * スタックにプッシュ
     *
     * @param str プッシュする文字列
     */
    public void push(String str) {
        hoges[currentElementCount] = str;
        currentElementCount = currentElementCount + 1;
    }

    /**
     * スタックからポップ
     *
     * @return ポップされた文字列
     */
    public String pop() {
        if (currentElementCount == 0) {
            throw new EmptyStackException();
        }
        currentElementCount = currentElementCount - 1;
        return hoges[currentElementCount];
    }
}

このクラス、Pushをすれば確かに要素が入るし、Popをすれば確かに最後に入れた要素が取得できるが、一つ問題がある。
それはPopする際にあくまで参照する番号をずらしているだけなので、オブジェクトの参照自体は残ったままなのである。

ロジックとしては正しく、Popされた要素は二度と使われることがないのだが、参照が残っているためガベージコレクタは削除対象か判断できない。
この例ではスタックの最大サイズが3なので、せいぜい3オブジェクトしかメモリに残らないが、サイズが大きいスタックの場合、深刻なメモリ不足を引き起こしてしまう。
Popする際に配列の該当要素に敢えてNullを入れ、元のオブジェクトへの参照を抹消することでこの問題は解決できる。

が、本当にスタックを使いたいなら素直にDequeインタフェースを利用しましょう。

使われない箇所での変数宣言

変数を宣言するのが早すぎて無駄にオブジェクトをメモリに残してしまうこともある。
以下のコードでは prefix の宣言箇所に問題がある。

/**
 * 指定したループ回数までの数字を繋げた文字列返す
 * 長さが指定したしきい値を超えていたらプレフィックスを追記
 *
 * @param limitLength 文字列しきい値
 * @param loopCount   ループ回数
 * @return ループで作られた文字列
 */
public String hoge(int limitLength, int loopCount) {

    String prefix = "Over" + limitLength + ": ";

    StringBuilder str = new StringBuilder();
    for (int i = 0; i < loopCount; i++) {
        str.append(i);
    }

    if (str.length() > limitLength) {
        return prefix + str;
    } else {
        return str.toString();
    }
}

問題は二点。

  • 実際にifブロックで使われるまで、オブジェクトがメモリに確保されっぱなしになる。
  • ifブロックの外で宣言しているので、elseに入った場合でもオブジェクトはメモリに残ったままである。

以下のように、本当に使う場所の直前で宣言することで、上記問題は解決できる。

    if (str.length() > limitLength) {
        String prefix = "Over" + limitLength + ": ";
        return prefix + str;
    } else {
        return str.toString();
    }

長々としたメソッドの場合、意外と気づかないことが多いので注意が必要である。