Javaでは定数を表現する専用の型はJ2SE 1.5になって(やっと)導入されました。J2SE 1.4以下のバージョンでは、型に安全な定数を自前で作成する必要があります。
よく目にする定数のイディオムとして、public final staticなフィールドがあります。
public class Crew { public final static int BORG = 0; public final static int FERENGI = 1; public final static int BETAZOID = 2; : public Crew(String name, int race) { ... } }
ここではプリミティブ型のintを使って定数フィールドを定義しました。正しく使っている限りは問題ないのですが、下記の例のように間違った使い方をした場合を想定してみます。
// 正しい指定 Crew lwaxana = new Crew("Lwaxana Troi", Crew.BETAZOID); // 間違った指定だが、コンパイルはエラーとならない Crew deanna = new Crew("Deanna Troi", 123);
コンパイラはエラーとならず正常にバイトコードを吐き出します。ソースコードレビューを実施しても、気付かれずに見過ごしてしまいがちです。その結果、原因が見つけにくいバグとなります。気づいてしまえば単純なコーディングミスなので、ささっと直して終わりにしてしまいます。しかし、この種のバグは今後何度となく繰り返し発生することになります。
種族というデータをintという汎用的なデータ型で表現してしまったため、コンパイル時の型検査が厳密に行えないことが問題なのです。
ここで、「じゃあ定数の値が有効な範囲にあることをチェックすればいい」と下記のように引数の有効チェックを行うことも考えられます。
if (race != Crew.BORG && race != Crew.FERENGI && race != Crew.BETAZOID) { // エラー処理 }
しかし、使用する個所全てに範囲チェックを記述していくことは、単に作業が増えるだけではなく、定数の種類が増減した場合の保守も大変となり、現実的ではありません。
では、種族を専用の型としてclass AlienRaceで定義してみます。
public final class AlienRace { public static final AlienRace BORG = new AlienRace(); public static final AlienRace FERENGI = new AlienRace(); public static final AlienRace BETAZOID = new AlienRace(); private AlienRace() {} }
これならば、あやまってint等の値を使用したら、コンパイルが型検査でエラーを検知してくれます。
// 正しい指定 Crew lwaxana = new Crew("Lwaxana Troi", AlienRace.BETAZOID); // 間違った指定、コンパイルエラーとなる Crew deanna = new Crew("Deanna Troi", 123);
ここでのポイントは、
です。
しかし、この方法にも次のような問題があります。
型に安全な定数(その1)のクラスAlienRaceは、各定数をクラスが最初にロードされた時にフィールドの各定数を生成します。この定数がシリアライズ可能なオブジェクトで使用される場合、デシリアライズされた時には最初に生成された定数とは異なるオブジェクトとして生成されることになります。すなわち、==や!=といったオブジェクトの参照を比較する演算が意図しない結果を返すことになります。そこで、シリアライズを考慮した定数を検討してみます。
public final class AlienRace implements Serializable { private static int nextOrdinal = 0; private final int ordinal = nextOrdinal++; public static final AlienRace BORG = new AlienRace(); public static final AlienRace FERENGI = new AlienRace(); public static final AlienRace BETAZOID = new AlienRace(); private static final AlienRace[] PRIVATE_VALUES = { BORG, FERENGI, BETAZOID }; private AlienRace() {} private Object readResolve() throws ObjectStreamException { return PRIVATE_VALUES[ordinal]; } }
まず、定数クラスに、各定数を配列で持つフィールドPRIVATE_VALUESを定義します。また、各定数オブジェクトには一意なIDを割り付けます。このIDはシリアライズされてデシリアライズされた後も同じ値となることを利用し、readResolveメソッドでIDから定数オブジェクトの参照に変換しています。
定数同士を比較可能にしたい場合を想定します。
Java2 5.0(Tiger)で導入されたenumとは、以下の特徴を持っています。(書籍「Java5.0 Tiger」より)
class、interfaceと同列にenumというのが新規追加されました。これを使用すると型に安全な定数を列挙型として定義することが簡単にできるようになります。
public enum AlienRace {
VULCANS,
CARDASSIAN,
KLINGON,
ROMULAN,
HUMAN,
BORG,
FERENGI,
BETAZOID,
ANDROID,
ELAURIAN
}
public static void printCounselor() { Crew counselor = new Crew("Deanna Troi", AlienRace.BETAZOID); System.out.println("Counselor is " + counselor); }
public void printCrews(AlienRace aRace) { for (Crew crew : crews) { if (crew.getRace() == aRace) { System.out.println(crew); } } }
enum型は、switch文にも利用できます。switch文で使用する場合、case文の定数にenum型名の修飾子は省略できます。
switch(race) { case HUMAN: // do something break; case KLINGON: // do something break; : }
enum型のtoString()は、デフォルトでは定数名と同じ文字列になります。また、オーバーライドすることができます。
public static void printRace() { Crew chiefEngineer = new Crew("Geordi La Forge", AlienRace.HUMAN); System.out.println(chiefEngineer.getRace().toString()); }
例えば既存のstatic final int定数で定義された定数とenum型とのマッピングをしたい場合など、列挙型と整数値との相互変換を行いたいことがあります。
enum型は、デフォルトでは文字列との相互変換メソッドを提供していますが、整数型については用意されていません。
[1] | Nigel Warren; Philip Bishop. Javaの格言 : より良いオブジェクト設計. ピアソン・エデュケーション, 2000. |
[2] | Joshua Bloch. Effective Java : プログラミング言語ガイド. ピアソン・エデュケーション, 2001. |
[3] | Jeff Langr. essential Java Style : Patterns for Implementation. PRENTICE HALL, 2000. |
[4] | Brett McLaughlin; David Flanagan. Java 5.0 Tiger. オライリー, 2004. |