読者です 読者をやめる 読者になる 読者になる

コンストラクタで final なフィールドをあきらめない方法

Java

思いつきエントリ。後で説明とか付け加える予定。付け加えた。


final なフィールドは基本的にコンストラクタ内部で初期化することしか出来ない。
でも、そのフィールドを初期化する方法が複雑な場合、素直に実装するとコンストラクタがどんどんふくれあがってしまう。
なのでメソッドに分割したい・・・というのはまぁ普通によくあることなんだけど、例えそのメソッドがコンストラクタからしか呼び出されていなかったとしても、

// コンパイルエラーになる
public final class Hoge {
    final int hoge;
    public Hoge(int piyo) {
        prepareHoge(piyo);
    }
    // コンストラクタからしか呼び出されない
    private void prepareHoge(int piyo) {
        // 何かとても複雑な処理
        // ...
        hoge = result;
    }
}

こういうコードはコンパイルを通らない。

private static なメソッドを使う方法

上で示したプログラムはコンパイルを通らないが、それはコンストラクタ内部でフィールドを初期化していないからだ。
なので、prepareHoge メソッドの戻り値として、複雑な処理をした結果を返し、それをコンストラクタ内で hoge の初期化に使えばいい。

public final class Hoge {
    final int hoge;
    public Hoge(int piyo) {
        hoge = prepareHoge(piyo);
    }
    private static int prepareHoge(int piyo) {
        // 何かとても複雑な処理
        // ...
        return result;
    }
}

ここでは、prepareHoge の戻り値の型を変更しただけではなく、static に変更している。
非 static だと Hoge クラスのフィールドにアクセスすることが出来るが、このメソッドがコンストラクタから呼び出されることを考えると、prepareHoge が使う入力はすべて引数で渡すべきという判断から static にしている。
こうすることで、まだ初期化されていないフィールドにアクセスしてしまうことがなくなる。

// 非staticの場合
public final class Hoge {
    final int hoge;
    final int piyo;
    public Hoge() {
        // ここをみただけではおかしいことに気付かない
        hoge = prepareHoge();
        piyo = preparePiyo();
    }
    private int prepareHoge() {
        // ここをみただけでもpiyoが先に初期化されているはずという先入観などがあると気付きにくい
        return piyo + 10;
    }
    private int preparePiyo() {
        return 32;
    }
    
    public static void main(String[] args) {
        Hoge h = new Hoge();
        System.out.println(h.hoge); // 42・・・ではなく、10
        System.out.println(h.piyo); // 32
    }
}
// staticの場合
public final class Hoge {
    final int hoge;
    final int piyo;
    public Hoge() {
        // コンパイルエラーが出てくれる
        hoge = prepareHoge(piyo);
        piyo = preparePiyo();
    }
    private static int prepareHoge(int piyo) {
        return piyo + 10;
    }
    private static int preparePiyo() {
        return 32;
    }
}

初期化用のクラスを分ける方法

ただ、上記の方法だけでは問題がある場合もある。
例えば、設定ファイルを読み込んで各種フィールドを設定するような場合だ。

public final class Hoge {
    final int hoge;
    final int piyo;
    public Hoge(String configFilePath) {
        // 何回もファイルを読み込むのは微妙
        hoge = prepareHoge(configFilePath);
        piyo = preparePiyo(configFilePath);
    }
    private static int prepareHoge(String configFilePath) {
        // ファイルからhogeを取り出す
    }
    private static int preparePiyo(String configFilePath) {
        // ファイルからpiyoを取り出す
    }
}

さすがに、何回も設定ファイルを読み込むのは避けたい。
これを解決するには、クラス内部からのみ使用される単純なクラスを用意するという方法がある。

public final class Hoge {
    final int hoge;
    final int piyo;
    // loadの結果をprivateなコンストラクタに丸投げ
    public Hoge(String configFilePath) {
        this(load(configFilePath));
    }
    private Hoge(HogeData data) {
        hoge = data.hoge;
        piyo = data.piyo;
    }
    private static HogeData load(String configFilePath) {
        HogeData result = new HogeData();
        // 設定ファイルからresult.hogeとresult.piyoのデータを読み込む
        return result;
    }
    // 初期化のみに使用するクラス
    // private staticなクラスなので、外部から使用されることはない。
    // そのため、このクラスのフィールドをfinalにする必要性はない。
    private static final class HogeData {
        int hoge;
        int piyo;
    }
}

このように、初期化のみに使用するクラスのオブジェクトを受け取る private なコンストラクタを用意し、public なコンストラクタは処理を丸投げしてしまう。
設定ファイルの読み込み処理は、初期化のみに使用するクラスを返す private なメソッドを用意することで解決している。


クラスのかわりに Map を使うだとかも出来るんだけど、まぁそれはそれ。