Help us understand the problem. What is going on with this article?

Effective Java を Kotlin で読む(5):第6章 enum とアノテーション

effectivekotlin.png

章目次

Effective Java を Kotlin で読む(1):第2章 オブジェクトの生成と消滅
Effective Java を Kotlin で読む(2):第3章 すべてのオブジェクトに共通のメソッド
Effective Java を Kotlin で読む(3):第4章 クラスとインタフェース
Effective Java を Kotlin で読む(4):第5章 ジェネリックス
Effective Java を Kotlin で読む(5):第6章 enum とアノテーション 👈この記事
Effective Java を Kotlin で読む(6):第7章 メソッド
Effective Java を Kotlin で読む(7):第8章 プログラミング一般
Effective Java を Kotlin で読む(8):第9章 例外
Effective Java を Kotlin で読む(9):第10章 並行性
Effective Java を Kotlin で読む(10):第11章 シリアライズ

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

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

概要

Java にはリリース1.5より列挙型として enum 型 が導入された。それまでは以下のような int enum パターンが用いられていた。

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

このパターンはいくつかの欠点を持つ。

  • 型安全でない
  • 名前空間を持たない
  • グループをイテレートできない
  • etc...

enum 型はこれら欠点を解消する事に加え、任意のメソッドやフィールドを追加でき、任意のインタフェースを実装する事ができる。

メソッドを追加し、定数固有のメソッドを持たせた例が以下である。

enum Hoge0 {
    A { int hoge(int n) { return n * 2; } },
    B { int hoge(int n) { return n * 2; } },
    C { int hoge(int n) { return n * 3; } };

    abstract int hoge(int n);
}

ここで、 A と B は同じ処理なので switch を用いて共通処理をまとめようとすると、次のようになる。

enum Hoge1 {
    A, B, C;

    int hoge(int n) {
        switch (this) {
            case A: case B: return n * 2;
            case C: return n * 3;
        }
        return -1; // unreachable
    }
}

ただしこの場合、新たな定数を追加した際に忘れず switch に分岐を追加しなければならない等、保守性に難がある。そこで、書籍では 戦略enumパターン を紹介している。以下のようになる。

enum Hoge2 {
    A(Strategy.STRATEGY_1), B(Strategy.STRATEGY_1), C(Strategy.STRATEGY_2);
    private final Strategy strategy;
    Hoge2(Strategy strategy) { this.strategy = strategy; }

    int hoge(int n) { return strategy.hoge(n); }

    private enum Strategy {
        STRATEGY_1 { int s(int n) { return n * 2; } }, STRATEGY_2 { int s(int n) { return n * 3; } };
        abstract int s(int n);
        int hoge(int n) { return s(n); }
    }
}

switch 文よりも簡潔ではないが、より安全で柔軟である。(個人的にはやりすぎに思う…)

enum 型は int enum パターンの欠点を解消し多くの利点を持つ。パフォーマンスもほぼ int 定数と比べて遜色ない。固定数の定数が必要な場合には、基本的に常に enum 型を利用するべきである。

追記
戦略 enum パターンは Java 8 で導入されたメソッド参照・関数インタフェースを用いて以下のように簡潔に書けるようになった!

enum Hoge3 {
    A (Hoge3::strategy1),
    B (Hoge3::strategy1),
    C (Hoge3::strategy2);

    private final Function<Integer, Integer> func;
    Hoge3(Function<Integer, Integer> func) { this.func = func; }

    public int hoge(int n) { return this.func.apply(n); }

    private static int strategy1(int n) { return n * 2; }
    private static int strategy2(int n) { return n * 3; }
}

Kotlin で読む

Kotlin でも列挙型は言語サポートされている。

enum class Direction {
  NORTH, SOUTH, WEST, EAST
}

// こう使う
Direction.NORTH.name // "NORTH"
Direction.SOUTH.ordinal // 1
Direction.values() // {NORTH, SOUTH, WEST, EAST}

Java とほぼ同じだが name()ordinal() がプロパティになっているのが変更点だろうか…

Java のコードを Kotlin に変換する際に getName() のようなメソッドを Java で定義していた場合は少し気をつける必要があるかもしれない。

また、Kotlin の enum は when 式と相性が良い。Java の例で出したクラスを Kotlin で書くと次のようになるだろう。

enum class Hoge {
    A, B, C;
    fun hoge(n: Int): Int = when(this) {
        A, B -> n * 2
        C -> n * 3
    }
}

Try Kotlin で確認

when は Java でいうところの switch であるが、すべての分岐をカバーする必要がある(網羅できていない場合 else 条件が必要)。そのため、新たな定数を追加した場合 when に分岐を追加しなければコンパイルエラーとなる。よって、戦略 enum パターンを Kotlin で使う必要性は無いと思われる。

追記
ラムダ式で書くとさらにスマート!

private fun strategy1(n: Int) = n * 2
private fun strategy2(n: Int) = n * 3
enum class Fuga(val fuga: (Int) -> Int) {
    A(::strategy1),
    B(::strategy1),
    C(::strategy2);
}

Try Kotlin で確認

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

概要

enum 型は ordinal メソッドを持っている。これは列挙宣言での各 enum 定数の位置を返すものであるが、これは EnumSet や EnumMap などの汎用の enum に基づくデータ構造のために設計されているものであり、通常利用すべきではない。

何故なら、以下のような性質があるためである。

  • 並べ替えで順序が変わる
  • 同じ値を割り振れない
  • 値を飛ばすことができない

よって、enum に関連付ける値がある場合は、インスタンスフィールドを用いるべきである。

public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4);

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

Kotlin で読む

Kotlin でも考えは同様。インスタンスフィールドの初期化が Java よりもすっきり書けて気持ち良い。

enum class Ensemble(val numberOfMusicians: Int) {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4);
}

// こう使う
Ensemble.TRIO.numberOfMusicians // 3

Try Kotlin で確認

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

概要

集合の要素として列挙型を利用する場合、従来は各定数に異なる2の累乗を割り当てる int enum パターンが使われてきた。(ビット演算を利用し効率的に操作するため)

public class Text {
    public static final int STYLE_BOLD      = 1 << 0; // 1
    public static final int SYTLE_ITALIC    = 1 << 1; // 2
    public static final int STYLE_UNDERLINE = 1 << 2; // 4

    // 例: styles = 5 ならば、 STYLE_BOLD かつ STYLE_UNDERLINE
    public void applyStyles(int styles) { ... }
}

しかし項目30の通り int enum パターンは多くの短所を持っている。代わりに EnumSet を利用するべきである。

EnumSet は Set インタフェースを実装し、かつ内部的にはビット演算を利用している。そのため効率的に集合操作を行うことができる。(唯一の短所は不変な EnumSet を生成できない事)

一応 Collections.unmodifiableSet(myEnumSet); のようにして不変にすることはできるが、簡潔性とパフォーマンスが損なわれる。

Kotlin で読む

Kotlin でも同様。EnumSet を使おう。

また Kotlin であれば不変な EnumSet も簡単に利用可能である。

enum class Style { BOLD, ITALIC, UNDERLINE }

// Set を受け取るようにすることで柔軟性が増す
fun applyStyle(styles: Set<Style>) { println(styles) }

fun main(args: Array<String>) {
    // Set として宣言することで不変にする
    val set: Set<Style> = EnumSet.of(Style.BOLD, Style.UNDERLINE)
    // EnumSet は様々なファクトリーメソッドを持っている
    applyStyle(set)
}

Try Kotlin で確認

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

概要

enum の種類でデータをまとめたい場合、項目31の通り ordinal を使ってはならない。

public class Herb {
    public enum Type {ANNUAL, PERNNIAL, BIENNIAL}
    private final Type type;
    private final String name;
    Herb(Type type, String name) { this.name = name; this.type = type; }
}

Herb[] garden = { ... }; // 様々なハーブ

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 h : garden) herbsByType[h.type.ordinal()].add(h);
// etc: herbsByType[0] -> ANNUAL(1年生植物)なハーブのSet

代わりに EnumMap を使うべきである。

Kotlin で読む

一応 Kotlin も Java 同様 EnumMap を使うべきではあるが…

class Herb(val type: Type, val name: String) {
    enum class Type {ANNUAL, PERNNIAL, BIENNIAL}
    override fun toString() = name
}

val garden = arrayOf(
    Herb(Herb.Type.ANNUAL, "a1"),
    Herb(Herb.Type.ANNUAL, "a2"),
    Herb(Herb.Type.BIENNIAL, "b1"),
    Herb(Herb.Type.BIENNIAL, "b2")
) // 様々なハーブ

fun main(args: Array<String>) {
    // Java の例と同じ処理, 出力: EnumMap<Herb.Type, MutableSet<Herb>>
    val map1 = EnumMap<Herb.Type, MutableSet<Herb>>(Herb.Type::class.java)
    Herb.Type.values().forEach { map1[it] = mutableSetOf() }
    garden.forEach { map1[it.type]?.add(it) }
    println(map1)

    // groupBy を使う, 出力: Map<Herb.Type, List<Herb>>
    val map2 = garden.groupBy { it.type }
    println(map2)

    // ※追記※
    // groupByTo を使う, 出力: EnumMap<Herb.Type, MutableList<Herb>>
    val map3 = EnumMap<Herb.Type, MutableList<Herb>>(Herb.Type::class.java)
    garden.groupByTo(map3){ it.type }
    println(map3)
}

Try Kotlin で確認

ハッシュ計算分くらいしかパフォーマンスに差は無さそうだし、この場合は素直に Iterable.groupByMap<Herb.Type, List<Herb>> として分類しても良さそう(あるいは groupByTo を使う)。 EnumMap の Kotlin でのスマートな使い方があれば知りたい。

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

概要

Java において enum 型は拡張する事ができない。ほとんどの場合 enum の拡張は間違った考えであるが、ときに拡張可能な enum を使いたい時がある。その際には、インタフェースを利用して擬似的に enum を拡張する事ができる。

interface Operation {
    int apply(int x, int y);
}

enum BasicOperation implements Operation {
    PLUS("+") {
        public int apply(int x, int y) { return x + y; }
    },
    MINUS("-") {
        public int apply(int x, int y) { return x - y; }
    };
    private final String symbol;
    BasicOperation(String symbol) { this.symbol = symbol; }
    @Override public String toString() { return symbol; }
}

enum ExtendedOperation implements Operation {
    TIMES("*") {
        public int apply(int x, int y) { return x * y; }
    };
    private final String symbol;
    ExtendedOperation(String symbol) { this.symbol = symbol; }
    @Override public String toString() { return symbol; }
}

// こう使う
Operation[] ops = {BasicOperation.PLUS, BasicOperation.MINUS, ExtendedOperation.TIMES};
for (Operation op : ops) {
    System.out.printf("3 %s 2 = %d\n", op, op.apply(3, 2));
}

ただし、実装は継承できないため、コードが重複してしまう問題はある。もしコードが複雑になるようなら、共通処理をヘルパークラスに抽出する等の工夫が必要である。

Kotlin で読む

Kotlin も Java と同様にインタフェースを利用して擬似的に enum を拡張する。

interface Operation {
    fun apply(x: Int, y: Int): Int
}

enum class BasicOperation(private val symbol: String) : Operation {
    PLUS("+") { override fun apply(x: Int, y: Int) = x + y },
    MINUS("-") { override fun apply(x: Int, y: Int) = x - y };

    override fun toString() = symbol
}

enum class ExtendedOperation(private val symbol: String) : Operation {
    TIMES("*") { override fun apply(x: Int, y: Int) = x * y };

    override fun toString() = symbol
}

// こう使う
val ops = listOf<Operation>(BasicOperation.PLUS, BasicOperation.MINUS, ExtendedOperation.TIMES)
ops.forEach {
    println("3 $it 2 = ${it.apply(3, 2)}")
}

Try Kotlin で確認

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

概要

Java では JDK1.5 からアノテーションが導入された。
それ以前は、ソースコードにメタデータを持たせたい場合は命名パターンが使われていた。
(例:JUnit では、テスト対象は名前が test で始まるメソッドであった)

が、この命名パターンは大きな短所を持っている。

  • 誤字を検知できない
    • 例: tsetHogeMethod という名前のメソッドはテストすべきか?
  • メタデータの使用箇所を制限できない
    • 例: testHogeClass という名前のクラスはテストすべきか?

書籍では、アノテーションの利用できる現在において命名パターンを使用するのは論外であると断言している。

Kotlin で読む

Kotlin も勿論アノテーションを利用する。
特に Java からの変更点があるわけではないが、Kotlin のプロパティやコンストラクタパラメータからは対応する Java コードが複数あるので、どの要素に対してアノテーションをつけるか指定する事ができる

class Example(@field:Ann val foo,    // field に対するアノテーション
              @get:Ann val bar,      // getter メソッドに対するアノテーション
              @param:Ann val quux)   // コンストラクタパラメータに対するアノテーション

また Kotlin では生の配列が使えなくなった関係で、アノテーションのパラメータに配列を指定する際少し記述が冗長だった。が、これは Kotlin 1.2 より array literal syntax が使えるようになり解決した

// Kotlin 1.2+:
@Ann(names = ["abc", "foo", "bar"]) 
class Hoge

// Older Kotlin versions:
@Ann(names = arrayOf("abc", "foo", "bar")) 
class Fuga

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

概要

JDK1.5でいくつかのアノテーション型がライブラリに追加された。その中で一般的なプログラマにとって最も重要なのが @Override である。このアノテーションはメソッドに対してのみ利用でき、このアノテーションが付けられたメソッド宣言はスーパータイプの宣言をオーバーライドしていることを示す。

例えば、以下のクラスにはバグが存在する。

public class Bigram {
    public char first, second;
    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;
    }
}

// こう使う
Set<Bigram> set = new HashSet<>();
set.add(new Bigram('a', 'b'));
set.add(new Bigram('a', 'b'));  // 等価なオブジェクトなのでSetのサイズは変わらず1?
System.out.println(set.size()); // -> 2

Object.equals をオーバーライドしたつもりのコードであるが、引数が Object でないので実際にはオーバーロードしてしまっている。

この場合も @Override アノテーションを付けていればコンパイルエラーになるためミスに気がつく事ができる。また、逆に @Override アノテーション無しにオーバーライドしている場合にも IDE が警告を出すため、意図しないオーバーライドにも気がつける。

このように、オーバーライドを意図したコードには常に @Override アノテーションをつけるべきである。

例外として、抽象メソッドをオーバーライドする際やインタフェースを実装する際には @Override アノテーションは任意である。つけなくとも必ず実装しないとコンパイルエラーになるためである(が、付けて害がある訳ではない)。

Kotlin で読む

Kotlin ではオーバーライドの為には override 指定子を用いる。
また、抽象メソッドをオーバーライドする際やインタフェースを実装する際も Java と異なり override をつけることが必須である。

interface InterfaceHoge {
    fun hoge()
}

abstract class AbstractFuga {
    abstract fun fuga()
}

class HogeFuga : InterfaceHoge, AbstractFuga() {
    override fun hoge() = println("hoge")
    override fun fuga() = println("fuga")
}

// こう使う
val hoge: InterfaceHoge = HogeFuga()
val fuga: AbstractFuga = HogeFuga()
hoge.hoge()
fuga.fuga()

Try Kotlin で確認

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

概要

マーカーインタフェースとは、メソッド宣言を持たないインタフェースの事である(例: Serializable)。これはアノテーション同様コードにメタデータをもたせる事になる。

マーカーインタフェースとアノテーション、どちらを使うべきか?

基本的には、型検査の恩恵を受けられるよう、マーカーインタフェースを使うと良い場合が多いが、以下の場合はアノテーションを利用すべきである。

  • クラスやインタフェース以外のプログラム要素に対してメタデータを持たせる場合
    • 当然インタフェースを使用できない要素にはマーカーインタフェースは使えない
  • メタデータを拡張する予定がある場合
    • アノテーションであれば、パラメータを追加したりといった拡張が可能
    • 一旦実装された後のインタフェースにメソッドを追加するのは、一般に不可能

Kotlin で読む

Kotlin でも Java 同様となる。
可能であればマーカーインタフェース、必要に応じてアノテーションを使うと良いだろう。

おわり

参考資料等

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした