Java Enumガイド

Java列挙のガイド

1. 概要

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

The enum keyword was introduced in Java 5.常にjava.lang.Enumクラスを拡張する特別なタイプのクラスを示します。 それらの使用法に関する公式ドキュメントについては、documentationを参照してください。

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

これは、ピザの注文のステータスを定義する列挙型の簡単で簡単な例です。注文ステータスは、ORDEREDREADY、またはDELIVEREDのいずれかになります。

public enum PizzaStatus {
    ORDERED,
    READY,
    DELIVERED;
}

さらに、多くの便利なメソッドが付属しているため、従来のパブリックな静的最終定数を使用している場合は、自分で記述する必要があります。

2. カスタム列挙型メソッド

さて、列挙型とは何か、そしてそれらをどのように使用できるかについての基本的な理解ができたので、列挙型にいくつかの追加の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. 「==」演算子を使用した列挙型の比較

列挙型はJVMに定数のインスタンスが1つだけ存在することを保証するため、「==」演算子を使用して2つの変数を安全に比較できます。さらに、「==」演算子は、コンパイル時および実行時の安全性を提供します。

まず、次のスニペットでat run-time safetyを見てみましょう。ここでは、「==」演算子を使用してステータスを比較し、どちらかの値がnullの場合、NullPointerExceptionはスローされません。 逆に、equalsメソッドが使用された場合、NullPointerExceptionがスローされます。

if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED));
if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED);

compile time safetyについては、equalsメソッドを使用して異なるタイプの列挙型が真であると判断される別の例を見てみましょう。列挙型とgetStatusの値がsメソッドは偶然にも同じですが、論理的には比較は誤りであるはずです。 この問題は、「==」演算子を使用することで回避されます。

コンパイラは、比較に非互換性エラーとしてフラグを付けます。

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は、RegularEnumSetJumboEnumSetと呼ばれる2つの実装を持つ抽象クラスであり、インスタンス化時の列挙型の定数の数に応じて1つが選択されます。

したがって、ほとんどのシナリオ(サブセット化、追加、削除、containsAllremoveAllなどの一括操作など)で列挙型定数のコレクションを操作する場合は、常にこのセットを使用することをお勧めします。 s)可能なすべての定数を繰り返し処理する場合は、Enum.values()を使用します。

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

public class Pizza {

    private static EnumSet 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 getAllUndeliveredPizzas(List 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 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 undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList);
    assertTrue(undeliveredPzs.size() == 3);
}

6.2. EnumMap

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

EnumMap map;

実際にどのように使用できるかを示す実際の例を簡単に見てみましょう。

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

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

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

@Test
public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {
    List 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> 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. 列挙型を使用して設計パターンを実装する

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

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

それに加えて、列挙型クラスは内部で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パターンは、さまざまなクラスによって実装されるインターフェイスを持つことで記述されています。

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

以下のコードスニペットは、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および列挙

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

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

9. EnumのJSON表現

Jacksonライブラリを使用すると、列挙型のJSON表現をあたかもPOJOであるかのように持つことができます。 以下のコードスニペットは、同じために使用できる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));

Pizzasステータスの次のJSON表現を生成するには:

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

列挙型のJSONシリアル化/逆シリアル化(カスタマイズを含む)の詳細については、Jackson – Serialize Enums as JSON Objectsを参照してください。

10. 結論

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

この記事のコードスニペットは、Githubリポジトリにあります。