enum とアノテーション
Effective Java 第 3 版の個人的メモ
- 項目 34 int 定数の代わりに enum を使う
- 項目 35 序数の代わりにインスタンスフィールドを使う
- 項目 36 ビットフィールドの代わりに EnumSet を使う
- 項目 37 序数インデックスの代わりに EnumMap を使う
- 項目 38 拡張可能な enum をインタフェースで模倣する
- 項目 39 命名パターンよりアノテーションを選ぶ
- 項目 40 常に Override アノテーションを使う
- 項目 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
以下の利点がある。
- コンパイル時の型安全性が提供される
- 名前空間を提供する
- 表示可能な文字列に変換できる
- シングルトンである。
- Object の全てのメソッド(第3章)を提供する。
- Comparable(項目 12)と Serializable(第 11 章)を実装している。
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 を生成できないことがあげられる
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. マーカーインタフェース の利点/欠点
- 利点:: マーカーインタフェースは型なので、コンパイル時にチェックできる。
- マーカーアノテーションは実行時までチェックできない。
- 欠点:: マーカーインタフェースはクラス以外をマークできない。
- メソッドや値をマークするなら、必然的にアノテーションになる。