🍇

15.1 列挙型(定数に対する優位性など)~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

15.1 列挙型

チャプターの概要

このチャプターでは、「静的な値」の組み合わせを管理するための特殊な型である列挙型について学びます。

15.1.1 列挙型の基本

列挙型とは

列挙型とは、何らかの「静的な値」の組み合わせを効率的に管理するための仕組みで、Javaに言語仕様として備わっています。
一般的には、定数によって何らかのコード値を定義するようなケースで、それを置き換えるために利用します。
列挙型はクラスやインタフェースと同じように、独立した型として扱われます。
列挙型はenumキーワードを使って以下のように宣言します。

【構文】列挙型の宣言
enum 列挙型名 {
    列挙子1, 列挙子2, 列挙子3, ....
}

列挙型のブロックには、カンマで区切って複数の列挙子を記述します。
列挙子とは列挙型が持つ個々の値のことで、列挙型におけるメンバーとして扱われます。
また列挙型は、他のクラスからは1つの型として扱われるため、クラスのフィールドやローカル変数の型として列挙型を使うことができます。
列挙型のそれぞれの列挙子には、以下のようにしてアクセスします。

【構文】列挙子へのアクセス
列挙型名.列挙子

列挙型の具体例

それでは列挙型を具体的に見ていきましょう。
このレッスンでは、これまでに何度も登場したECサイトにおける顧客種別を例として用います。顧客が、一般会員、ゴールド会員、プラチナ会員、ダイヤモンド会員という4つの種別に分かれているものとします。
まずこれを定数で表すと、Customerクラスなど任意のクラス内のフィールドとして、以下のように宣言することになるでしょう。

snippet
public static final int GENERAL_CUSTOMER = 0;
public static final int GOLD_CUSTOMER = 1;
public static final int PLATINUM_CUSTOMER = 2;
public static final int DIAMOND_CUSTOMER = 3;

顧客種別を定数で表す場合、それぞれの種別に対するコード値が必要ですが、ここではint型の0、1、2、3という値を設定します。
今度はこれをCustomerTypeという列挙型で表すと、以下のようになります。

pro.kensait.java.basic.lsn_15_1_1.CustomerType
enum CustomerType {
    GENERAL, GOLD, PLATINUM, DIAMOND
}

CustomerType列挙型のコードブロック内には、コード値の代わりに列挙子によって顧客種別を定義します。具体的には、GENERAL、GOLD、PLATINUM、 DIAMONDといった列挙子を、カンマ区切りで定義しています。このようにして作成されたCustomerType列挙型は、他のクラスからは1つの型として扱われるため、フィールドやローカル変数の型として使うことができます。
4つある列挙子のうち、例えば一般会員であれば、以下のように列挙型のメンバーとしてアクセスします。

snippet
CustomerType ct = CustomerType.GENERAL; // 一般会員

取得された列挙子は、通常の変数と同じように扱うことが可能です。この場合、変数の型はCustomerType型になります。

列挙型の修飾子

列挙型もクラスと同様に、その性質を表すために特定の修飾子を付与することがあります。
修飾子はenumキーワードの前に記述します。
列挙型に付与可能な修飾子は、以下の二種類です。

  • public … すべてのクラスからアクセス可能であることを表す
  • strictfp … 浮動小数点の演算が実行環境に依存しないことを表す(本コースでは対象外)

定数に対する優位性

チャプター8.2で説明したように、定数には以下のようなメリットがあります。

  • 可読性の向上
  • 保守性の向上
  • 安全性の確保

列挙型にも、定数と同様のメリットがあります。加えて列挙型を利用すると、定数よりもさらにもう一段、コードをタイプセーフにすることができます。例えばECサイトの顧客種別を定数で表す場合、顧客種別は4種類なので0から3までのコード値を取ります。このとき、もし何らかのクラスのコンストラクタやメソッドが引数として顧客種別を受け取るような場合、コンパイラでは値の有効性まではチェックできません。すなわち存在しないコード値である4を、コンストラクタやメソッドに渡すことができてしまいます。
また詳細は後述しますが、列挙型にはswitch-case文による条件分岐と相性が良い、という特徴があります。switch-case文の式に列挙型を指定すると、すべての列挙子が網羅的に実装されていることを、コンパイラにチェックさせることができます。
このような観点から、昨今では何らかの「静的な値」の組み合わせを実装する場合、定数よりも列挙型を利用するケースが一般的です。

15.1.2 列挙型の様々な機能

列挙型に定義されたAPI

列挙型は広い意味でのクラスの一種であり、java.lang.Enumクラスを暗黙的に継承しています。そして列挙型に記述されたそれぞれの列挙子は、Enumクラスのインスタンスとして扱われます。
java.lang.Enumクラスには、主に以下のようなAPIがあります。これらのAPIのうち、インスタンスメソッドはそれぞれの列挙子が暗黙的に保持しています。またスタティックメソッドは、列挙型そのものに対して呼び出します。

API(メソッド) 説明
String name() 列挙子の名前を返す。
int ordinal() 列挙子の序数(記述した順に0から割り当てられる)を返す。
String toString() 列挙子の文字列表現を返す。
static T[] values() この列挙型に定義された列挙子を、配列で返す。
static T valueOf(String) 指定された名前に対応する列挙子を返す。

これらのAPIを使って、列挙型の挙動を確認してみましょう。
以下のように、既出のCustomerTypeに対していくつかのAPIを呼び出します。

snippet_1 (pro.kensait.java.basic.lsn_15_1_2.Main)
CustomerType ct = CustomerType.DIAMOND; // ダイヤモンド会員
System.out.println(ct.name()); // "DIAMOND"
System.out.println(ct.ordinal()); // 3
System.out.println(ct.toString()); // "DIAMOND"

name()メソッドを呼び出すと、列挙子の名前が文字列としてそのまま返されます。
ordinal()メソッドを呼び出すと、列挙子が記述された順に割り当てられる序数が返されます。ダイヤモンド会員は4番目に記述していますので、序数3が割り当てられます。
toString()メソッドを呼び出すと、列挙子の文字列表現が返されますが、このメソッドはオーバーライドして使うケースが一般的です。このメソッドをどのようにしてオーバーライドするのか、その方法については後述します。

列挙型を利用したクラス設計

ここでは、列挙型を利用したクラス設計を具体例で説明します。
列挙型は、クラスの属性として使われるケースが一般的です。例えば顧客を表すCustomerクラスがあり、ID、名前、顧客種別という属性を持っているものとすると、以下のようなコードになります。

pro.kensait.java.basic.lsn_15_1_2.Customer
public class Customer {
    // フィールド
    private Integer id; // ID
    private String name; // 名前
    private CustomerType customerType; //【1】顧客種別
    // コンストラクタ
    public Customer(Integer id, String name, CustomerType customerType) {
        this.id = id;
        this.name = name;
        this.customerType = customerType;
    }
    // アクセサメソッド
    ........
    public CustomerType getCustomerType() {
        return customerType;
    }
    public void setCustomerType(CustomerType customerType) {
        this.customerType = customerType;
    }
}

顧客種別を表すcustomerTypeフィールドは、CustomerType列挙型として定義します【1】。このクラスでは、customerTypeフィールドの値をコンストラクタで初期化し、アクセサメソッドによって値の取得または設定を可能にしていますが、このあたりは通常のクラスと同じ考え方です。
このようにして作成したCustomerクラスは、以下のようにコンストラクタを呼び出して、インスタンスを生成します。ここではプラチナ会員のインスタンスを生成するため、第三引数に、CustomerType.PLATINUMという列挙子を指定しています。

snippet_2 (pro.kensait.java.basic.lsn_15_1_2.Main)
Customer customer = new Customer(1, "Alice", CustomerType.PLATINUM);

また生成されたCustomerインスタンスに対して「それがプラチナ会員かどうか」を判定するためのif文は、以下のようになります。

snippet_3 (pro.kensait.java.basic.lsn_15_1_2.Main)
if (customer.getCustomerType() == CustomerType.PLATINUM) { .... }

列挙型のインスタンスは一意であることが保証されているため、等価性の判定には==演算子を使用することができます。

また「ゴールド会員以上かどうか」を判定するのであれば、以下のようにordinal()メソッドを使う方法があります。

snippet_4 (pro.kensait.java.basic.lsn_15_1_2.Main)
if (CustomerType.GOLD.ordinal() <=
        customer.getCustomerType().ordinal()) { .... }

この例では、この顧客はプラチナ会員だとします。するとゴールド会員の序数1に対してプラチナ会員の序数2の方が大きいため、この条件式はtrueを返します。ただしordinal()メソッドの大小比較による条件分岐は、列挙子の記述順が業務仕様に即していることが前提になりますので、注意してください。

列挙型と条件分岐

列挙型には「switch-case文による条件分岐と相性が良い」という特徴があります。
switch-case文の式に列挙型を指定すると、すべての列挙子が網羅的に実装されていることを、コンパイラにチェックさせることができます。
例えばECサイトの例において、顧客種別に応じて配送料が決まるものとします。具体的には、一般会員またはゴールド会員の場合は配送料が900円、プラチナ会員の場合は600円、そしてダイヤモンド会員の場合は無料、という仕様を前提に考えます。
これをswitch-case文と列挙型によって実装すると、以下のようになります。

snippet (pro.kensait.java.basic.lsn_15_1_2.Main_Switch)
Customer customer = new Customer(1, "Alice", CustomerType.PLATINUM);
int deliveryFee = 0;
switch (customer.getCustomerType()) { // 式に列挙型を指定
case GENERAL, GOLD: // 一般会員またはゴールド会員を表すラベル
    deliveryFee = 900;
    break;
case PLATINUM: // プラチナ会員を表すラベル
    deliveryFee = 600;
    break;
case DIAMOND: // ダイヤモンド会員を表すラベル
    deliveryFee = 0;
    break;
}

このように、case文のラベルには列挙子を直接指定することができます。また4つある列挙子がすべてのcase文で網羅的に指定されていないと、コンパイルエラーが発生します。

15.1.3 列挙型の応用

列挙型へのメンバー追加

列挙型には、クラスと同じようにフィールド、コンストラクタ、メソッドといったメンバーを定義することができます。
その場合、以下ような構文になります。

【構文】列挙型の宣言
enum 列挙型名 {
    列挙子1(初期値), 列挙子2(初期値), 列挙子3(初期値), ....;
    ....フィールド....
    ....コンストラクタ....
    ....メソッド....
}

このようにしてメンバーを追加すると、列挙型の応用範囲が大きく広がります。ここでは既出のCustomerType列挙型に、フィールド、コンストラクタ、メソッドを追加します。
以下のコードを見てください。

pro.kensait.java.basic.lsn_15_1_3.CustomerType
public enum CustomerType {
    //【1】列挙子
    GENERAL("一般会員"),
    GOLD("ゴールド会員"),
    PLATINUM("プラチナ会員"),
    DIAMOND("ダイヤモンド会員");
    //【2】フィールド
    private final String customerType;
    //【3】コンストラクタ
    CustomerType(String customerType) {
        this.customerType = customerType;
    }
    //【4】メソッド
    @Override
    public String toString() {
        return customerType;
    }
}

まず列挙子を見てください【1】。それぞれの列挙子を宣言する箇所で、( )で引数を指定しています。1つ1つの列挙子がEnumクラスのインスタンスになるわけですが、このように記述すると指定された値がコンストラクタに渡され、それぞれの列挙子が初期化されます。つまり例えばGENERAL("一般会員")と記述すると、指定された文字列"一般会員"がコンストラクタ【3】に渡され、String型のcustomerTypeフィールド【2】に設定される、という挙動になります。このコードではこのようにして、customerTypeフィールドにそれぞれの列挙子に応じた文字列を設定しています。もちろんそれぞれの列挙子に対して、それ以外の属性を定義することも可能です。
このようにして設定された文字列は、toString()メソッドをオーバーライドして返しています【4】。このコードではCustomerType.GENERAL.toString()を呼び出すと、コンストラクタで設定された"一般会員"という文字列が返されます。

列挙型でのロジック構築

ここでは引き続きECサイトを題材に、列挙型の応用例をさらに深掘りしていきます。
1つ前のレッスンでは、顧客種別に応じた配送料の決定ロジックを、switch-case文によって実装する例を取り上げました。この例のように、列挙子の種類から何らかの値が自動的に決まるのであれば、その値を列挙型の属性として保持してしまう、という実装パターンもあります。
具体的には以下のコード(CustomerType2列挙型)を見てください。

pro.kensait.java.basic.lsn_15_1_3.CustomerType2
public enum CustomerType2 {
    // 【1】列挙子
    GENERAL("一般会員", 900),
    GOLD("ゴールド会員", 900),
    PLATINUM("プラチナ会員", 600),
    DIAMOND("ダイヤモンド会員", 0);
    // フィールド
    private final String customerType;
    private final int deliveryFee; //【2】配送料
    // コンストラクタ
    CustomerType2(String customerType, int deliveryFee) {
        this.customerType = customerType;
        this.deliveryFee = deliveryFee;
    }
    // メソッド
    @Override
    public String toString() {
        return customerType;
    }
    public int getDeliveryFee() {
        return deliveryFee;
    }
}

既出のCustomerType列挙型を修正し、配送料を表すdeliveryFeeフィールドを追加しています【2】。そして列挙子を記述するとき、顧客種別の種類ごとに、配送料をコンストラクタに渡すことで値を設定しています【1】。
このように列挙子の種類から自動的に決まる値がある場合、その値を列挙型の中に直接実装することにより、ロジックの凝集性を高めることができます。

文字列や数値からの列挙子の逆引き

外部から与えられた文字列や数値から、列挙子を逆引きしたいケースがあります。例えばユーザーが入力した値や、データベースに保存された値から、列挙子が決まるようなケースがその典型です。
まず列挙子そのものの名前(例えば既出のCustomerType列挙型であれば"GENERAL"など)から列挙子を特定するためには、EnumクラスのスタティックメソッドであるvalueOf()メソッドを使います。

snippet_1 (java
String customerTypeName = "GENERAL"; // 一般会員
CustomerType ct = // 一般会員の列挙子が決まる
        CustomerType.valueOf(customerTypeName);

このようにすると、"GENERAL"という文字列から、列挙子CustomerType.GENERALを取得することができます。

次に、列挙子の序数(記述した順に0から割り当てられる)から列挙子を特定するためには、以下のようにします。

snippet_2 (java
int customerTypeNum = 1; // ゴールド会員
CustomerType ct = null;
for (CustomerType ct2 : CustomerType.values()) { //【1】
    if (customerTypeNum == ct2.ordinal()) { //【2】
        ct = ct2; // ゴールド会員の列挙子が決まる
        break;
    }
}

列挙型が持つvalues()メソッドは、当該列挙型に定義された列挙子の配列を返します。従ってfor文にこのメソッドを指定することで、CustomerType列挙型の列挙子を取り出しながら、繰り返し処理を行うことができます【1】。そして取り出した列挙子のordinal()メソッドによって序数を取得し、それを指定された数値と突き合わせすることで、列挙子を特定します【2】。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. 列挙型とは何らかの「静的な値」の組み合わせを効率的に管理するための仕組みであること。
  2. 列挙型を宣言する方法について。
  3. 列挙型の修飾子について。
  4. 定数に対する列挙型の優位性について。
  5. 列挙型に定義された、name()やordinal()といったAPIについて。
  6. 列挙型にメンバーを追加することで、ロジックの凝集性を高めることが可能。
  7. 文字列や数値から列挙子を逆引きする方法について。

Discussion