Java Enumガイド

1.概要

この記事では、Java列挙型とは何か、それらが解決する問題は何か、そして実際に使用できる設計パターンのいくつかを見ていきます。

  • enum キーワードはJava 5で導入されました。** これは常に java.lang.Enum クラスを拡張する特殊なクラスのクラスを表します。使い方の公式ドキュメントはhttp://docs.oracle.com/javase/6/docs/api/java/lang/Enum.html[ドキュメント]をご覧ください。

このように定義された定数は、コードを読みやすくし、コンパイル時のチェックを可能にし、受け入れられた値のリストを事前に文書化し、無効な値が渡されることによる予期しない動作を避けます。

これは、ピザの注文のステータスを定義する列挙型の簡単で簡単な例です。注文ステータスは、 ORDERED READY 、または DELIVERED です。

public enum PizzaStatus {
    ORDERED,
    READY,
    DELIVERED;
}

さらに、それらには多くの便利なメソッドが付属しています。そうでなければ、伝統的なpublic static final constantを使っていたなら自分で書く必要があります。

2カスタムEnumメソッド

これで、列挙型とは何か、またその使用方法について基本的な理解ができたので、列挙型にいくつかの追加のAPIメソッドを定義して、前の例を次のレベルに進めましょう。

public class Pizza {
    private PizzaStatus status;
    public enum PizzaStatus {
        ORDERED,
        READY,
        DELIVERED;
    }

    public boolean isDeliverable() {
        if (getStatus() == PizzaStatus.READY) {
            return true;
        }
        return false;
    }

   //Methods that set and get the status variable.
}

3 「==」演算子を使用した列挙型の比較

enum型はJVM内に存在する定数のインスタンスが1つだけであることを保証するので、上の例で見られるように2つの変数を比較するために“ ==”演算子を安全に使用できます。さらに、 "=="演算子はコンパイル時と実行時の安全性を提供します。

次のスニペットで最初に 実行時の安全性 を見てみましょう。ここでは "=="演算子を使用してステータスを比較し、どちらかの値が null の場合は NullPointerException はスローされません。

反対に、equalsメソッドが使用されている場合は NullPointerException がスローされます。

if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED));
if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED);
  • コンパイル時の安全性** については、 equals メソッドを使用して異なる型のenumが比較される別の例を見てみましょう。論理的には比較は偽になるはずです。この問題は、「==」演算子を使用することで回避できます。

コンパイラーは、比較に互換性のないエラーとしてフラグを立てます。

if(testPz.getStatus().equals(TestColor.GREEN));
if(testPz.getStatus() == TestColor.GREEN);

4 switchステートメントでの列挙型の使用

列挙型は、 switch ステートメントでも使用できます。

public int getDeliveryTimeInDays() {
    switch (status) {
        case ORDERED: return 5;
        case READY: return 2;
        case DELIVERED: return 0;
    }
    return 0;
}

5列挙型のフィールド、メソッド、およびコンストラクタ

あなたはそれを非常に強力にする列挙型の中にコンストラクタ、メソッド、そしてフィールドを定義することができます。

上記の例を拡張し、ピザのある段階から別の段階への移行を実装し、以前に使用した if ステートメントと switch ステートメントをどのようにして取り除くことができるかを確認しましょう。

public class Pizza {

    private PizzaStatus status;
    public enum PizzaStatus {
        ORDERED (5){
            @Override
            public boolean isOrdered() {
                return true;
            }
        },
        READY (2){
            @Override
            public boolean isReady() {
                return true;
            }
        },
        DELIVERED (0){
            @Override
            public boolean isDelivered() {
                return true;
            }
        };

        private int timeToDelivery;

        public boolean isOrdered() {return false;}

        public boolean isReady() {return false;}

        public boolean isDelivered(){return false;}

        public int getTimeToDelivery() {
            return timeToDelivery;
        }

        PizzaStatus (int timeToDelivery) {
            this.timeToDelivery = timeToDelivery;
        }
    }

    public boolean isDeliverable() {
        return this.status.isReady();
    }

    public void printTimeToDeliver() {
        System.out.println("Time to delivery is " +
          this.getStatus().getTimeToDelivery());
    }

   //Methods that set and get the status variable.
}

以下のテストスニペットは、これがどのように機能するかを示しています。

@Test
public void givenPizaOrder__whenReady__thenDeliverable() {
    Pizza testPz = new Pizza();
    testPz.setStatus(Pizza.PizzaStatus.READY);
    assertTrue(testPz.isDeliverable());
}

6. EnumSet EnumMap

6.1. EnumSet

EnumSet は、 Enum 型と共に使用することを意図した特殊な Set 実装です。

内部の Bit Vector Representation が使用されているため、 HashSet と比較すると、 Enum 定数の特定の Set を非常に効率的かつコンパクトに表現できます。そしてそれは、伝統的な int ベースの「ビットフラグ」に代わるタイプセーフな代替手段を提供し、より読みやすく保守しやすい簡潔なコードを書くことを可能にします。

EnumSet は、 RegularEnumSet および JumboEnumSet という2つの実装を持つ抽象クラスです。そのうちの1つは、インスタンス化時の列挙内の定数の数に応じて選択されます。

したがって、ほとんどのシナリオ(サブセット化、追加、削除、および containsAll removeAll などの一括操作など)で列挙型定数のコレクションを処理するときは常にこのセットを使用し、__Enum.values( )すべての可能な定数を反復したいだけならば。

以下のコードスニペットでは、 EnumSet を使用して定数のサブセットを作成する方法とその使用方法を確認できます。

public class Pizza {

    private static EnumSet<PizzaStatus> undeliveredPizzaStatuses =
      EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY);

    private PizzaStatus status;

    public enum PizzaStatus {
        ...
    }

    public boolean isDeliverable() {
        return this.status.isReady();
    }

    public void printTimeToDeliver() {
        System.out.println("Time to delivery is " +
          this.getStatus().getTimeToDelivery() + " days");
    }

    public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
        return input.stream().filter(
          (s) -> undeliveredPizzaStatuses.contains(s.getStatus()))
            .collect(Collectors.toList());
    }

    public void deliver() {
        if (isDeliverable()) {
            PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
              .deliver(this);
            this.setStatus(PizzaStatus.DELIVERED);
        }
    }

   //Methods that set and get the status variable.
}

次のテストを実行すると、 Set インタフェースの EnumSet 実装の強力さが実証されました。

@Test
public void givenPizaOrders__whenRetrievingUnDeliveredPzs__thenCorrectlyRetrieved() {
    List<Pizza> pzList = new ArrayList<>();
    Pizza pz1 = new Pizza();
    pz1.setStatus(Pizza.PizzaStatus.DELIVERED);

    Pizza pz2 = new Pizza();
    pz2.setStatus(Pizza.PizzaStatus.ORDERED);

    Pizza pz3 = new Pizza();
    pz3.setStatus(Pizza.PizzaStatus.ORDERED);

    Pizza pz4 = new Pizza();
    pz4.setStatus(Pizza.PizzaStatus.READY);

    pzList.add(pz1);
    pzList.add(pz2);
    pzList.add(pz3);
    pzList.add(pz4);

    List<Pizza> undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList);
    assertTrue(undeliveredPzs.size() == 3);
}

6.2. EnumMap

EnumMap は、列挙型定数をキーとして使用することを目的とした特殊な Map 実装です。これは、対応する HashMap と比較して効率的でコンパクトな実装であり、内部的に配列として表されています。

EnumMap<Pizza.PizzaStatus, Pizza> map;

実際の使用方法を示す実際の例を簡単に見てみましょう。

public static EnumMap<PizzaStatus, List<Pizza>>
  groupPizzaByStatus(List<Pizza> pizzaList) {
    EnumMap<PizzaStatus, List<Pizza>> pzByStatus =
      new EnumMap<PizzaStatus, List<Pizza>>(PizzaStatus.class);

    for (Pizza pz : pizzaList) {
        PizzaStatus status = pz.getStatus();
        if (pzByStatus.containsKey(status)) {
            pzByStatus.get(status).add(pz);
        } else {
            List<Pizza> newPzList = new ArrayList<Pizza>();
            newPzList.add(pz);
            pzByStatus.put(status, newPzList);
        }
    }
    return pzByStatus;
}

以下のテストを実行すると、 Map インターフェースの EnumMap 実装の強力さが実証されました。

@Test
public void givenPizaOrders__whenGroupByStatusCalled__thenCorrectlyGrouped() {
    List<Pizza> pzList = new ArrayList<>();
    Pizza pz1 = new Pizza();
    pz1.setStatus(Pizza.PizzaStatus.DELIVERED);

    Pizza pz2 = new Pizza();
    pz2.setStatus(Pizza.PizzaStatus.ORDERED);

    Pizza pz3 = new Pizza();
    pz3.setStatus(Pizza.PizzaStatus.ORDERED);

    Pizza pz4 = new Pizza();
    pz4.setStatus(Pizza.PizzaStatus.READY);

    pzList.add(pz1);
    pzList.add(pz2);
    pzList.add(pz3);
    pzList.add(pz4);

    EnumMap<Pizza.PizzaStatus,List<Pizza>> map = Pizza.groupPizzaByStatus(pzList);
    assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1);
    assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2);
    assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1);
}

7. Enumを使ってデザインパターンを実装する

7.1. シングルトンパターン

通常、シングルトンパターンを使用してクラスを実装することは非常に簡単です。列挙型はシングルトンを実装するための簡単で素早い方法を提供します。

それに加えて、enumクラスは内部で Serializable インターフェースを実装しているので、クラスはJVMによってシングルトンであることが保証されています。

以下のコードスニペットでは、シングルトンパターンをどのように実装できるかを見ています。

public enum PizzaDeliverySystemConfiguration {
    INSTANCE;
    PizzaDeliverySystemConfiguration() {
       //Initialization configuration which involves
       //overriding defaults like delivery strategy
    }

    private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL;

    public static PizzaDeliverySystemConfiguration getInstance() {
        return INSTANCE;
    }

    public PizzaDeliveryStrategy getDeliveryStrategy() {
        return deliveryStrategy;
    }
}

7.2. 戦略パターン

通常、ストラテジーパターンは、異なるクラスによって実装されるインターフェースを持つことによって書かれます。

新しい戦略を追加するということは、新しい実装クラスを追加するということです。列挙型では、これはより少ない労力で達成されます。新しい実装を追加することは、ある実装で別のインスタンスを定義することを意味します。

以下のコードスニペットは、Strategyパターンを実装する方法を示しています。

public enum PizzaDeliveryStrategy {
    EXPRESS {
        @Override
        public void deliver(Pizza pz) {
            System.out.println("Pizza will be delivered in express mode");
        }
    },
    NORMAL {
        @Override
        public void deliver(Pizza pz) {
            System.out.println("Pizza will be delivered in normal mode");
        }
    };

    public abstract void deliver(Pizza pz);
}

次のメソッドを Pizza クラスに追加します。

public void deliver() {
    if (isDeliverable()) {
        PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
          .deliver(this);
        this.setStatus(PizzaStatus.DELIVERED);
    }
}
@Test
public void givenPizaOrder__whenDelivered__thenPizzaGetsDeliveredAndStatusChanges() {
    Pizza pz = new Pizza();
    pz.setStatus(Pizza.PizzaStatus.READY);
    pz.deliver();
    assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED);
}

8. Java 8とEnum

Pizza クラスはJava 8で書き直すことができ、 getAllUndeliveredPizzas() メソッドと groupPizzaByStatus() メソッドがlambdasと Stream APIの使用によって非常に簡潔になることがわかります。

public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
    return input.stream().filter(
      (s) -> !deliveredPizzaStatuses.contains(s.getStatus()))
        .collect(Collectors.toList());
}
public static EnumMap<PizzaStatus, List<Pizza>>
  groupPizzaByStatus(List<Pizza> pzList) {
    EnumMap<PizzaStatus, List<Pizza>> map = pzList.stream().collect(
      Collectors.groupingBy(Pizza::getStatus,
      () -> new EnumMap<>(PizzaStatus.class), Collectors.toList()));
    return map;
}

9. EnumのJSON表現

Jacksonライブラリを使用すると、あたかもそれらがPOJOであるかのようにenum型のJSON表現を持つことが可能です。以下のコードスニペットは、同じものに使用できるJacksonの注釈を示しています。

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum PizzaStatus {
    ORDERED (5){
        @Override
        public boolean isOrdered() {
            return true;
        }
    },
    READY (2){
        @Override
        public boolean isReady() {
            return true;
        }
    },
    DELIVERED (0){
        @Override
        public boolean isDelivered() {
            return true;
        }
    };

    private int timeToDelivery;

    public boolean isOrdered() {return false;}

    public boolean isReady() {return false;}

    public boolean isDelivered(){return false;}

    @JsonProperty("timeToDelivery")
    public int getTimeToDelivery() {
        return timeToDelivery;
    }

    private PizzaStatus (int timeToDelivery) {
        this.timeToDelivery = timeToDelivery;
    }
}

PizzaとPizzaStatusは次のように使用できます。

Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
System.out.println(Pizza.getJsonString(pz));

__Pizza __sステータスの次のJSON表現を生成します。

{
  "status" : {
    "timeToDelivery" : 2,
    "ready" : true,
    "ordered" : false,
    "delivered" : false
  },
  "deliverable" : true
}

列挙型のJSONシリアライズ/デシリアライズ(カスタマイズを含む)についての詳細は、リンクを参照してください。

10.結論

この記事では、言語の基礎から、より高度で興味深い現実世界のユースケースまで、Javaのenumを調べました。

この記事のコードスニペットはhttps://github.com/eugenp/tutorials/tree/master/core-java-lang-syntax[Github]リポジトリにあります。