1.6. 列挙型の定義
『目的』
いわゆる「列挙型」を実現しましょう。
『Before』
メソッドのパラメータとして定数を使うケースはよくありますが、 定数をオブジェクトとして用意することで事前に設定ミスに気づくことができます。
一般的によく見かける定数の表現は以下のようなものです。
public class Organization {
// 総務部
public static final int GENERAL_AFFAIRS = 0;
// 第1事業部
public static final int ORG_1 = 1;
// 第2事業部
public static final int ORG_2 = 2;
// 第3事業部
public static final int ORG_3 = 3;
// 技術支援グループ
public static final int TECH_SUPPORT = 4;
}
public static final で定数を表現しています。
public なのでどこからでもアクセスできます。
static なのでインスタンス化せず使用できます。
final なので初期化後に値の変更はできません。
例えば、こんなメソッドがあったとします。
/**
* 所属を設定し初期化します。
*
* @param org 所属。
* Organization.GENERAL_AFFAIRS、
* Organization.ORG_1、
* Organization.ORG_2、
* Organization.ORG_3、
* Organization.TECH_SUPPORT
* のいずれかを指定してください。
*/
public void setOrganization(int org) {
// :
}
このメソッドにはどのような値を入れることができるか考えてみましょう。
「その1.用意されている定数を使う」
定数が Organization にて定義されているのでおとなしくそれを使います。
普通はこうしますね。
定数を使うときはこのようにして使います。
Organization.TECH_SUPPORT
/* 「技術支援グループ」とする。 */
field.setOrganization(Organization.TECH_SUPPORT);
「その2.定数を使わない」
定数を使わなくても、int値をそのまま放り込むことが可能ですね。
・Organization.TECH_SUPPORT は 4 と同等だから問題ないと考える。
・Javadoc を読まず、定数が用意されていることに気づかず数値をそのまま指定した。
という状況下ならあり得そうなケースです。
/* 「技術支援グループ」とする。 */
field.setOrganization(4);
「その3.定数を使わない」
そもそもint値ならば何でも放り込むことが可能です。
何が起こるかわかりませんが。
field.setOrganization(-1);
field.setOrganization(100);
「その1」から「その3」のどのケースにおいても、intの範囲内である有効な整数ですから エラーなくコンパイルは通ります。けれども、コンパイルできたから問題はないかというと実はそうとも言えません。
まず、「その2」は必ずしもうまくいく保証はありません。
後日、定数の定義が変更された場合に正しく動作しなくなる可能性が大です。
定数の定義を変更してみましょう。
public class Organization {
public static final int GENERAL_AFFAIRS = 0;
public static final int ORG_1 = 1;
public static final int ORG_2 = 2;
public static final int ORG_3 = 3;
// 「第4事業部」誕生!値は 4。
public static final int ORG_4 = 4;
// 押し出されるように 4 -> 5 へ。
public static final int TECH_SUPPORT = 5;
}
するとどうでしょう。
/* 「技術支援グループ」とする。 */
field.setOrganization(4);
// でも、「第4事業部」が誕生したおかげで、「第4事業部」にすり替わってしまう!
このままでは、コメントに書いてあるような期待した動作にはなりませんよね。 正しくするにはこのコードも変更する必要があるわけです。 定数値を使わなかったことによって変更に対して新たな変更を強いられてしまうわけです。
メソッドの呼び出し元を全て確認することになってしまうので面倒ですね。 保守が大変です。
「その3」のケースは明らかに無効な値です。
setOrganization メソッド内で、 GENERAL_AFFAIRS ~ TECH_SUPPORT が渡されることを想定していたらどうなるでしょうか。
public void setOrganization(int org) {
if (org == Organization.GENERAL_AFFAIRS) {
// 総務部用の初期化処理
} else if (org == Organization.ORG_1) {
// 第1事業部用の初期化処理
} else if (org == Organization.ORG_2) {
// 第2事業部用の初期化処理
} else if (org == Organization.ORG_3) {
// 第3事業部用の初期化処理
} else if (org == Organization.TECH_SUPPORT) {
// 技術支援グループ用の初期化処理
}
// -1 や 100 のような無効値では初期化されない。
// 後々おかしくなってしまうでしょう。
}
無効な値が渡ってきたら、正常な動作は期待できずに実行時にエラーが出てしまう恐れがあります。 それでは、メソッド内に範囲チェックを実装して無効な値を防ぐようにするとどうなるでしょうか。
public void setOrganization(int org) {
if (org == Organization.GENERAL_AFFAIRS) {
// 総務部用の初期化処理
} else if (org == Organization.ORG_1) {
// 第1事業部用の初期化処理
} else if (org == Organization.ORG_2) {
// 第2事業部用の初期化処理
} else if (org == Organization.ORG_3) {
// 第3事業部用の初期化処理
} else if (org == Organization.TECH_SUPPORT) {
// 技術支援グループ用の初期化処理
} else {
/* 不正な設定値。 */
throw new IllegalArgumentException("無効な所属です。");
}
}
こうすることで、無効な値が渡されても何食わぬ顔をして実行されることはなくなるでしょう。
しかし、依然として実行時にIllegalArgumentException例外が送出されることに代わりありません。
つまり、「その2」や「その3」ともに「実行するまで」ミスに気づきません。
なるべくなら未然に防ぎたいものです。
『After』
パラメータに渡された値が有効かどうかをコンパイラにチェックさせる方法を取り入れてみましょう。 定数は、intなどのプリミティブ型ではなくクラスのインスタンスで作ります。
public final class Organization {
// 総務部
public static final Organization GENERAL_AFFAIRS = new Organization();
// 第1事業部
public static final Organization ORG_1 = new Organization();
// 第2事業部
public static final Organization ORG_2 = new Organization();
// 第3事業部
public static final Organization ORG_3 = new Organization();
// 技術支援グループ
public static final Organization TECH_SUPPORT = new Organization();
private Organization() {}
}
よく見かける定数定義と変わった点は以下です。
・定数を int ではなくて自身のインスタンス(Organization)とする。
・クラスを final とする。
こうすることでサブクラスの生成を防止します。サブクラスの生成を許すと、 引数にサブクラスのインスタンスを渡せてしまい、範囲チェックが必要となってしまうからです。 新しい定数をガンガン作られても困るわけです。
また、これも大事です。
・コンストラクタを private とする
定数クラスの有効なインスタンスが他の場所で生成されるのを防止するためです。 新しい定数をガンガン作られても困るわけです。
メソッドのパラメータの型を int から Organization に変更します。
public void setOrganization(Organization org) {
// :
}
こうすれば、int ではなくて Organization という型を入れなければならないと 具体的にわかりやすくなりますし、「その2」や「その3」のような暴挙は コンパイラがエラーとして叱ってくれます。
また、Organizationt のインスタンス(GENERAL_AFFAIRS ~ TECH_SUPPORT)しか パラメータとして渡すことを許さないので、setOrganization メソッド内では 範囲チェックする必要がなくなりますね。
これで万事問題なしかというと実はまだ一つ抜け道があります。
パラメータとして null が設定できてしまいます。
null がパラメータとして渡された場合の対応策です。
「対応策その1」
単純に null をメソッド内で捕捉して NullPointerException を投げてしまいます。
/**
* 所属を設定し初期化します。
*
* @param org 所属
* @exception NullPointerException パラメータに null を指定した場合
*/
public void setOrganization(Organization org);
if (org == null) {
throw new NullPointerException("パラメータに null は指定できません。");
}
if (Organization.GENERAL_AFFAIRS.equals(org)) {
// 総務部用の初期化処理
} else if (Organization.ORG_1.equals(org)) {
// 第1事業部用の初期化処理
} else if (Organization.ORG_2.equals(org)) {
// 第2事業部用の初期化処理
} else if (Organization.ORG_3.equals(org)) {
// 第3事業部用の初期化処理
} else if (Organization.TECH_SUPPORT.equals(org)) {
// 技術支援グループ用の初期化処理
}
}
こうすると、少なくとも範囲チェックの煩わしさからは解放されます。
null を設定したら実行するまでミスに気がつきませんが、 さすがにわざわざ null を設定するようなひねくれたことはしないでしょう。
Javadoc には null を設定したら例外が発生すると明記してあるので、 その時点で null は設定してはいけないのだという契約は成立ですね。
# Javadoc に書いてあるのにそれを見ない人が悪いということです。
「対応策その2」
null もいずれかの定数と同じ値とみなします。
public final class Organization {
// 総務部
public static final Organization GENERAL_AFFAIRS = null;
// 第1事業部
public static final Organization ORG_1 = new Organization();
// 第2事業部
public static final Organization ORG_2 = new Organization();
// 第3事業部
public static final Organization ORG_3 = new Organization();
// 技術支援グループ
public static final Organization TECH_SUPPORT = new Organization();
private Organization() {}
}
こうすると、null は GENERAL_AFFAIRS と同等の扱いになります。
メソッド内では null チェックの必要性もなくなります。
ただし、注意すべき点があります。状態を持つ定数に対応できないということです。
以下のような場合だと問題が発生します。
public final class Organization {
// 総務部
public static final Organization GENERAL_AFFAIRS = null;
// 第1事業部
public static final Organization ORG_1 = new Organization(1);
// 第2事業部
public static final Organization ORG_2 = new Organization(2);
// 第3事業部
public static final Organization ORG_3 = new Organization(3);
// 技術支援グループ
public static final Organization TECH_SUPPORT = new Organization(4);
private int orgValue;
private Organization(int org) {
orgValue = org;
}
public int getValue() {
return orgValue;
}
}
GENERAL_AFFAIRS.getValue() を呼び出すと、実行時に NullPointerException 例外が送出されてしまいます。
状態を持つ定数に対応するには、代理となるクラスを用意してそれに値を保持させることで解決はできます。 ただし、もちろんその分だけコード量が増えます。
「対応策その2-2」
代理クラスを作って値を保持させます。
public class ConstProxy {
private Organization organization;
private int orgValue;
public ConstProxy(Organization organization, int value) {
this.organization = organization;
orgValue = value;
}
public boolean isHolding(Organization organization) {
return this.organization == organization;
}
public int getValue() {
return orgValue;
}
}
定数クラスも改良します。
public final class Organization {
// 総務部
public static final Organization GENERAL_AFFAIRS = null;
// 第1事業部
public static final Organization ORG_1 = new Organization();
// 第2事業部
public static final Organization ORG_2 = new Organization();
// 第3事業部
public static final Organization ORG_3 = new Organization();
// 技術支援グループ
public static final Organization TECH_SUPPORT = new Organization();
private static List proxies = new ArrayList();
static {
proxies.add(new ConstProxy(GENERAL_AFFAIRS, 0));
proxies.add(new ConstProxy(ORG_1, 1));
proxies.add(new ConstProxy(ORG_2, 2));
proxies.add(new ConstProxy(ORG_3, 3));
proxies.add(new ConstProxy(TECH_SUPPORT, 4));
}
private Organization() {}
public static int valueOf(Organization org) {
ConstProxy target = null;
for (Iterator i = proxies.iterator; i.hasNext(); ) {
target = (Organization)i.next();
if (target.isHolding(org)) {
break;
}
}
return target.getValue();
}
}
Organization.valueOf(Organization.GENERAL_AFFAIRS) を呼び出しても、 NullPointerException 例外は発生せず、0 が取得できます。
いずれの対応策を採用するかは状況に応じたトレードオフだと思いますが、 大抵の状況では「対応策その1」で事足りるのではないでしょうか。
『まとめ』
定数をオブジェクトとして用意すると以下のような効果があります。
・メソッド内における範囲チェックの煩わしさから解放される。
・無効な値の設定をコンパイル時に発見できる。
なので、パラメータの設定ミスを抑止することができますね。
列挙型に近い効果が得られます。
なお、Java 5.0 では列挙型の考えが導入されていますので、 このイディオムを使わなくても簡単に列挙型が定義できます。