やり直しJava

列挙型

J2SE 5.0から列挙型が追加されました。列挙型は特殊なクラスで java.lang.Enumクラスのサブクラスとして実現されます。クラスとして実現されているため、型安全であり フィールドやメソッドを追加して機能を拡充することができます。

列挙型の定義と利用

列挙型の定義

列挙型はキーワード「enum」を使って定義します。

enum 列挙名 { 列挙子, 列挙子, ... }
public enum Number {
    ONE, TWO, THREE,
}

上の例ではOne, Two, Threeがそれぞれ列挙子です。列挙子には「public static final」が暗黙に付けられます。命名則としては定数と同様に大文字のsnake_caseとする場合と、クラスなどと同様にCamelCaseにする場合が見受けられます。

列挙型の利用

Number number = Number.ONE;
switch(number) {
    case ONE:
        処理;
        break;
    case TWO:
        処理;
        break;
    case THREE:
        break;
}

列挙子は「列挙名.列挙子」の形で参照しますが、switch文のcase節では列挙子のみで参照することができます。また、staticインポートによって「列挙名」を省略することもできます。

列挙型の特徴、メリット、制限など

列挙型の特徴やメリット、制限などを簡単にまとめます。

  • 各列挙子はpublic static finalなフィールドとして実現されています。
  • 列挙型からサブクラスを作成することはできません。
  • publicなコンストラクタを持つことはできません。
  • 列挙型の変数がnullでなければ、定義した列挙子のいずれかを参照していることが保証されます。
  • 列挙型ごとに異なる名前空間を持ちます。そのため、異なる列挙型で同じ名前の列挙子を定義しても問題ありません。
  • 任意のフィールドやメソッドを追加することができます。また、任意のインタフェースを実装することもできます。
  • Object由来のメソッド(equals()やhashCode()など)を実装していて、ComparableやSerializableインタフェースを実装しています。

列挙型のメソッドとメンバの追加

Javaの列挙型は CやC++と異なり、単純な定数の集まりではなく 実体はクラスです。列挙型は特殊なクラスで、列挙子は不変オブジェクトで実現されています。列挙型のスーパークラスになる抽象クラスjava.lang.Enumが用意されていて、キーワード「enum」で宣言した列挙型はEnumクラスのサブクラスとして実現されます。そのため 通常のクラスと同じようにフィールドやメソッドも持っていますし、任意のフィールドやメソッドを追加したりオーバーライドすることもできます。更に、任意のインタフェースを実装することもできます。

列挙型のメソッド

列挙型はjava.lang.Enumクラスのサブクラスとして実現されます。列挙子は列挙型のクラスフィールドとして実現されていて、コンストラクタをprivateにすることにより 他のクラスからインスタンスを生成できないようにしています。

列挙型は主に次のようなメソッドを持っています。(この他にも Object由来のメソッドやComparableのメソッドも実装しています。)

列挙型の主なメソッド
メソッド概要
String name()列挙子の名前を返します。
int ordinal()列挙子の序数(0オリジンの連番)を返します。
String toString()name()と同じものを返します。オーバーライド可能です。
列挙名[] values()列挙子の配列を返します。

Enumクラスではname(名前)とordinal(序数)というfinalフィールドを持っています。列挙子のインスタンスを生成する際にEnumクラスのコンストラクタに列挙子の名前と序数が渡されてフィールドが初期化されます。

尚、CやC++のように 列挙子の序数を指定することはできず、次のようになります。

  • 序数は0オリジンの連番となり、初期値を指定することはできません。
  • 序数の番号を飛ばしたり、重複した序数を指定するようなことはできません。
  • 列挙子の追加・削除を行うとそれに伴って序数も変更されます。そのため、純粋に列挙子のインデックスが必要な箇所以外では 序数に依存した処理は避けるべきです。

コンストラクタ・フィールド・メソッドの追加

列挙型はEnumのサブクラスであり、任意のコンストラクタ・フィールド・メソッドを追加することができます。また、上の表の中で触れたようにtoString()をオーバーライドすることができます。(name()やordinal()など他のメソッドはfinal修飾子が指定されているためオーバーライドすることはできません。)

// Javaのバージョンを表す列挙型
public enum JavaVer {
    JDK1_0(LocalDate.of(1996, 1, 23)),
    JDK1_1(LocalDate.of(1997, 2, 19)),
    J2SE1_2(LocalDate.of(1998, 12, 8));
    
    // フィールドの追加
    private final LocalDate date;
    
    // コンストラクタの追加
    private JavaVer(LocalDate date) {
        this.date = date;
    }

    // メソッドの追加
    public String getNameAndDate() {
        return name() + ": " + date;  // name()メソッドも使える。
    }
}

public static void main(String[] args) {

    // 使用例
    for(JavaVer javaVer : JavaVer.values()) {
        System.out.println(javaVer.getNameAndDate());  // "JDK1_0: 1996-01-23"
                                                       // "JDK1_1: 1997-02-19"
                                                       // "J2SE1_2: 1998-12-08"
    }
}

列挙型でコンストラクタを追加した場合 暗黙的にスーパークラスのEnumのコンストラクタが呼ばれてnameとordinalが初期化されます。そのためコンストラクタを追加していない場合と同様に name()やordinal()を利用することができます。

列挙子は不変オブジェクトであるため、フィールドを追加する場合は基本的にはfinalにします。

列挙型クラスの初期化では、列挙子のインスタンスが生成されてからクラスフィールドが初期化されます。そのため、列挙型のコンストラクタの中ではクラスフィールドにアクセスすることはできません。(コンパイルエラーになります。)逆に クラスフィールドの初期化やstaticイニシャライザの中では 初期化された列挙子にアクセスすることができます。

列挙子固有のメソッド実装

列挙型では 列挙子ごとにメソッドの振る舞いを定義することができます。これにより 列挙子をStrategyパターンの各Strategyとすることができます。列挙子ごとにメソッドを実装する場合は次のように定義します。

enum 列挙型 {
    列挙子 { 列挙子固有クラス本体 },
    列挙子 { 列挙子固有クラス本体 },
    列挙子 { 列挙子固有クラス本体 };
}
enum BitOperation {
    AND { public int apply(int x, int y) { return x & y;}},
    OR  { public int apply(int x, int y) { return x | y;}},
    XOR { public int apply(int x, int y) { return x ^ y;}};
    
    public abstract int apply(int x, int y);
}

列挙子固有クラス本体を記述することによって、匿名クラスが作成されます。列挙子固有クラス本体を定義しない列挙子は列挙型のインスタンスになり、列挙子固有クラス本体を定義した列挙子は列挙型の匿名サブクラスのインスタンスになります。匿名クラスで列挙型のメソッドをオーバーライドすることにより、列挙子ごとにメソッドの振る舞いを定義できる仕組みになっています。

そのため、列挙子固有クラス本体は 匿名クラスと同じように書くことができ、同じ制限を受けます。任意のフィールドやメソッドを定義することができますが、finalフィールドを除くstaticなメンバを持つことはできません。

列挙型BitOperationではapply()を抽象メソッドにしています。これによって、新たな列挙子を追加した場合に apply()の実装が強制され apply()の実装漏れを防ぐことができます

列挙型にまつわるライブラリ

Java標準ライブラリには、列挙型にまつわる便利なクラスが用意されています。java.util.EnumSetクラスはビットフィールドを表現することができます。また、java.util.EnumMapクラスは列挙子をキーとするMapの実装です。

ビットフィールドを表現するEnumSet

ビットフィールドを表現したい場合には EnumSetを利用することができます。EnumSet自体は抽象クラスでstaticなファクトリメソッド of()、allOf()、noneOf()を通してEnumSetのインスタンスを生成します。staticファクトリメソッドに列挙型のクラスを指定することにより、その列挙型の列挙子を要素に持つSetを作成して返します。列挙子の数が64以下であれば 内部的にはlong値のビットフィールドそのものになるため、ビットフィールドを実装する場合に比べてパフォーマンスに遜色はありません。

EnumSetの生成例は次のようになります。

// 指定したビットをONにしたビットフィールドを作成。Setの要素は指定した列挙子。
EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.READ);

// 全てのビットをONにしたビットフィールドを作成。Setの要素は全ての列挙子。
EnumSet.allOf(StandardOpenOption.class);

// 全てのビットをOFFにしたビットフィールドを作成。Setの要素は空。
EnumSet.noneOf(StandardOpenOption.class);

EnumSetはSetインタフェースを実装しているため、EnumSetを受け取った側はSetインタフェースのメソッドを通して指定された列挙子を取得することができます。

列挙子をキーとするMapの実装:EnumMap

EnumMapは列挙子をキーとするMapの実装です。列挙型のフィールドを持つクラスのオブジェクトを列挙子ごとにグルーピングしたMapが必要な場合や、列挙子のマトリクスを表現するMapが必要な場合に利用することができます。EnumMapは内部的に配列を使っているので、配列で実現した場合と比べてパフォーマンスに遜色はありません。

列挙型のフィールドを持つクラスのオブジェクトの配列から 列挙子ごとにグルーピングしたMapに変換する例を挙げます。フルーツを表すクラスのオブジェクトを色ごとにグルーピングします。Mapへの変換方法には次の2通りがあり、どちらも一長一短があります。

  • Streamを用いてMapの生成とMapへの格納を同時に行う方法
    未使用の列挙子に対するエントリがMapに格納されません。そのため、使用量の面では効率的ではありますが、Map.get()で未使用の列挙子を指定するとnullが返るためnullに対する処理が必要になります。
  • 列挙子を走査して予めMapを作成した後に Mapへの格納を行う方法
    未使用の列挙子に対するエントリもMapに格納されます。そのため、使用量の面では非効率になりますが、Map.get()で未使用の列挙子を指定した場合にも空のSetが返るため 安全に利用できます。
// フルーツを表すクラス。
// 色を表す列挙型のフィールドを持っている。
class Fruit {
    enum Color {
        RED, ORANGE, GREEN, BLUE;
    }
    String name;
    Color color;
    Fruit(String name, Color color) {
        this.name = name;
        this.color = color;
    }
    @Override
    public String toString() {
        return name;
    }
}

// Fruit配列をEnumMapに変換。
public static void main(String[] args) {

    Fruit[] fruitArray = new Fruit[] {
            new Fruit("apple", Color.RED),
            new Fruit("orange", Color.ORANGE),
            new Fruit("melon", Color.GREEN),
            new Fruit("kiwi", Color.GREEN),
            new Fruit("strawberry", Color.RED),
    };

    // Streamを利用する方法。
    {
        // Fruit配列をColorごとにグルーピングしたMapを生成。
        // キー:Colorの列挙子、値:FruitのSet
        Map<Fruit.Color, Set<Fruit>> map = Arrays.stream(fruitArray)
            .collect(
                // 第1引数:Collector collector
                groupingBy(
                    // 第1引数:Function classifier
                    e -> e.color,
                    // 第2引数:Supplier mapFactory
                    () -> new EnumMap<>(Color.class),
                    // 第3引数:Collector downstream
                    toSet()));
        System.out.println(map);
    }
    
    // 予め列挙子を走査してMapを作成する方法。
    {
        // Mapを生成。
        Map<Fruit.Color, Set<Fruit>> map = new EnumMap<>(Color.class);
        for(Color color : Color.values()) {
            map.put(color, new HashSet<>());    // 全てのColor列挙子に対して空のHashMapを作成
        }
        // 配列をMapに格納。
        for(Fruit fruit: fruitArray) {
            map.get(fruit.color).add(fruit);
        }
        System.out.println(map);
    }
}

groupingBy()やtoSet()はjava.util.Collectorsのクラスメソッドで、汎用的な終端操作を提供してくれます。詳しくは「ストリーム」の章の「終端操作」を参照してください。

実行結果は次のようになります。

{RED=[apple, strawberry], ORANGE=[orange], GREEN=[kiwi, melon]}
{RED=[apple, strawberry], ORANGE=[orange], GREEN=[kiwi, melon], BLUE=[]}

未使用の列挙子BLUEに対するエントリが、前者では格納されず 後者では格納されている様子が分かります。

続いて列挙子のマトリックスを表現するMapを作成する例を挙げます。開発元と種類に応じたオフィススイートの製品のマトリックスを作成します。同様に製品の配列をMapに変換するには2通りの方法があります。先の例では列挙子が未使用の場合は空のSetを格納しましたが、こちらの例では列挙子が未使用の場合は空のOptionalを格納しています。

// オフィス製品を表すクラス。
// 開発元と種類を表す2つの列挙型フィールドを持っている。
class Product {
    enum Developer { Microsoft, OpenOffice, Google, IBM; }
    enum Type { Text, SpreadSheet, Presentation, Graphics; }
    
    final String name;
    final Developer developer;
    final Type type;
    
    Product(String name, Developer developer, Type type) {
        this.name = name;
        this.developer = developer;
        this.type = type;
    }
    @Override
    public String toString() {
        return name;
    }    
}

// 配列からEnumMapのマトリクスに変換
public static void main(String[] args) {

    Product[] productArray = new Product[] {
            new Product("Word", Developer.Microsoft, Type.Text),
            new Product("Excel", Developer.Microsoft, Type.SpreadSheet),
            new Product("PowerPoint", Developer.Microsoft, Type.Presentation),
            new Product("Visio", Developer.Microsoft, Type.Graphics),
            new Product("Writer", Developer.OpenOffice, Type.Text),
            new Product("Calc", Developer.OpenOffice, Type.SpreadSheet),
            new Product("Impress", Developer.OpenOffice, Type.Presentation),
            new Product("Draw", Developer.OpenOffice, Type.Graphics),
            new Product("Docs", Developer.Google, Type.Text),
            new Product("Sheets", Developer.Google, Type.SpreadSheet),
            new Product("Slides", Developer.Google, Type.Presentation),
    };

    // Streamを利用する方法。
    {
        Map<Developer, Map<Type, Product>> map = Arrays.stream(productArray)
            .collect(
                // 第1引数:Collector collector
                groupingBy(
                    // 第1引数:Function classifier
                    p -> p.developer,
                    // 第2引数:Supplier mapFactory
                    () -> new EnumMap<>(Developer.class),
                    // 第3引数:Collector downstream
                    toMap(
                        // 第1引数:Function keyMapper
                        p -> p.type,
                        // 第2引数:Funciton valueMapper
                        p -> p,
                        // 第3引数:mergeFunction (未使用)
                        (x, y) -> y,
                        // 第4引数:Supplier mapFactory
                        () -> new EnumMap<>(Type.class))));
        System.out.println(map);
    }            

    //予め列挙子を走査してMapを作成する方法。
    {
        // MapのMapを生成。
        Map<Developer, Map<Type, Optional<Product>>> map = new EnumMap<>(Developer.class);
        for(Developer developer : Developer.values()) {
            map.put(developer, new EnumMap<>(Type.class));
            for(Type type : Type.values()) {
                map.get(developer).put(type, Optional.empty());    // 全てOptional.empty()で初期化
            }
        }
        // 配列をMapに格納。
        for(Product product : productArray) {
            map.get(product.developer).put(product.type, Optional.of(product));
        }
        System.out.println(map);
    }
}

実行結果は次のようになります。

{Microsoft={Text=Word, SpreadSheet=Excel, Presentation=PowerPoint, Graphics=Visio}, OpenOffice={Text=Writer, SpreadSheet=Calc, Presentation=Impress, Graphics=Draw}, Google={Text=Docs, SpreadSheet=Sheets, Presentation=Slides}}
{Microsoft={Text=Optional[Word], SpreadSheet=Optional[Excel], Presentation=Optional[PowerPoint], Graphics=Optional[Visio]}, OpenOffice={Text=Optional[Writer], SpreadSheet=Optional[Calc], Presentation=Optional[Impress], Graphics=Optional[Draw]}, Google={Text=Optional[Docs], SpreadSheet=Optional[Sheets], Presentation=Optional[Slides], Graphics=Optional.empty}, IBM={Text=Optional.empty, SpreadSheet=Optional.empty, Presentation=Optional.empty, Graphics=Optional.empty}}

“IBM”に対するエントリが、前者では格納されず 後者では格納されている様子が分かります。また、”Google”の”Graphics”に対するエントリが、前者では格納されず 後者では格納されている様子が分かります。

列挙子の拡張

列挙型は派生クラスを作成することができません複数の列挙型を同じ型として扱いたい場合は スーパータイプとなるインタフェースを定義します。列挙子を複数の列挙型に分けたい場合や、ユーザに列挙子追加の拡張性を提供する場合などに このパターンが有効です。

メソッドの引数や戻り値を 列挙型のスーパータイプのインタフェースのコレクションや配列とします。これによって 異なる列挙型の列挙子を混在させて受け渡すことができます。渡す側は 任意の列挙子をコレクションや配列に詰め込んで渡し、受け取る側ではコレクションや配列を走査して列挙子に応じた処理を行います。

interface CarMaker {}

enum JpnCarMaker implements CarMaker {
    TOYOTA, HONDA, NISSAN;
}

enum UsCarMaker implements CarMaker {
    GM, FORD, CHRYSLER;
}

// 利用例
public static void main(String[] args) {
    Set<CarMaker> makerSet = new HashSet<>();  // 型引数はスーパータイプのインタフェース
    makerSet.addAll(Arrays.asList(JpnCarMaker.values()));  // 任意の列挙子を指定
    makerSet.addAll(Arrays.asList(UsCarMaker.values()));   // 同様
    
    handleCarMaker(makerSet);
}

public static void handleCarMaker(Set<CarMaker> makerSet) {
    for(CarMaker maker : makerSet) {
        // 処理...
    }
}

上の例ではCarMakerインタフェースを通してJpnCarMakerとUsCarMakerの列挙子を同じ型として扱っています。CarMakerを実装した列挙型を定義することによって、列挙子追加の拡張性を提供することができます

また、任意の列挙子を渡すのではなく、列挙型のクラスを渡して 受け取り側で列挙子を列挙することもできます。

public static void main(String[] args) {
    handleCarMaker(JpnCarMaker.class);
}

public static <T extends Enum<T> & CarMaker> void handleCarMaker(Class<T> cls) {
    for(CarMaker maker : cls.getEnumConstants()) {
        // 処理...
    }
}

ここで渡しているCarMakerインタフェースのクラスリテラル(JpnCarMaker.class)は境界型トークン(bounded type token)と呼ばれます。また、受け取り側では 型Tが列挙型でありCarMakerのサブタイプであると定義しています(T extends Enum & CarMaker)。型Tが列挙型であることから getEnumConstants()がnullでないことを保証しています。また、型TがCarMakerのサブタイプであることから getEnumConstants()の戻り値を キャストなしでCarMakerの変数に格納することができます。