Effective Java 第3版を読んだので、本書で紹介されていた全90項目のプラクティスを簡潔にまとめる。
https://www.maruzen-publishing.co.jp/item/?book_no=303408

本記事は第6章「enumとアノテーション」の項目について記載する。

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

項目34 int定数の代わりにenumを使う

Javaにenum型が提供されるまでは、int enumパターンという技法を用いていた。

public static final int JANUARY = 1;
public static final int FEBRUARY = 2;
public static final int MARCH = 3;

ただ、この技法には多くの欠点が存在する。

  • 型安全性を提供しない
  • 表現力・可読性が悪い
  • 変更に弱い

また、同様にString enumパターンも存在するが、これも望ましくない。
そこで、これらの欠点を解決することのできるenum型を利用するべき。

enumはコンパイル時の型安全性を提供している。
誤った型の値を渡したり、異なるenum型同士を比較しようとするとコンパイルエラーとなる。

enumを採用する場面は、コンパイル時に要素が明確な定数の集合が必要な場合。

定数固有メソッド実装

各enum定数に、異なる振る舞いを関連付ける方法。
enum型で抽象メソッドを宣言して、定数固有クラス本体で定数ごとにオーバーライドする。

public enum 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;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        // デフォルトのtoStringは列挙子名を返却する。
        return symbol;
    }

    public abstract int apply(int x, int y);
}

項目35 序数の代わりにインスタンスフィールドを使う

enumはordinalという序数を返すメソッドを持っている。

この序数をEnumの列挙子に関連付けたint値で利用したい場合、ordinalメソッドは使うべきではない。

なぜなら、保守性が悪く、定数の順序が変更された場合などに対応が難しくなるから。

そこで、ordinalメソッドの代わりにインスタンスフィールドに値を保存する手法を取るのが良い。

public enum Number {
    ONE(1),
    TWO(2),
    THREE(3),
    ONE_HUNDRED(100);

    private final int numberString;

    Number(int symbol) {
        this.numberString = symbol;
    }
}

項目36 ビットフィールドの代わりにEnumSetを使う

ビットフィールドとは、各定数に集合操作をビット演算で実装する手法である。

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パターンを用いるので短所が多い。

そこで、値の集合を効率的に表現することのできるEnumSetを使うのが良い。

public class Text {
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
    public void applyStyles(Set<Style> styles) {}
}

クライアント側では、EnumSet<Style>インスタンスを引数に指定して利用する。

text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

applyStylesの引数の型がSet<Style>なのは、実装型ではなくインタフェース型で受け付けるようにして、汎用性を広げるためである。

項目37 序数インデックスの代わりにEnumMapを使う

enumの序数でインデックスされている配列は使うべきでない。

そこで、EnumMapというenumをキーで利用できるクラスを使うべきである。

public class Animal {
    enum AnimalType {
        MAMMALIAN, // 哺乳類
        REPTILES, // 爬虫類
        AMPHIBIANS // 両生類
    }

    private final String name;
    private final AnimalType animalType;

    Animal(String name, AnimalType animalType) {
        this.name = name;
        this.animalType = animalType;
    }

    public AnimalType getAnimalType() {
        return animalType;
    }

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

    // 種別ごとに動物をまとめる
    public static void main(String[] args) {

        // 動物と種別のリスト
        List<Animal> animals = List.of(
                new Animal("human", Animal.AnimalType.MAMMALIAN),
                new Animal("bear", Animal.AnimalType.MAMMALIAN),
                new Animal("snake", Animal.AnimalType.REPTILES));

        Map<Animal.AnimalType, Set<Animal>> animalByAnimalType = new EnumMap<>(Animal.AnimalType.class);

        for (Animal.AnimalType animalType : Animal.AnimalType.values()) {
            animalByAnimalType.put(animalType, new HashSet<>());
        }

        for (Animal animal : animals) {
            animalByAnimalType.get(animal.getAnimalType()).add(animal);
        }

        System.out.println(animalByAnimalType);
        // {MAMMALIAN=[human, bear], REPTILES=[snake], AMPHIBIANS=[]}
    }
}

序数でインデックスされている配列に比べ、EnumMapは動作速度に遜色がない。

なぜなら、EnumMapが内部的に配列を使っているから。

また、このコードはストリームを使ってさらに簡潔に書くことができる。

public class No37 {

    public static void main(String[] args) {

        List<Animal> animals = List.of(
                new Animal("human", Animal.AnimalType.MAMMALIAN),
                new Animal("bear", Animal.AnimalType.MAMMALIAN),
                new Animal("snake", Animal.AnimalType.REPTILES));

        System.out.println(animals.stream().collect(groupingBy(animal -> animal.getAnimalType(),
                () -> new EnumMap<>(Animal.AnimalType.class), toSet())));
        // => {MAMMALIAN=[human, bear], REPTILES=[snake]}
    }
}

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

拡張可能なenumを実装することはできないが、インタフェースを実装することで模倣できる。

pubilc interface Operation {
    int apply(int x, int y);
}
public 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;
    }
}

java.nio.file.LinkOptionでもこの手法が取られている。

package java.nio.file;

/**
 * Defines the options as to how symbolic links are handled.
 *
 * @since 1.7
 */

public enum LinkOption implements OpenOption, CopyOption {
    /**
     * Do not follow symbolic links.
     *
     * @see Files#getFileAttributeView(Path,Class,LinkOption[])
     * @see Files#copy
     * @see SecureDirectoryStream#newByteChannel
     */
    NOFOLLOW_LINKS;
}

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

プログラムが特別な処理をするときに、呼び出すメソッドの判断にメソッド名の命名パターンを用いることがある。

例えば、Junitのリリース4までは、テストメソッドにtestで始まる名前をつけなければテストメソッドとして認識してくれない。

public void testMethod() {}

アノテーションを用いるとこの問題が解決できる。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME) // Testアノテーションが実行時に保持される
@Target(ElementType.METHOD) // Testアノテーションがメソッドの宣言のみに有効である
public @interface Test {
}

このようなアノテーションをクライアント側のメソッドに付与することで、プログラムやフレームワークがテストメソッドとして認識できる。

アノテーション型の宣言に対するアノテーションは、メタアノテーションと呼ばれる。
クライアント側に指定するアノテーションは、マーカーアノテーションと呼ばれる。

また、アノテーションの制限事項をコンパイラが強制することはできないので、アノテーションプロセッサを別途書く必要がある。

項目40 常にOverrideアノテーションを使う

@Overrideアノテーションをメソッドに付与することで、様々な恩恵が受けられる。

  • スーパータイプの宣言をオーバーライドしていることを示せる
  • コンパイラやIDEがスーパータイプのメソッドとシグニチャが異なることを検知できる

したがって、オーバーライドしているメソッドには@Overrideアノテーションを付与するべきである。

例外として、

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

マーカーインタフェースとは、メソッドの宣言を含んでいないインタフェースで、インタフェースを実装しているクラスの属性を表現する際に使われる。

マーカーインタフェースは、マーカーアノテーションにはない2つの長所がある。

  • マークされたクラスのインスタンスが実装している型を定義できる。マーカーアノテーションは、型を定義しない。
  • 正確に対象を特定することができる。

対するマーカーアノテーションの利点は、アノテーションに紐づくフレームワークの一貫性を可能にする。