Effective java 輪読会 第6章 項目32-34

Preview:

Citation preview

Effective Java 輪読会Item 32-34

開発部 陳映融 2014/2/5

第6章 enumとアノテーション

項目30 int 定数の代わりに enum を使用する

項目31 序数の代わりにインスタンスフィールドを使用する

項目32 ビットフィールドの代わりに EnumSet を使用する

項目33 序数インデックスの代わりに EnumMap を使用する

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

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

項目36 常に Override アノテーションを使用する

項目37 型を定義するためにマーカーインタフェースを使用する

2

Item 32ビットフィールドの代わりに EnumSet を使用する

集合での列挙型の要素の使用

4

例えば、文字のスタイルを表現する時

従来の方法だと int enum パターンを使用

今頃の Java では EnumSet を使用するべき

// ビットフィールド列挙定数 - 廃れている!public class Text {

public static final int STYLE_BOLD = 1 << 0; // 1public static final int STYLE_ITALIC = 1 << 1; // 2public static final int STYLE_UNDERLINE = 1 << 2; // 4public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8

// パラメータは、0 個以上の STYLE_ 定数のビットpublic void applyStyles(int styles) { ... }

}

// EnumSet - ビットフィールドの最新の置換public class Text {

public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

// どんな Set でも渡せるが、 EnumSet が明らかに最善public void applyStyles(Set<Style> styles) { ... }

}

従来の方法の場合 int enum パターン使用

ビット演算で集合操作を効率よく行うことができる

和集合(要素追加): A | B

共通集合(要素存在確認): A & B

要素削除: A & (~B)

要素数が多くなると、数値の解釈が困難

例えば最大 26 個の要素を持つ集合を定義すると

5

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

public static final int ELEM_A = 1 << 0; // 1public static final int ELEM_B = 1 << 1; // 2...public static final int ELEM_Z = 1 << 25; // 2^25 = 33554432 (!)...private int elemSet = 0;...System.out.println(Integer.toBinaryString(elemSet)); // こんなの出されても分からないよ...>_<

従来の方法の場合 int enum パターン使用(続き)

型安全ではない

集合内の要素をイテレートするのは大変

要素数が int や long 型のビット数を超えるとさらに大変…

6

// 枠線設定用定数public static final int UPPER_BORDER = 1 << 4; // 16...text.applyStyles(STYLE_BOLD | UPPER_BORDER); // 関係のない枠線定数を入れても文句を言ってくれない...

// 定数に対応する情報も自前で管理しなければいけないprivate static final String STYLE_NAME[] =

{ "BOLD", "ITALIC", "UNDERLINE", "STRIKETHROUGHT" };...// 要素をイテレートして、対応するスタイル名を出力for (int offset = 0; offset < 4; offset++) {

if ((this.styles & 1 << offset) != 0) {System.out.printf(“%s ”, STYLE_NAME[offset]);

}}

新しい方法の場合 EnumSet使用

型安全、可読性向上

要素数が 64個以下なら、一つの long で表現される

集合操作はビット演算で実装される

要素数が 65 を超えた場合、 longの配列で表現される

要素数が多くても重労働にならない

7

class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {/*** Bit vector representation of this set. The 2^k bit indicates the* presence of universe[k] in this set.*/private long elements = 0L;

class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {/*** Bit vector representation of this set. The ith bit of the jth* element of this array represents the presence of universe[64*j +i]* in this set.*/private long elements[];

まとめ

8

列挙型が集合内で使用されるだけでは、ビットフィールドで表現する理由にならない

EnumSet はビットフィールドの簡潔性とパフォーマンスと、enum 型の多くの利点を持っている

項目30 「int 定数の代わりに enum を使用する」 を参照

EnumSet の短所は、不変な EnumSet を生成できない

Java7 でも Collections.unmodifiableSet でラップすることになる

Item 33序数インデックスの代わりに EnumMap を使用する

序数で配列インデックス?(1)

10

料理のハーブを表すクラス Herb

庭園内の植物で、種類ごとのハーブのセットを作りたい

ならば、種類をハーブのセットのインデックスにすればいいじゃない?

public class Herb {public enum Type { ANNUAL, PERENNIAL, BIENNIAL }

private final String name;private final Type type;...

}

...Herb[] garden = ...; // 庭園にあるハーブ

実際作ってみる…

11

セットの配列と ordinal() を使用したインデックスで作ると

enum をキーにして EnumMap で作ると

// 配列をインデックスするのに ordinal() を使用 – これはやってはいけない!

Set<Herb>[] herbsByType = // Type.ordinal() でインデックスされる(Set<Herb>[]) new Set[Type.values().length]; // 配列はジェネリックスと互換性がない、警告が出る

for (int i = 0; i < herbsByType.length; i++)herbsByType[i] = new HashSet<>();

// 配列アクセス時に正しいインデックス値を入れないと ArrayIndexOutOfBoundException をスローしてしまうfor (Herb h : garden)

herbsByType[h.type().ordinal()].add(h);

// データを enum と関連付けるために EnumMap を使用

EnumMap<Type, Set<Herb>> herbsByType =new EnumMap<>(Type.class); // 実行時ジェネリック型情報はキー型の Class オブジェクトで提供

for (Type t : Type.values())herbsByType.put(t, new HashSet<Herb>());

for (Herb h : garden)herbsByType.get(h.type()).add(h); // インデックスの境界に気にする必要ないほか、キーの型チェックもある

序数で配列インデックス?(2)

12

物質の相を表す enum 型クラス Phase

洗練されているように見えるが…

遷移表の検証が困難、項目の追加・削除でバグを織り込みやすい

⇒ 配列で実装される「状態転移表」のメンテナンス性は総じて低い

// 配列の配列をインデックスするのに ordinal() を使用 – これはやってはいけないpublic enum Phase {

SOLID, LIQUID, GAS;public enum Transition {

MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;

// src の序数で行が、dst の序数で列がインデックスされるprivate static final Transition[][] TRANSITIONS = { // 転移表に誤りがあると大変!

{null, MELT, SUBLIME}, // 間違えると、 ArrayIndexOutOfBoundException や{FREEZE, null, BOIL}, // NullPointerException が出るかもしれないし、{DEPOSIT, CONDENSE, null} // 何も出ないでそのまま見逃してしまうかもしれない (!!)

};public static Transition from(Phase src, Phase dst) {

return TRANSITIONS[src.ordinal()][dst.ordinal()];}

}}

EnumMap で作ってみる

13

public enum Phase {SOLID, LIQUID, GAS;public enum Transition {

// 相転移の転移元と転移先の相を定数固有データとして定義MELT(SOLID,LIQUID), FREEZE(LIQUID,SOLID),BOIL(LIQUID,GAS), CONDENSE(GAS,LIQUID),SUBLIME(SOLID,GAS), DEPOSIT(GAS,SOLID);private final Phase src;private final Phase dst;Transition(Phase src, Phase dst) {

this.src = src;this.dst = dst;

}

// 相転移マップを初期化private static final Map<Phase, Map<Phase, Transition>> m = new EnumMap<>(Phase.class);static {

for (Phase p : Phase.values())m.put(p, new EnumMap<Phase, Transition>(Phase.class));

for (Transition t : Transition.values())m.get(t.src).put(t.dst, t);

}

public static Transition from(Phase src, Phase dst) {return m.get(src).get(dst);

}}

}

enum 値を追加する場合の比較

14

相: PLASMA 追加

相転移: IONIZE(GASPLASMA), DEIONIZE(PLASMAGAS) 追加

配列インデックスに ordinal() 使用時

EnumMap 使用時

public enum Transition {MELT(SOLID,LIQUID), FREEZE(LIQUID,SOLID),BOIL(LIQUID,GAS), CONDENSE(GAS,LIQUID),SUBLIME(SOLID,GAS), DEPOSIT(GAS,SOLID), // それぞれの値の情報が独立して、干渉し合わないIONIZE(GAS,PLASMA), DEIONIZE(PLASMA,GAS); // この一行追加で十分...

}

public enum Transition {MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT, IONIZE, DEIONIZE;private static final Transition[][] TRANSITIONS = {

{null, MELT, SUBLIME, null}, {FREEZE, null, BOIL, null},{DEPOSIT, CONDENSE, null, IONIZE}, {null, null, DEIONIZE, null}

}; // 実際の修正場所 PLASMA の序数に依存、間違えると今までの動作もおかしくなる...

}

まとめ

15

配列をインデックスするために序数を使用することが適切であることはめったにない

代わりに EnumMap を使用する

多次元の関係を表示する場合は EnumMap<...,EnumMap<...>> を使用する

Item 34拡張可能な enum をインタフェースで模倣する

enum 型の拡張性について

17

ほとんど場合は、enum 型の拡張性は間違った考え

もし拡張できたとすると

拡張された型の要素が基底型の要素であり

基底型の要素は拡張された型の要素ではない

⇒ 混乱を生じることに

拡張可能な列挙型を使う場面

オペレーションコード

何らかの操作を表す要素を持つ列挙型

API が提供する操作の集合の拡張

⇒ enum が任意のインタフェースを実装できる事実を利用して達成する

項目30 の Operation 型の場合

18

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

操作を模倣するために抽出されたインタフェース定義

public enum Operation {PLUS("+") { 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; } },DEVIDE("/") { public double apply(double x, double y) { return x / y; } };

private final String symbol;Operation(String symbol) { this.symbol = symbol; }@Override public String toString() { return symbol; }

abstract double apply(double x, double y); // 模倣するためにインタフェースとして抽出}

public interface Operation {double apply(double x, double y);

}

enum 型によるインタフェース実装

19

基本 enum 型

模倣された拡張 enum 型

public enum BasicOperation implements Operation {PLUS("+") { 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; } },DEVIDE("/") { 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; }

}

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; }@Override public String toString() { return symbol; }

}

拡張 enum 型の使用

20

インタフェースが期待された場所、拡張 enum 型も使用可能

境界型トークン使用(項目29)

パラメータ opSet の宣言は、その Class オブジェクトが enum でかつOperation のサブタイプであることを保証

境界ワイルドカード型使用(項目28)

この場合、パラメータ opSet の要素は Operation のサブタイプであることが要求されるが、要素が enum でなくてもよい

その代わり、操作の実装に EnumSet と EnumMap は使用できない

⇒ 複数の実装型を組み合わせる必要がなければ、境界型トークンが良さそう

private static <T extends Enum<T> & Operation> void test(Class<T> opSet, double x, double y) {for (T op : opSet.getEnumConstants())

System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));}

private static void test(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));}

インタフェースで模倣する手法の欠点

21

一つの enum 型が他の enum 型から実装を継承できない

Operation 型の場合は操作に関連付けられた記号の保存と取り出しのロジックが重複

重複するコード量が大きい場合、ヘルパークラスや static なヘルパーメソッドでカプセル化できる

public enum BasicOperation implements Operation {...private final String symbol;BasicOperation(String symbol) { this.symbol = symbol; }@Override public String toString() { return symbol; }

}

public enum ExtendedOperation implements Operation {...private final String symbol;ExtendedOperation(String symbol) { this.symbol = symbol; } // 実装を継承できないのでコードが重複@Override public String toString() { return symbol; } // 実装を継承できないのでコードが重複

}

まとめ

22

拡張可能な enum 型を書くことできないが

基本の enum 型に伴うインタフェースを書いて、そのインタフェースをその基本の enum 型に実装させることで模倣できる

クライアントがそのインタフェースを実装して独自の enum を作れる

API がインタフェースで書かれたとしたら、基本の enum 型を使用する場所でもそれらの enum 型を使用できる