Effective Java 第３版 第６章enumとアノテーション


Java中級者以上の必須本である、[Effective Java 第３版](https://www.amazon.co.jp/Effective-Java-%E7%AC%AC3%E7%89%88-%E3%82%B8%E3%83%A7%E3%82%B7%E3%83%A5%E3%82%A2%E3%83%BB%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF-ebook/dp/B07RHX1K53)に Kindle版が出たので、まとめる。

前：[Effective Java 第３版 第５章ジェネリックス](https://qiita.com/tseno/items/87b86f852ee3f262dc2b)
次：[Effective Java 第３版 第７章ラムダとストリーム](https://qiita.com/tseno/items/7bbac195dc3382fab167)

# 項目３４　intの代わりにenumを使う

- enumは、public static finalフィールドを通して、各列挙定数に対して一つのインスタンスを公開しているクラス。
- enumはアクセス可能なコンストラクタを持っていないので、事実上final。
- enum型は、基本的に単一要素のenumであるシングルトンを汎用化したもの。
- enum型には、任意のメソッドやフィールドを追加できる。

```java:データと振る舞いを持つenum型
public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS(4.869e+24, 6.052e6),
    EARTH(5.975e+24, 6.378e6),
    MARS(6.419e+23, 3.393e6);
    // 省略

    private final double mass; // 質量
    private final double radius; // 半径
    private final double surfaceGravity; // 表面重力

    private static final double G = 6.67300E-11; // 万有引力定数
    // enum定数に、データを関連づけるコンストラクタ
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }
    public double mass() {
        return mass;
    }
    public double radius() {
        return radius;
    }
    public double surfaceGravity() {
        return surfaceGravity;
    }
    // 
    public double getSurfaceWeight(double mass) {
        return mass * surfaceGravity;   // F = ma
    }
    // 呼び出し例
    public static void main(String[] args) {
　　　　　// 地球上で60kgは他の惑星ではどれくらいの重さになるか？
        double earthWeight = Double.parseDouble("60.0");
        double mass = earthWeight / Planet.EARTH.surfaceGravity();
        for (Planet p: Planet.values()) {
            System.out.printf("Weight on %s is %f%n", p, p.getSurfaceWeight(mass));
        }
    }
}

// 結果
Weight on MERCURY is 22.674402
Weight on VENUS is 54.303060
Weight on EARTH is 60.000000
Weight on MARS is 22.776240
```

- 定数固有クラス本体と定数固有データを持つenum型

```java:定数固有クラス本体と定数固有データを持つenum型
public enum Operation {
    PLUS("+") {
        // applyをオーバーライドする
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        public double apply(double x, double y) {
            return x / y;
        }
    };
    private final String symbol;

    @Override
    public String toString() {
        return symbol;
    }
    // コンストラクタ
    Operation(String symbol) {
        this.symbol = symbol;
    }
    // abstractでメソッドを定義すれば、各euum定数はメソッドのオーバーライドが義務付けられる
    public abstract double apply(double x, double y);

    // 呼び出し例
    public static void main(String[] args) {
        double x = 2.0;
        double y = 4.0;
        for (Operation op : Operation.values()) {
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
        }
    }
}
```

# 項目３５　序数の代わりにインスタンスフィールドを使う

- enum型には、自動的にint値が割り当てられるが、、欠番や重複が出た時に対応できないので使うべきではない。int値を使う場合は、明示的に割り当てる。

```java:インスタンスフォールドに値を保存する
public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(0), DECTET(10), TRIPLE_QUARTET(12);

    private final int numberOfMusicians;

    // 数値を持たせたいときは、コンストラクタに定義する
    Ensemble(int size) {
        this.numberOfMusicians = size;
    }

    public int getNumberOfMusicians() {
        return numberOfMusicians;
    }

    // 呼び出し例
    public static void main(String[] args) {
        for (Ensemble e : Ensemble.values()) {
            System.out.printf("%s %s %n",e.name(), e.numberOfMusicians);
        }
    }
}
// 実行結果
SOLO 1 
DUET 2 
TRIO 3 
QUARTET 4 
QUINTET 5 
SEXTET 6 
SEPTET 7 
OCTET 8 
DOUBLE_QUARTET 8 
NONET 0 
DECTET 10 
TRIPLE_QUARTET 12 
```

# 項目３６　ビットフィールドの代わりにEnumSetを使う

- ビットフィールドとは、a=1、b=2、c=4、d=8 とした場合、abcを1+2+4=7と表現するやり方。
- EnumSetは複数のenum型を持てるSetである。

```java:EnumSetの例
public class Text {
  public enum Style { BOLD, ITALIC, UNDERLINE STRIKETHROUGH }
  // SetでEnumSetを受け取る
  public void applyStyles(Set<Style> styles) { ... }
}
// EnumSetの使用例
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC);
```

# 項目３７　序数インデックスの代わりにEnumMapを使う

- EnumSetとは、enumをキーとして使うように設計された高速なMapの実装。

```java:EnumSetの使用例
public class Plant {

    enum LifeCycle {ANNUAL, PERENNIAL, BIENNIAL;}

    final String name;
    final LifeCycle lifeCycle;

    Plant(String name, LifeCycle lifeCycle) {
        this.name = name;
        this.lifeCycle = lifeCycle;
    }

    @Override
    public String toString() {
        return name;
    }

    // 呼び出し例
    public static void main(String[] args) {
        var garden = List.of(new Plant("annual", LifeCycle.ANNUAL),
                new Plant("biennial1", LifeCycle.BIENNIAL),
                new Plant("biennial2", LifeCycle.BIENNIAL));
        // EnumMapのインスタンスを作成する
        Map<LifeCycle, Set<Plant>> plantsByLifeCycle =
                new EnumMap<>(Plant.LifeCycle.class);
        // キー：enum型LifeCycle、値が空のHashSetを持つMapを追加する
        for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
            plantsByLifeCycle.put(lc, new HashSet<>());
        }
        // MapのHashSetに値を追加する。
        for (Plant p : garden) {
            plantsByLifeCycle.get(p.lifeCycle).add(p);
        }
        System.out.println(plantsByLifeCycle);
    }
}
// 出力結果
{ANNUAL=[annual], PERENNIAL=[], BIENNIAL=[biennial1, biennial2]}
```

# 項目３８　拡張可能なenumをインタフェースで模倣する

- enum型は拡張できないが、インタフェースをenum型で実装することは可能である。

```java:インタフェースをenumで実装する
// インタフェースの定義
public interface Operation {
    double apply(double x, double y);
}
// インタフェースを実装する
enum BasicOperation implements Operation {
    PLUS("+") {
        // applyを実装する
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        public double apply(double x, double y) {
            return x / y;
        }
    };
    private final String symbol;

    // コンストラクタ
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }
}

// 拡張enum型
enum ExtendedOperation implements Operation {
    EXP("^") {
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        public double apply(double x, double y) {
            return x % y;
        }
    };
    private final String symbol;

    // コンストラクタ
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }
}

// 呼び出し例
class Main {
    public static void main(String[] args) {
        double x = 2.0;
        double y = 4.0;
        text1(BasicOperation.class, x, y);
        text1(ExtendedOperation.class, x, y);
        text2(Arrays.asList(BasicOperation.values()), x, y);
        text2(Arrays.asList(ExtendedOperation.values()), x, y);
    }

    // enumかつOperationのサブタイプとして渡す
    private static <T extends Enum<T> & Operation> void text1(Class<T> opEnumType, double x, double y) {
        for (Operation op : opEnumType.getEnumConstants()) {
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
        }
    }

    // 境界ワイルドカード型を使って渡す方法
    private static void text2(Collection<? extends Operation> opSet, double x, double y) {
        for (Operation op : opSet) {
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
        }
    }
}
// 実行結果
2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000
2.000000 ^ 4.000000 = 16.000000
2.000000 % 4.000000 = 2.000000
2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000
2.000000 ^ 4.000000 = 16.000000
2.000000 % 4.000000 = 2.000000
```

# 項目３９　命名パターンよりもアノテーションを選ぶ

- メソッド名などに命名パターンによる特別な性質を持たせることは欠点があるため、アノテーションを使うべきである。(例：testSafetyOverrideはtestから始まるのでテストメッド、は悪い例)
- Testアノテーションの定義

```java

// @Testアノテーションの定義
@Retention(RetentionPolicy.RUNTIME) // Testアノテーションが実行時に保持されるべき
@Target(ElementType.METHOD) // メソッド宣言に対してのみ許される
@interface Test {
}

// テスト対象クラス
class Sample {
    @Test
    public static void m1() {
    }

    public static void m2() {
    }

    @Test
    public static void m3() {
        throw new RuntimeException("Boom");
    }

    public static void m4() {
    }

    @Test
    public static void m5() {
    }

    public static void m6() {
    }

    @Test
    public static void m7() {
        throw new RuntimeException("Crash");
    }

    public static void m8() {
    }
}

/// テストアノテーション処理クラス
class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName("sec39.Sample");
        // テスト対象内の全メソッドをループする
        for (Method m : testClass.getDeclaredMethods()) {
            // @Testアノテーションが付いているか？
            if (m.isAnnotationPresent(Test.class)) {
                tests++;
                try {
                    // テスト対象メソッド呼び出し
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " failed: " + exc);
                } catch (Exception e) {
                    System.out.println("Invalid @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}

// 実行結果
public static void sec39.Sample.m7() failed: java.lang.RuntimeException: Crash
public static void sec39.Sample.m3() failed: java.lang.RuntimeException: Boom
Passed: 2, Failed: 2
```

# 項目４０　常にOverrideアノテーションを使う

- オーバーライド時に、`@Override`アノテーションを付けることは、オーバーライドされていなかったことを検知してくれる。
- `@Override`アノテーションを付けないでオーバーライドしようとすると、IDEは警告を出してくれるので、意図しないオーバーライドを避けることができる。

# 項目４１　型を定義するためにマーカーインタフェースを使う

- マーカーインタフェースとは、メソッド宣言を含んでいない（空っぽの）インタフェースで、そのインタフェースを実装しているクラスは何らかの特性を持っていることを指定（マーク）する。
- 関連づけられた新たなメソッドを持たない型を定義したいなら、マーカーインタフェースであるべき。
- クラスとインタフェース以外のプログラム要素をマークしたい、あるいはアノテーション型をすでに多用しているフレームワーク内にマーカーを適合させたいなら、マーカーアノテーションを選ぶべき。