motacaplaのめう

日頃得られた知見と気付きをdumpするところ

Effective Javaを読んでまとめてみる

はじめに

秋からの業務でJavaを書くこととなった. Javaを使う上での best practiceを学びたいと思い, おすすめされたEffective Javaを読み始めた. 著者の主張を自分なりに解釈してまとめることとする. 項目数が100近くあるので, いくつかに分ける.

Item 1-5

Item 1: constructorよりもstatic factory methodsを使うこと

static factory methodsの利点は以下である:

  • 利点1 : 名前がつけられること
  • 利点2 : immutable (呼び出されるときに新しいobjectを作る必要がない)
  • 利点3 : 柔軟性(任意のsub typeを返り値として設定可能)
  • 利点4 : input parameterであるfunctionによって, 返り値となるobjectを変化させることができる
  • 利点5 : 返り値となるobjectが不要

  • 欠点1 : sub classにできない (public/protected constructorがないclassのため)

  • 欠点2 : プログラマにとって, static factory methodsを見つけるのが難しい

Item 2: constructorのparameterが多数のときにはbuilder patternを利用すること

objectを作成する書き方は大きく3種類ある.

Telescoping constructor pattern

個々の引数に応じてconstructorをオーバーロードしまくるパターン.

見ての通りスケールしない.

public class NutritionFacts {
    private final int servingSize;     // (ml)                     required
    private final int servings;          //  (per container)  required
    private final int calories;           //  (per serving)     optional
    private final int fat;                    //  (g/serving)        optional
    private final int sodium;            //  (mg/serving)     optional
    private final int carbohydrate;  //  (g/serving)        optional

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }
 
    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }

    ...
}

JavaBeans Pattern

default valueを埋め, setterを利用して値を代入する.

public class NutritionFacts {
    private final int servingSize = -1;
    private final int servings = -1;
    private final int calories = 0;
    private final int fat = 0;
    private final int sodium = 0;
    private final int carbohydrate = 0;

    public NutritionFacts() { }
    ....

Builder Pattern

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder() { 
        // Required parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private final int calories;
        private final int fat;
        private final int sodium;
        private final int carbohydrate;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val) {
            calories = val;
            return this;
        }

        public Builder fat(int val) {
            fat = val;
            return this;
        }

        public Builder sodium(int val) {
            sodium = val;
            return this;
        }

        public Builder carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

以下のように利用できる.

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build()

mandatory parametersはbuilder constructorに引き渡し, optionalなparametersは個別にsetしてbuildする.

このように多数のparametersがある際にはbuilder patternが有効である.

Item 3: singleton propertyにはenumを強いること

singletonの実装には大きく3つの方法がある.

public final fieldで定義

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

    public void leaveTheBuilding() { ... }
}
  • 利点1: singletonのクラスであることが自明
  • 利点2: 簡潔

static factory

Date classなどでよく使われている実装方法である.

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

    public void leaveTheBuilding() { ... }
}
  • 利点1: 柔軟性がある (例. Factoryはsingletonじゃないobjectを返す, といった変更がAPIを変えずに可能)
  • 利点2: genericsを使ったsingleton factoryを利用できる

singletonであることを保証するため, すべてのinstance fieldにtransientを宣言してreadResolve methodを定義する必要がある. そうしなければ, instanceがdeserializeされた際に新しいinstanceが生成されてしまう.

private Object realResolve() {
    return INSTANCE;
}

enum

public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}
  • 利点1: public final field の方法よりも簡潔
  • 利点2: 自動的にserializationできる
  • 利点3: 単一instanceを保証できる (serializationやreflectionされた場合においても可能)

よって, singletonにはenumを利用するのが最も良い.

Item 4: private constructorにはnoninstantiabilityを強いること

noninstantiableはconstructorを呼び出すことを許さないことである. noninstantiabilityを得るため, 以下の例では privateなconstructorに AssertionError() を入れている.

public class UtilityClass {
    // Suppress default constructor for noninstantiability
    private UtilityClass() {
        throw new AssertionError();
    }    
}

これにより, constructorが呼ばれないことを保証できる.

Item 5: ハードコード.singletonよりもDIをすること

以下に SpellCheckerクラスを示す.

public class SpellChecker {
    private static final Lexicon dictionary = ...;

    private SpellChecker() {} // Noninstantiable

    public static boolean isValid(String word) { ... }
    public static List<String> suggestions(String typo) { ... }
}

この例では dictionary をハードコーディングしている.

ハードコーディングには大きく2つの欠点がある: - 欠点1: 柔軟性...言語(日本語, 英語など)によってdictionaryは異なるはずなのに, ハードコーディングでは柔軟に変更できない. - 欠点2: テスタビリティ...SpellCheckerクラスがdictionaryに依存しているため, テストが実施できない.

その他の方法として, SeppCheckerをsingletonとして実装することも可能である. しかしながら, こちらもハードコーディングと同様に柔軟性とテスタビリティの観点から避けるべきである. これらの欠点を克服するために, DIをすべきである.

public class SpellChecker {
    private static final Lexicon dictionary;

    // DI
    private SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    } 

    public static boolean isValid(String word) { ... }
    public static List<String> suggestions(String typo) { ... }
}

次回は Item6-10 をまとめる.