[Java How To Programming] [Home on 246net] [Home on Alles net]
Powered by SmartDoc

型に安全な定数

TAKAHASHI, Toru
torutk@alles.or.jp

目次

型に安全な定数を使用するTips

Javaでは定数を表現する専用の型はJ2SE 1.5になって(やっと)導入されました。J2SE 1.4以下のバージョンでは、型に安全な定数を自前で作成する必要があります。

定数フィールド

よく目にする定数のイディオムとして、public final staticなフィールドがあります。

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) {
        // エラー処理
    }

しかし、使用する個所全てに範囲チェックを記述していくことは、単に作業が増えるだけではなく、定数の種類が増減した場合の保守も大変となり、現実的ではありません。

型に安全な定数(その1)

では、種族を専用の型としてclass AlienRaceで定義してみます。

型に安全な定数(その1)
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等の値を使用したら、コンパイルが型検査でエラーを検知してくれます。

型に安全な定数(その1)の使用例
    // 正しい指定
    Crew lwaxana = new Crew("Lwaxana Troi", AlienRace.BETAZOID);
    // 間違った指定、コンパイルエラーとなる
    Crew deanna = new Crew("Deanna Troi", 123);

ここでのポイントは、

です。

しかし、この方法にも次のような問題があります。

型に安全な定数(その2)

型に安全な定数(その1)のクラスAlienRaceは、各定数をクラスが最初にロードされた時にフィールドの各定数を生成します。この定数がシリアライズ可能なオブジェクトで使用される場合、デシリアライズされた時には最初に生成された定数とは異なるオブジェクトとして生成されることになります。すなわち、==や!=といったオブジェクトの参照を比較する演算が意図しない結果を返すことになります。そこで、シリアライズを考慮した定数を検討してみます。

型に安全な定数(その2)
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から定数オブジェクトの参照に変換しています。

型に安全な定数(その3)

定数同士を比較可能にしたい場合を想定します。

Java2 5.0で導入されたenum型

Java2 5.0(Tiger)で導入されたenumとは、以下の特徴を持っています。(書籍「Java5.0 Tiger」より)

定数の列挙で使用

class、interfaceと同列にenumというのが新規追加されました。これを使用すると型に安全な定数を列挙型として定義することが簡単にできるようになります。

Enum型を使用した定数
public enum AlienRace {
    VULCANS,
    CARDASSIAN,
    KLINGON,
    ROMULAN,
    HUMAN,
    BORG,
    FERENGI,
    BETAZOID,
    ANDROID,
    ELAURIAN
}
Enumの使用例(1)
    public static void printCounselor() {
        Crew counselor = new Crew("Deanna Troi", AlienRace.BETAZOID);
        System.out.println("Counselor is " + counselor);
    }
Enumの使用例(2)
    public void printCrews(AlienRace aRace) {
        for (Crew crew : crews) {
            if (crew.getRace() == aRace) {
                System.out.println(crew);
            }
        }
    }

enum型は、switch文にも利用できます。switch文で使用する場合、case文の定数にenum型名の修飾子は省略できます。

Enum型をswitch文
switch(race) {
case HUMAN:
    // do something
    break;
case KLINGON:
    // do something
    break;
  :
}

enum型のtoString()は、デフォルトでは定数名と同じ文字列になります。また、オーバーライドすることができます。

enum型のtoStringメソッド
    public static void printRace() {
        Crew chiefEngineer = new Crew("Geordi La Forge", AlienRace.HUMAN);
        System.out.println(chiefEngineer.getRace().toString());
    }

整数とenum定数の相互変換

例えば既存の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.