章目次
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
}
}
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);
}
項目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
項目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)
}
項目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)
}
ハッシュ計算分くらいしかパフォーマンスに差は無さそうだし、この場合は素直に Iterable.groupBy
で Map<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)}")
}
項目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()
項目37 型を定義するためにマーカーインタフェースを使用する
概要
マーカーインタフェースとは、メソッド宣言を持たないインタフェースの事である(例: Serializable
)。これはアノテーション同様コードにメタデータをもたせる事になる。
マーカーインタフェースとアノテーション、どちらを使うべきか?
基本的には、型検査の恩恵を受けられるよう、マーカーインタフェースを使うと良い場合が多いが、以下の場合はアノテーションを利用すべきである。
- クラスやインタフェース以外のプログラム要素に対してメタデータを持たせる場合
- 当然インタフェースを使用できない要素にはマーカーインタフェースは使えない
- メタデータを拡張する予定がある場合
- アノテーションであれば、パラメータを追加したりといった拡張が可能
- 一旦実装された後のインタフェースにメソッドを追加するのは、一般に不可能
Kotlin で読む
Kotlin でも Java 同様となる。
可能であればマーカーインタフェース、必要に応じてアノテーションを使うと良いだろう。
おわり
参考資料等
- Kotlin Reference
- Kotlin Language Documentation
- Effective Java 第2版
- Kotlin イン・アクション
- (Blog)Effective Java 読書会 8 日目 「それ enum で出来るよ」