enum型をわかりやすく説明する

## はじめに

Javaではenum型というものを使って、定数を定義するらしい。
これまで仕事でちょくちょく使っていたが、知識不足を痛感したので、勉強がてら使い方などをまとめてみる。
enumの定義や基本的な使い方を扱い、忘れたときに参照できるような記事を作成していく。

## enumとは？

``enum``はJava5.0から導入されており、``列挙型``と呼ばれ、関連のある定数(列挙子)をまとめるためのクラスの特殊な形式である。

## 使い方

基本的な使い方としてはまとめたい定数を
**大文字** で定義し、 **カンマ区切り** で列挙していく。

``` java
public enum Drink {
 COLA, CIDER, COFFEE

// また、以下のように改行も可能
public enum Drink {
 COLA, 
 CIDER, 
 COFFEE
}
```

また、それ以外にも以下のようなことが可能である。

1. フィールド、コンストラクタを定義することで、定数に関連する値を紐付けることができる。
2. 定数特有の処理を行うためのクラス本体(クラスを宣言するときに使う{から}までの部分)を定義することができ、オーバーライドで処理を定義する。なお、このクラス本体は定数を取り囲んでいるenum型を継承した匿名クラスとみなされる。


``` java
public enum Drink {
  // 文字列や数値を紐付けることが可能
  COLA("コーラ", 1) ,
  CIDER("サイダー", 2) {
    // enum型に作成したメソッドをオーバーライドする
    @Override
    public String getColor() {
      return "white";
    }
  },
  COFFEE("コーヒー", 3) {
    @Override
    public boolean isSweet() {
      return false;
    }
  };

  // 定数と紐付ける値をフィールドとして定義する。
  private String name;
  private int id;

  // enumのコンストラクタはprivateで宣言する。
  private Drink(String name, int id) {
    this.name = name;
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public int getId() {
    return id;
  }

  // 甘いかを判定するメソッド
  public boolean isSweet() {
    return true;
  }

  // 飲み物の色を出力するメソッド
  public String getColor() {
    return "black";
  }
}
```

``` java
Drink cider = Drink.CIDER;
Drink coffee = Drink.COFFEE;

// ①定数に紐づく値を出力する
System.out.println(cider.getName()); // サイダー
System.out.println(cider.getId()); // 2

// ②クラス本体を定義した定数の処理が他と異なることを確認する。
System.out.println(cider.isSweet()); // true
System.out.println(coffee.isSweet()); // false

System.out.println(cider.getColor()); // white
System.out.println(coffee.getColor()); // black

```

## enumのメリット

### 予期しない値が代入されない

enumが導入される前はプリミティブ型(もしくはString型)の変数にstatic final修飾子をつけて定数クラスを作成していた。

``` java
public class Drink {
 public static final String COLA = "コーラ"
 public static final String CIDER = "サイダー"
 public static final String COFFEE = "コーヒー"
}
```

だが、下記コードのように定数を定義しているクラス(以後、定数クラスと呼ぶ)の値を引数とするメソッドがあった場合、定数クラス以外の値が代入されても、コンパイルエラーにならない。
そのため、仕様を十分に理解しないまま開発を進めると、想定していた結果とは異なる動作を起こす可能性があった。

```java
public void getDrink(String drink) {
  // 引数にはDrinkクラスに定義した変数のみが代入される想定
  ....
}

public void execute() {
  String ORANGE = "オレンジ"
  getDrink(ORANGE) // 定数クラス以外の値が代入されてもコンパイルエラーにならない
}
```

しかし、enumを利用すれば事前に定義した定数のみをまとめた1つの独自の型を作成できる。
そのため、利用クラス側で定義されていない値が代入される心配がない。
なにより、定数を定義するための修飾子をわざわざ書く必要がないので、コードがシンプルになる。

``` java
// ORANGEは定義されていないので、コンパイルエラーになる。
Drink drink = Drink.ORANGE
```

### 実行時に値が展開される

final修飾子をつけて初期化されているフィールドはコンパイル時に値を展開(代入)する。
そのため、利用クラス側をコンパイルしてから定数クラスを変更すると、両方で同じ変数にも関わらず、異なる値が参照されてしまう。 
(ただし、EclipseやIntelliJのようなIDEでは、定数クラスが変更された場合、利用クラス側も自動で再コンパイルしてくれる。。。)

一方、enum型はコンパイル時に値を展開しない。
そのため、定数クラスが変更されても、利用クラス側をコンパイルし直す必要がない。
また、コンパイルのタイミングによって、値が異なることもなくなる。

## enumのデメリット

### 継承ができない

enum型はjava.lang.Enumを継承したクラスであり、イメージ的には以下のようなクラス定義となる。

```java

public enum Enum extends java.lang.Enum{
  ......
}

```

そのため、enum型に他のクラスを継承することはできない。
これはJavaが多重継承を許していないためである。
(なお、当たり前の話かもしれないが、enum型ではない通常の定数クラスであれば継承は行える。)

## enumのメソッド

ここでは、メソッドをいくつか紹介する。

### toString()

enum型定数の文字列を取得するメソッドである。
同じようなメソッドにname()があるが、こちらは公式で非推奨とされている。
また、name()はオーバーライドできないが、toString()は可能という違いもある。

### getDeclaringClass()

enum型のクラスオブジェクトを返すメソッド。
基本的に定数をただ宣言するだけであれば、getClass()と実行結果は変わらないが、
定数に対応するクラス本体を作成した場合には実行結果に違いが出る。

```java
// ①クラス本体を定義している定数の場合
System.out.println(Drink.CIDER.getClass()); // class Drink$1
System.out.println(Drink.CIDER.getDeclaringClass()); // class Drink

// ②クラス本体を定義していない定数の場合
System.out.println(Drink.COLA.getClass()); // class Drink
System.out.println(Drink.COLA.getDeclaringClass()); // class Drink

```

上記のコードを例に説明すると、`getClass()`を実行した場合、`class enum型$1`のような形式で定数のクラスオブジェクトが出力される。
なお、$1は匿名クラスであることを示している。
これは、定数のクラス本体がenum型の匿名クラスとみなされるからである。

対して、`getDeclaringClass()`は`class enum型`となり、定数の親クラスであるenum型のクラスオブジェクトが出力される。

### valueOf()

引数で指定された文字列に対応するenum型定数を返す。
ただし、指定した文字列に対応する定数が存在しない場合は`IllegalArgumentException`となる。
なお、大文字、小文字を区別するので引数の文字列は必ず大文字にする。

```java
System.out.println(Drink.valueOf("COLA"));
System.out.println(Drink.valueOf("COFFEE"));

// どちらもIllegalArgumentExceptionになる
System.out.println(Drink.valueOf("ORANGE")); // 指定した文字列に対応する定数が存在しないため
System.out.println(Drink.valueOf("cola")); // 小文字になっているため
```

## 参考

[getDeclaringClass vs getClass](https://stackoverflow.com/questions/5758660/java-enum-getdeclaringclass-vs-getclass)
[列挙型（enum）の基本的な使い方とコード例](https://qiita.com/igm50/items/8c9788d4ba5868642c69)
[Java本格入門](https://www.amazon.co.jp/Java%E6%9C%AC%E6%A0%BC%E5%85%A5%E9%96%80-%E3%83%A2%E3%83%80%E3%83%B3%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E3%81%AB%E3%82%88%E3%82%8B%E5%9F%BA%E7%A4%8E%E3%81%8B%E3%82%89%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91%E3%83%BB%E5%AE%9F%E7%94%A8%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%BE%E3%81%A7-%E8%B0%B7%E6%9C%AC-%E5%BF%83/dp/477418909X/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=java+%E6%9C%AC%E6%A0%BC%E5%85%A5%E9%96%80&qid=160712)