enum とアノテーション

Effective Java 第 3 版の個人的メモ

  1. 項目 34 int 定数の代わりに enum を使う
  2. 項目 35 序数の代わりにインスタンスフィールドを使う
  3. 項目 36 ビットフィールドの代わりに EnumSet を使う
  4. 項目 37 序数インデックスの代わりに EnumMap を使う
  5. 項目 38 拡張可能な enum をインタフェースで模倣する
  6. 項目 39 命名パターンよりアノテーションを選ぶ
  7. 項目 40 常に Override アノテーションを使う
  8. 項目 41 型を定義するためにマーカーインタフェースを使う

1. 項目 34 int 定数の代わりに enum を使う

1.1. 結論

  • int enumパターン(int定数)は使わないでおきましょう。
  • enumを使いましょう。
    • 単に定数として使う。
    • 定数に対するメソッドが必要であれば、定数固有メソッド
    • 複数の定数に対して共通のメソッドが必要であれば、戦略enumパターン

1.2. int enumパターンの欠点

以下の欠点がある。

  • 型安全性が提供されない
    • 例:APPLEと間違えてORANGEを渡してもエラーにならない。
  • 名前空間が提供されない
    • 例では接頭辞で分けている。
  • 表示可能な文字列に変換できない(変数名を出力できない)
    • String enumパターンも考えられるが文字列比較になりパフォーマンスが良くない。
1
2
3
4
5
6
7
8
// int enumパターン- かなり不完全
public static final int APPLE_FUJI = 0
public static final int APPLE_PIPPIN = 1
public static final int APPLE_GRANNY_SMITH = 2

public static final int ORANGE_NAVEL = 0
public static final int ORANGE_TEMPLE = 1
public static final int ORANGE_BLOOD = 2

1.3. 単なる定数としてのenum

以下の利点がある。

1
2
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum ORANGE { NAVEL, TEMPLE, BLOOD }

1.4. 定数固有メソッド実装を持つenum

1.4.1. 問題が多い例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 値によって切り替えるenum型
public enum Operation {
  PLUS, MINUS, TIMES, DIVIDE;

  // 定数で表される算術操作を行う
  double apply(double x, double y) {
    switch(this) {
      case PLUS: return x + y;
      case MINUS: return x - y;
      case TIMES: return x * y;
      case DIVIDE: return x / y;
    }
    throw new AssertionError("Unknown op: " + this);
  }
}

1.4.2. 改善例(定数固有メソッド実装を持つenum)

1
2
3
4
5
6
7
8
9
// 定数固有メソッド実装を持つenum型
public enum Operation {
  PLUS {double apply(double x, double y) {return x + y;} },
  MINUS {double apply(double x, double y) {return x - y;} },
  TIMES {double apply(double x, double y) {return x * y;} },
  DIVIDE {double apply(double x, double y) {return x / y;} };

  abstract double apply(double x, double y);
}

抽象メソッドを具象メソッドでオーバーライドし忘れることがない。

1.5. 戦略enumパターン

1.5.1. 問題が多い例

 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
// コードを共有するために値でswitchするenum
enum PayrollDay {
  MONDAY,
  TUESDAY,
  WEDNESDAY,
  THURSDAY,
  FRIDAY,
  SATURDAY,
  SUNDAY;

  private static final int HOURS_PER_SHIFT = 8;

  double pay(double hoursWorked, double payRate) {
    double basePay = hoursWorked * payRate;

    double overtimePay; // 残業手当
    switch(this) {
      // 働いた分だけ残業代
      case SATURDAY: case SUNDAY:
        overtimePay = hoursWorked * payRate / 2;
        break;
      // 定時以降分だけ残業代
      default:
        overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
        0 : (hoursWorked - HOURS_PER_SHIFT ) * payRate / 2;
        break;
    }

    return basePay + overtimePay;
  }
}

1.5.2. 改善例(戦略enumパターン)

 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
35
36
37
38
39
40
41
42
43
44
45
46
47
// 戦略enumパターン
enum PayrollDay {
  // 平日
  MONDAY(PayType.WEEKDAY),
  TUESDAY(PayType.WEEKDAY),
  WEDNESDAY(PayType.WEEKDAY),
  THURSDAY(PayType.WEEKDAY),
  FRIDAY(PayType.WEEKDAY),
  // 土日
  SATURDAY(PayType.WEEKEND),
  SUNDAY(PayType.WEEKEND);

  private final PayType payType;

  PayrollDay(PayType payType) {
    this.payType = payType;
  }

  double pay(double hoursWorked, double payRate) {
    return payType.pay(hoursWorked, payRate);
  }

  // 戦略enum型
  private enum PayType {
    // 定時以降分だけ残業代
    WEEKDAY {
      double overtimePay(double hours, double payRate) {
        return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)
            * payRate / 2;
      }
    },
    // 働いた分だけ残業代
    WEEKEND {
      double overtimePay(double hours, double payRate) {
        return hours * payRate / 2;
      }
    };
    private static final int HOURS_PER_SHIFT = 8;

    abstract double overtimePay(double hrs, double payRate);

    double pay(double hoursWorked, double payRate) {
      double basePay = hoursWorked * payRate;
      return basePay + overtimePay(hoursWorked, payRate);
    }
  }
}

enum 定数を追加するたびに、指定することになるため、指定し忘れることがない。

2. 項目 35 序数の代わりにインスタンスフィールドを使う

2.1. 結論

タイトルのままですが、序数は使わずに、インスタンスフィールドを利用する。

2.1.1. 序数を使った例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 関連付けられた値を導き出すために`ordinal`を乱用
public enum Ensemble {
  SOLO,
  DUET,
  TRIO,
  OUARTET,
  QUINTET,
  SEXTET,
  SEPTET,
  OCTET,
  NONET,
  DECTET;
  public int numberOfMusicians(){
    return ordinal() + 1;
  }
}

定数が並び替えられたら、正しく動作しなくなるため、良くない。
11 人は不要で、12 人が必要な場合はダミーが必要、、、

2.1.2. インスタンスフィールドを使った例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public enum Ensemble {
  SOLO(1),
  DUET(2),
  TRIO(3),
  OUARTET(4),
  QUINTET(5),
  SEXTET(6),
  SEPTET(7),
  OCTET(8),
  NONET(9),
  DECTET(10),
  TRIPLE_OUARTET(12);

  private final int numberOfMusicians;
  Ensemble(int size){
    this.numberOfMusicians = size;
  }
  public int numberOfMusicians(){
    return numberOfMusicians;
  }
}

余談:
Enum#ordinal()の JavaDocに以下のように記載されています。
このメソッドは、ほとんどのプログラマにとって役に立ちません。それは、EnumSet や EnumMap などの洗練された enum ベースのデータ構造で使用するために設計されています。

3. 項目 36 ビットフィールドの代わりに EnumSet を使う

3.1. 結論

  • 集合として enum 型を使いたいときは、EnumSet を使用するべき

3.2. ビットフィールド列挙定数

  • 下のようにビットフィールド定数を列挙して、集合を表現する方法
1
2
3
4
5
6
7
8
9
public class Text{
   public static final int STYLE_BOLD          = 1 << 0; // 1
   public static final int STYLE_ITALIC        = 1 << 1; // 2
   public static final int STYLE_UNDERLINE     = 1 << 2; // 4
   public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
   public void applyStyles(int styles) { ... }
}

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
  • int enum パターンの欠点を持っているし、他にも欠点がある
    • 数値として表示された場合にビットフィールドを読み取るのが困難
      • 「13」と言われてもぱっと見どんな集合か分からない
    • 集合のすべての要素をイテレートをする簡単な方法が無い

3.3. EnumSet

  • EnumSet クラスは単一の enum 型から選ばれた値の集合を効率的に表現できる
  • EnumSet は Set の実装クラス
  • EnumSet を使うと下記のようになる
1
2
3
4
5
6
public class Text {
   public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
   public void applyStyles(Set<Style> styles){ ... }
}

text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
  • デメリットの一つとして、不変な EnumSet を生成できないことがあげられる
    • Collections.unmodifiableSetで包むことができるが、簡潔性とパフォーマンスが損なわれる
      • Collections.unmodifiableSetから返ってきた Set は更新しようとするとエラーになる
      • コンポジットパターンで作られている。

4. 項目 37 序数インデックスの代わりに EnumMap を使う

4.1. 結論

  • enum の列挙子ごとにデータを持ちたい場合は、EnumMap を使うべき

4.2. 序数インデックス

  • ハーブの種類ごとに、ハーブをまとめたいとする。
  • 序数インデックス(ordinal())を用いると下のようになる。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Herb {
    public enum Type {
        ANNUAL, PERENNIAL, BIENNIAL
    }

    private final String name;
    public final Type type;

    public Herb(String name, Type type) {
        this.name = name;
        this.type = type;
    }

    @Override
    public String toString(){
        return name;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 庭園にあるハーブを表す配列
Herb[] garden = new Herb[] { ... };

// ハーブの種類ごとにSetを用意
Set<Herb>[] herbsByType =
       (Set<Herb>[]) new Set[Herb.Type.values().length]; // 無検査キャスト
for (int i = 0; i < herbsByType.length; i++) {
   herbsByType[i] = new HashSet<Herb>();
}

// ハーブを種類ごとに分類
for (Herb herb: garden) {
   herbsByType[herb.type.ordinal()].add(herb);
}
  • 欠点
    • 無検査キャストが発生
    • 配列の要素数以上の int 値でアクセスするとArrayIndexOutOfBoundsExceptionが発生

4.3. EnumMap

  • EnumMap は enum 型をキーとして Map を実装したクラス
  • EnumMap で実装すると下のようになる。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ハーブの種類ごとに Set を用意
Map<Herb.Type, Set<Herb>> map = new EnumMap<>(Herb.Type.class);
for (Herb.Type type: Herb.Type.values()){
    map.put(type, new HashSet<Herb>());
}

// ハーブを種類ごとに分類
for (Herb herb: garden) {
    map.get(herb.type).add(herb);
}

for (Herb.Type type: Herb.Type.values()){
    System.out.println(type+" "+map.get(type));
}
  • 利点
    • 型安全である
    • foreach文が使えるのでArrayIndexOutOfBoundsExceptionは発生しない

5. 項目 38 拡張可能な enum をインタフェースで模倣する

5.1. 結論

Enumは拡張できないけれど拡張性をもたせたい場合がある。 そのような場合は、インタフェースで型を定義し、Enumでスタンダードな実装を提供すれば良い。このときのEnumを擬似拡張Enumと呼ぶ

5.2. 疑似拡張Enumの例

 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
35
36
37
38
39
interface Operation {
    double apply(double x, double y);
}

public enum BasicOperation implements Operation {
    PLUS("+") {
        @Override public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        @Override public double apply(double x, double y) {
            return x - y;
        }
    };

    private final String symbol;
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }
}

public 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;
    }
}

5.2.1. 注意点

Enumであるため、実装の継承を使えない。コードの重複を排除する際は、ヘルパーに切り出す等で対応する。

引数はインタフェース or Enumのどちらを選択するか

  • 疑似拡張Enumを考慮すると、メソッドの引数にはインタフェースで宣言したほうがよい。
  • Enumでは要素すべてを処理するということがあるが、いずれの場合でも実現できる。
    • Enum をメソッド引数の宣言としていれば、すべての定数を列挙する方法は 単に Enum.values() を用いればよい。
    • 引数として実装したインタフェースをうけとるようにした場合、Class オブジェクトの getEnumConstants を使う方法がある。
1
2
3
4
5
private static <T extends Enum<T> & Operation> void getAllConstants(Class<T> opSet) {
    for (Operation op : opSet.getEnumConstants()) {
        System.out.printf("Operation: %s%n", op);
    }
}

getEnumConstants は Class オブジェクトが Enum 型を表していた場合に、すべての定数の配列を返す。

もし、Enum 型を表さない場合には null が返る。 ポイントは T の制約。

  • Enum を継承していること(= Enum であること)
  • Operation を継承していること

この制約によって、Operation を実装し、Enum であることが保障される。

6. 項目 39 命名パターンよりアノテーションを選ぶ

6.1. 結論

  • 過去、メソッド名にルールをつけることを強制するツールやフレームワークがあった。
  • 現在は、アノテーションによって実現されていることがほとんど。
    • アノテーションを採用するメリットは大きい。

6.1.1. 命名パターンとはなにか

JUnit 3.x では、 テストメソッドは test から始める必要があった(JUnit4以降、アノテーションで指定するようになった)。

一言でいうと、命名パターンはミスしやすく、後から確認しにくい、という致命的なデメリットがある。 細かく言うと以下があがる。

  • コンパイラが間違いを判断できない
  • パラメータを関連付けにくい
  • 利用箇所を限定できない
  • アノテーションによる指定の例

6.1.2. JUnit5の @Test の実装。

1
2
3
4
5
6
7
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = STABLE, since = "5.0")
@Testable
public @interface Test {
}

6.1.3. 使うときの例

1
2
3
4
5
6
class SampleTest {
    @Test
    void plus() {
        assertThat(1+2).isEqualTo(3);
    }
}

6.2. 値アノテーション

アノテーション宣言時に値を付与することで、より柔軟な宣言が実現できる。

1
2
3
4
5
6
// JUnit4の場合
@Test(expected = IllegalArgumentException.class)
public void exception1() {
    Example1 target = new Example1();
    target.execute(null);
}

7. 項目 40 常に Override アノテーションを使う

7.1. 結論

親クラスのメソッドをオーバーライドするときは、 必ず @Override をつける。例外はなし。

7.2. 用途

  • @Override アノテーションが付けられているのに、実際にはオーバーライドしていない場合にはコンパイルエラーとなる
  • オーバーライドするつもりがないのに意図せずオーバーライドしてしまう場合には IDE が警告を出してくれる(ことがある)

8. 項目 41 型を定義するためにマーカーインタフェースを使う

8.1. 結論

マーカーインタフェース、マーカーアノテーションのどちらをつかえばいいか

  • 型を縛る=マーカーインタフェース
  • メタデータを定義する=マーカーアノテーション

Effective Java では、マーカーインタフェースが使える場合はそちらを使うべき とある。

一般的な Java フレームワークでは、アノテーションベースが主流。

8.2. マーカXXXとは

  • マーカーインタフェース
    • メソッドを一つももたないインタフェース
      • ex) Serializable, Cloneable
  • メタデータを定義する=マーカーアノテーション
    • アノテーション自体がマークアップするためのものなので、アノテーションはマーカーアノテーションとして機能する。

8.3. マーカーインタフェース の利点/欠点

  • 利点:: マーカーインタフェースは型なので、コンパイル時にチェックできる。
    • マーカーアノテーションは実行時までチェックできない。
  • 欠点:: マーカーインタフェースはクラス以外をマークできない。
    • メソッドや値をマークするなら、必然的にアノテーションになる。