LoginSignup
24
23

More than 5 years have passed since last update.

EffectiveJava読書会6日目 - 第6章 enum とアノテーション

Last updated at Posted at 2014-05-08

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

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

JDK1.5で enum が追加されるまでは、下記のような int enum pattern が主流だったがこれには多くの欠点が、、
java
// The int enum pattern - severely deficient!
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;

  • オレンジとアップルの演算が可能(==比較もなんのその)
  • グループごとの項目の名前衝突を防ぐため接頭辞をつける
  • valueに変更があった場合でも、コンパイルは通るのでそれを利用してる箇所の振る舞いが保障されない
  • デバッグしても数字が見えるだけで役に立たない
  • グループの大きさを調べたり、定数を全て居てレートするのは負荷

じゃあintじゃなくてStringにしたらいいんじゃないのは 大間違い
* 文字列比較依存でパフォーマンス悪いよ
* int同様コンパイル時には振る舞いが担保されない。スペルミスしたら実行時エラー

ってことで enum 使ってね

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

Javaのenum型の基本的な考え方は 単純 です
public staic final なフィールドを通して、ここの列挙定数に対して1つのインスタンスを公開しているクラス

enum のいいところ
* コンパイル時の型安全を保証
* 名前空間を持つので、同一名の定数が共存可能
* Objectの全てのメソッドの高品質な実装の提供
* Comparable, Serializableな全てのメソッドの高品質な実装の提供。toString()で表示可能な文字列に変換できるとか
* 任意のメソッド、フィール追加が可能、インターフェースの実装も可能

enum型にメソッドやフィールドを追加する

太陽系の例
```java
// Enum type with data and behavior
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),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);

private final double mass;           // In kilograms
private final double radius;         // In meters
private final double surfaceGravity; // In m / s^2

// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;

// Constructor
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 surfaceWeight(double mass) {
    return mass * surfaceGravity; // F = ma
}

}
```

enum定数にデータを関連付けるために
1. インスタンスフィールドを宣言
2. データを受け取るコンストラクタを記述して、そのフィールドにデータを保存

toStringを使ってイテレータブルに各要素の値を演算して表示したり、
もちろん toString をオーバーライドすることも可能です

public class WeightTable {
    public static void main(String[] args) {
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight / Planet.EARTH.surfaceGravity();
        for (Planet p : Planet.values())
            System.out.printf("Weight on %s is %f%n",
                    p, p.surfaceWeight(mass));
    }
}
Weight on MERCURY is 66.133672
Weight on VENUS is 158.383926
Weight on EARTH is 175.000000
Weight on MARS is 66.430699
Weight on JUPITER is 442.693902
Weight on SATURN is 186.464970
Weight on URANUS is 158.349709
Weight on NEPTUNE is 198.846116

enum定数と関連付けられた振る舞いは、他のクラス同様、必要最小限のアクセス数に止めるべきだが、特に理由がなければ、enumはトップレベルのクラスであるべき。

上記プラネットの例では、各要素に同じ振る舞いを提供したが、それぞれの要素に対して、違う振る舞いを定義することも。

// Enum type that switches on its own value - questionable
public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;

    // Do the arithmetic op represented by this constant
    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);
    }
}

でも switch使うとthrowなしではコンパイルできないのし、新しい要素を定義して case分を書くのを忘れると恐怖の実行時エラー

抽象メソッドを定義して、それを定数固有クラス本体で具象化しよう
これで要素を追加して振る舞いを実装し忘れることはなくなります。

// Enum type with constant-specific method implementations
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);
}

toStringをオーバーライドしてみるよ

// Enum type with constant-specific class bodies and data
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; }
    };
    private final String symbol;
    Operation(String symbol) { this.symbol = symbol; }
    @Override public String toString() { return symbol; }
    abstract double apply(double x, double y);
}
public static void main(String[] args) {
    double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]);
    for (Operation op : Operation.values())
        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

fromStringを作ろう。何のことでしょう、、後述

// Implementing a fromString method on an enum type
private static final Map<String, Operation> stringToEnum
        = new HashMap<String, Operation>();

static { // Initialize map from constant name to enum constant
    for (Operation op : values())
        stringToEnum.put(op.toString(), op);
}

// Returns Operation for string, or null if string is invalid
public static Operation fromString(String symbol) {
    return stringToEnum.get(symbol);
}
// Enum that switches on its value to share code - questionable
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; // Calculate overtime pay
        switch(this) {
            case SATURDAY: case SUNDAY:
                overtimePay = hoursWorked * payRate / 2;
            default: // Weekdays
                overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
            0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
                break;
        }
        return basePay + overtimePay;
    }
}

戦略enumパターン

本当にやりたいのは、enum定数を追加するごとに、戦略の選択を強制すること

// The strategy enum pattern
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);
    }

    // The strategy enum type
    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);
        }
    }
}
// Switch on an enum to simulate a missing method
public static Operation inverse(Operation op) {
    switch(op) {
        case PLUS: return Operation.MINUS;
        case MINUS: return Operation.PLUS;
        case TIMES: return Operation.DIVIDE;
        case DIVIDE: return Operation.TIMES;
        default: throw new AssertionError("Unknown op: " + op);
    }
}

いつ enum を使用すべき?

固定数の定数が必要な場合には常に使用すべき
惑星、週の曜日、チェスの駒のような「自然な列挙型」や、メニュー上の選択肢、操作コード、コマンドラインのフラグなど、「コンパイル時に全ての可能な値が分かっているもの」
ただし、enum 型定数の集合が必ずしも永遠に固定されている必要はない(状況に応じて要素や振る舞いの追加や修正を加える)

まとめ

  • int 定数に対して enum ははるかに読みやすく、安全で強力である
  • 多くの enum は明示的なコンストラクタやメンバーを必要としないが、その他多くの enum は各定数にデータを関連付けたり、そのデータに依存して振る舞いが変わるメソッドを提供することで恩恵を受けている
  • 単一メソッドに複数の振る舞いを関連付ける場合は、自己の値に対して switch を行うよりは、定数固有メソッドを選択する
  • 複数の enum 定数が共通の振る舞いを共有するのであれば、strategy enum type を検討する

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

多くの enum は必然的に単一の int 値と関連付けられ、全ての enum は ordinal() メソッドを持つが、これは使用しないこと!
* 定数が並び替えられると下記 numberOfMuxsicians() は動作しない
* 既に使用されている int 値に関連付けられている定数を追加できない

// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET,
    SEXTET, SEPTET, OCTET, NONET, DECTET;
    public int numberOfMusicians() { return ordinal() + 1; }
}

序数から enum に関連付けられる値を導かないこと!
インスタンスフィールドを使いましょう

public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);

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

EnumSet (項目32)や EnumMap (項目33) などの汎用の enum に基づくデータ構造のときに限り ordinal() の利用を検討すべし。

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

列挙型の要素が主に集合で使用される場合、各定数に異なる2の累乗を割り当てて int enum type を使うのがJDK1.5以前の従来の方法

// Bit field enumeration constants - OBSOLETE!
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

    // Parameter is bitwise OR of zero or more STYLE_ constants
    public void applyStyles(int styles) { ... }
}

この表現の場合、ビットフィールド定数を1つの集合にまとめるために、ビット和操作を行わせる。
* 例えば集合の和が4なら UNDERLINE のみが含まれる
* 例えば集合の和が13なら、 BOLDとUNDERLINEとSTRIKETHROUGHが含まれる

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

しかしこれは、項目30で述べたようなint enum type の全ての欠点を孕んでいる。
* 定数表示されたときに意味がわからない
* 容易にイテレートできない

そんなときは EnumSet だ

// EnumSet - a modern replacement for bit fields
public class Text {
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

    // Any Set could be passed in, but EnumSet is clearly best
    public void applyStyles(Set<Style> styles) { ... }
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

まとめ

  • 列挙型が集合の中で使用される というだけで、それをビットフィールドで表現する理由はなく、EnumSet クラスがビットフィールドの簡潔性とパフォーマンスに加えて enum 型の多くの利点を組み合わせてくれる。
  • EnumSetの唯一の短所は(JDK1.6の時点では)、不変(final?)なEnumSetを生成できないこと=>これはEnumSetをCollections#unmodifiableSetでラップすることができるが、簡潔性とパフォーマンスが損なわれるえ

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

キーを列挙型にしたMapを使いたい場合は、(HashMapでなく)EnumMapを使うと実行効率が良い。
(列挙子の個数が固定なので、内部的には、序数を使った配列で実装されている)

例えばこんなクラスがあったとして

public class Herb {
    public enum Type { ANNUAL, PERENNIAL, BIENNIAL }
    private final String name;
    private final Type type;

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

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

これはやちゃだめ!

// Using ordinal() to index an array - DON'T DO THIS!
Herb[] garden = ... ;

Set<Herb>[] herbsByType = // Indexed by Herb.Type.ordinal()
        (Set<Herb>[]) new Set[Herb.Type.values().length];
for (int i = 0; i < herbsByType.length; i++)
    herbsByType[i] = new HashSet<Herb>();

for (Herb h : garden)
    herbsByType[h.type.ordinal()].add(h);

// Print the results
for (int i = 0; i < herbsByType.length; i++) {
    System.out.printf("%s: %s%n",
            Herb.Type.values()[i], herbsByType[i]);
}
  • 配列はジェネリクスと互換性がないので(項目25)、無検査キャスト=>コンパイラ警告
  • 配列はインデックスが何を表しているかわからないので、ラベルを手作業(コメント)でつける必要がある
  • 一番の問題は、enum の序数でインデックスされている配列へのアクセスで、正しい int 値を選ぶのはコーダーの責任(not compiler)

そんなときは EnumMap だ!

この後の各相転移はよくわかってないので後述、、

// Using an EnumMap to associate data with an enum
Map<Herb.Type, Set<Herb>> herbsByType =
        new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);
for (Herb.Type t : Herb.Type.values())
    herbsByType.put(t, new HashSet<Herb>());
for (Herb h : garden)
    herbsByType.get(h.type).add(h);
System.out.println(herbsByType);
// Using ordinal() to index array of arrays - DON'T DO THIS!
public enum Phase { SOLID, LIQUID, GAS;
    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;

        // Rows indexed by src-ordinal, cols by dst-ordinal
        private static final Transition[][] TRANSITIONS = {
            { null, MELT, SUBLIME },
            { FREEZE, null, BOIL },
            { DEPOSIT, CONDENSE, null }
        };

        // Returns the phase transition from one phase to another
        public static Transition from(Phase src, Phase dst) {
            return TRANSITIONS[src.ordinal()][dst.ordinal()];
        }
    }
}

```java
// Using a nested EnumMap to associate data with enum pairs
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);

        final Phase src;
        final Phase dst;

        Transition(Phase src, Phase dst) {
            this.src = src;
            this.dst = dst;
        }

    // Initialize the phase transition map
    private static final Map<Phase, Map<Phase,Transition>> m =
            new EnumMap<Phase, Map<Phase,Transition>>(Phase.class);
        static {
            for (Phase p : Phase.values())
                m.put(p,new EnumMap<Phase,Transition>(Phase.class));
            for (Transition trans : Transition.values())
                m.get(trans.src).put(trans.dst, trans);
        }

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

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

// Emulated extensible enum using an interface
public interface Operation {
    double apply(double x, double y);
}

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; }
    },
    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;
    }
}
// Emulated extension enum
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;
    }
}
public static void main(String[] args) {
    double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]);
    test(ExtendedOperation.class, x, y);
}

private static <T extends Enum<T> & Operation> void test(
        Class<T> opSet, double x, double y) {
    for (Operation op : opSet.getEnumConstants())
        System.out.printf("%f %s %f = %f%n",
                x, op, y, op.apply(x, y));
}
public static void main(String[] args) {
    double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]);
    test(Arrays.asList(ExtendedOperation.values()), 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));
}
4.000000 ^ 2.000000 = 16.000000
4.000000 % 2.000000 = 0.000000

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

例えば、JUnitが頭にtestとprefixのついたメソッドをテストメソッドとしてみなしたように、JDK1.5以前は命名パターンが普通だった。
ところがこれは問題を孕んでいる
1. スペルミスしたらSkipされて気づかない
2. 適切なプログラム要素にだけそれらが使用されることを保障しない。例えばクラス名をtestHogeとしてもJUnitは何も文句を言わないがテストを実行もしない
3. プログラム要素にパラメータ値を関連付ける良い方法がない。例えばある特定の例外を起こすテストを実装するとき、その例外はパラメータだが、これをコンパイラレベルで縛れないので、実行時に初めてエラーと遭遇

そんなときはアノテーション

皆さんご存知の @Test はこんな感じ
アノテーション型の宣言に対するアノテーションは meta annotation と呼ばれます。
*

// Marker annotation type declaration
import java.lang.annotation.*;

/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME) // テストを実行時に保持
@Target(ElementType.METHOD)         // メソッドにしかつけられない
public @interface Test {
}

@Test のようにパラメータをもっておらず、単なるマークに使われるものを marker annotation と呼びます。

テストアノテーションがどのように使われるか
(まぁこれはいいでしょう、、)

// Program containing marker annotations
public class Sample {
    @Test public static void m1() { } // Test should pass
    public static void m2() { }
    @Test public static void m3() { // Test Should fail
        throw new RuntimeException("Boom");
    }
    public static void m4() { }
    @Test public void m5() { } // INVALID USE: nonstatic method
    public static void m6() { }
    @Test public static void m7() { // Test should fail
        throw new RuntimeException("Crash");
    }
    public static void m8() { }
}

基本的に、マーカーアノテーションは元のクラスのセマンティックスを変更せず影響を与えない。
この場合は、ある特定の(Test)frameworkがこのメソッドを実行する ということを伝えている。
下記は、そのフレームワーク側の実装例

// Program to process marker annotations
import java.lang.reflect.*;

public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class testClass = Class.forName(args[0]);
        for (Method m : testClass.getDeclaredMethods()) {
            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 exc) {
                    System.out.println("INVALID @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n",
                passed, tests - passed);
    }
}

結果出力

public static void Sample.m3() failed: RuntimeException: Boom
INVALID @Test: public void Sample.m5()
public static void Sample.m7() failed: RuntimeException: Crash
Passed: 1, Failed: 3

特定の例外がスローされた倍威にだけ成功するテストのためのサポートを追加。
例外用の新規アノテーション型を追加

// Annotation type with a parameter
import java.lang.annotation.*;

/**
* Indicates that the annotated method is a test method that
* must throw the designated exception to succeed.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception> value();
}

こんな感じで、命名パターンではできないことがアノテーションならできる。

もはや命名パターンを使用するのは論外

まぁほとんどのプログラマはアノテーションを実装することはないでしょうけど、
ちゃんとJava platformが準備しているアノテーションや、Springなどのフレームワークがサポートしているアノテーションを利用しましょう

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

JDK1.5で追加されたアノテーションのうち最も重要なのが @Override
メソッド宣言にだけ使用可能。メソッドがスーパータイプの宣言をオーバーライドしていることを示し、これを常に使用する事で非道なバグの多くから保護してくれる

ExistingBug.java
// Can you spot the bug?
public class Bigram {
    private final char first;
    private final char second;
    public Bigram(char first, char second) {
        this.first = first;
        this.second = second;
    }
    public boolean equals(Bigram b) {
        return b.first == first && b.second == second;
    }
    public int hashCode() {
        return 31 * first + second;
    }
    public static void main(String[] args) {
        Set<Bigram> s = new HashSet<Bigram>();
        for (int i = 0; i < 10; i++)
            for (char ch = 'a'; ch <= 'z'; ch++)
                s.add(new Bigram(ch, ch));
                System.out.println(s.size());
    }
}

ここでクラスの作者は Object.equals をオーバーライドすることを期待しているように見えるが、実際にはオーバーロードされて期待値と異なる挙動になる。
これをコンパイル時に検知するには次のようにアノテーションをつける必要がある。

@Override public boolean equals(Bigram b) {
    return b.first == first && b.second == second;
}

すると、実行結果は下記となりコンパイル時にエラーを検知できる。

Bigram.java:10: method does not override or implement a method
from a supertype
@Override public boolean equals(Bigram b) {
^

正しくは、
java
@Override public boolean equals(Object o) {
if (!(o instanceof Bigram))
return false;
Bigram b = (Bigram) o;
return b.first == first && b.second == second;
}

スーパークラスの宣言をオーバーライドしている全てのメソッド宣言に対して @Override アノテーションを使用すべき
ただ1つの例外は、抽象クラスでないクラスで抽象メソッドをオーバーライドするとき
 抽象クラスでないクラスで、スーパークラスの抽象メソッドをオーバーライドしていないとコンパイルエラーになるため。

ただし、アノテーションをつけておく分には問題ないし、スーパークラスやスーパーインターフェースに新たなメソッドを誤って追加しないことを保証するために、アノテーションはつけておくべき。

最近のIDEはここら辺ちゃんと検知して警告してくれるし、コードインスペクションである程度オーバーライドすべきメソッドとアノテーションが自動生成される!
(もはや常識ですね)

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

marker interface = メソッド宣言を含んでいないインターフェース
そのインターフェースを実装しているクラスが何らかの特性を持っていることを表す
e.g. java.io.Serializable

marker interface が marker annotation より優れている点2つ

1. マークされたクラスのインスタンスが実装している型を定義する

marker annotation では実行時にしか気づけない問題をコンパイル時に見つけることができる
アンチパターン: ObjectOutputStream#write(Object)の引数型はSerializableであるべきだった
 >Serializableを実装していないObjectに対する呼び出しで、実行時エラーが発生

2. marker interface に比べ、より正確に対象を特定できる

e.g. Set
ここうまく説明できない

marker annotation が marker interface より優れている点

1.アノテーションが実装された後でも、アノテーション型に情報を追加できる。

marker interfaceは一般的なインターフェースがそうであるように一度実装された後にメソッドを追加するのは実質的に不可能

2. 大きなアノテーション機構の一部。様々なプログラム要素のアノテーションを許すフレームワーク内での一貫性を可能に

marker interface と marker annotation をどう使い分けるか?

  1. クラス、インターフェース以外の要素にマーカーを適用したい => annotation
  2. そのマークを持つオブジェクトだけを引数にとるメソッドを書きたい => interface
  3. 1,2以外で マーカーの使用を特定のインターフェースの要素に永久に制限したい? interface + annotation
  4. 将来的な拡張を許したり、既にannotation型を多数利用しているフレームワーク内に新しいマーカーを適合させる => annotation

ある意味、本項目は、項目19の「型を定義したくなければ、インターフェースを使用しない」の逆で、「型を定義したいのであれば、インターフェースを利用しなさい」と述べている。

24
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
23