目次


列挙型を使う

Java 5.0を使ってタイプセーフな方法で定数を表現する

Comments

皆さんは恐らく、Javaコードの基本的な2つの構成ブロックがクラスとインターフェースであることを既に知っているでしょう。Tigerではこれにもう一つ、列挙(enumeration)が導入されています。通常は簡単にenumと呼ばれますが、この新しい型を使うと、必要な時に事前定義された一連の値のみを受け付ける、特定なデータポイントを表現できるようになります。

良く鍛錬したプログラマーであれば当然ながら、静的定数でもこの機能を実現できることを知っているでしょう(リスト1)。

リスト1. Public static final定数
public class OldGrade {
  public static final int A = 1;
  public static final int B = 2;
  public static final int C = 3;
  public static final int D = 4;
  public static final int F = 5;
  public static final int INCOMPLETE = 6;
}

注記: 私の本Java 1.5 Tiger: A Developer's Notebookの「Enumerations」章からこの記事のために、サンプル・コードを使用することを許可くださったO'Reilly Media, Inc.に感謝致します(参考文献)。

次に、OldGrade.Bのような定数を取り入れるようにクラスを設定することができますが、そういう時にはそうした定数はJavaのintであることを頭に置いてください。つまりメソッドはどんなintでも、たとえOldGradeで定義される特定なグレードには対応しないintでも受け付けるのです。ですから、上限と下限をチェックする必要があり、もし無効な値が現れた場合には恐らくIllegalArgumentExceptionを含む必要があります。また、やがて別のグレードが追加されると(例えばOldGrade.WITHDREW_PASSING)、この新しい値を許すために、全コードの上限を変更する必要があるでしょう。

言い換えると、整数の定数を持つクラスを使うのは一つの方法かも知れませんが、あまり効率的なものではありません。幸い、列挙を使うことで、もっとうまくできるようになるのです。

列挙を定義する

リスト2は列挙を使って、リスト1と似た機能を実現しています。

リスト2. 簡単な列挙型
package com.oreilly.tiger.ch03;
public enum Grade {
  A, B, C, D, F, INCOMPLETE
};

ここで私は新しいキーワードenumを使い、列挙に名前を与え、許される値を定義しています。そうするとGradeは列挙型になり、リスト3に示すような使い方ができます。

リスト3. 列挙型を使う
package com.oreilly.tiger.ch03;
public class Student {
  private String firstName;
  private String lastName;
  private Grade grade;
  public Student(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }
  public String getFirstName() {
    return firstName;
  }
  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
  public String getLastName() {
    return lastName;
  }
  public String getFullName() {
    return new StringBuffer(firstName)
           .append(" ")
           .append(lastName)
           .toString();
  }
  public void assignGrade(Grade grade) {
    this.grade = grade;
  }
  public Grade getGrade() {
    return grade;
  }
}

以前定義された型の新しい列挙(grade)を作ることで、これを普通のメンバー変数のように使うことができます。もちろん列挙は、列挙値(例えばACINCOMPLETE)の中の一つのみに割り当てられます。またassignGrade() には、エラー・チェックのコードや境界に関する考慮が無いことにも注意してください。

列挙値を扱う

これまでに挙げた例はかなり簡単なものでしたが、列挙型ではもっと複雑なこともできます。中でも列挙値は、それに対して繰り返しを行うことができ、switchステートメントの中でも使えるので、非常に貴重なものです。

列挙で繰り返す

任意の列挙型の値すべてを次々と処理して行く例から始めましょう。リスト4に示すこの手法は、デバッグや、ちょっとしたプリントタスク、コレクションに列挙をロードする(すぐ後で説明します)ために手軽に使うことができます。

リスト4. 列挙値に対して繰り返す
public void listGradeValues(PrintStream out) throws IOException {
  for (Grade g : Grade.values()) {
    out.println("Allowed value: '" + g + "'");
  }
}

このコード断片を実行すると、リスト5に示すような出力が得られます。

リスト5. 繰り返しの出力
Allowed Value: 'A'
Allowed Value: 'B'
Allowed Value: 'C'
Allowed Value: 'D'
Allowed Value: 'F'
Allowed Value: 'INCOMPLETE'

ここでは非常に多くのことが行われています。第一に、私はTigerの新しいfor/inループを使っています(これはforeachとかenhanced forとも呼ばれます)。さらに、values() メソッドは個々のGradeインスタンスの配列を返し、そのそれぞれが列挙型の値の一つを持っていることが分かると思います。言い換えると、values()の戻り型はGrade[] です。

列挙でswitchを行う

列挙の値を次々に全て処理できるのは良いことなのですが、もっと重要なのは、列挙の値に基づいて判定が行えるということです。もちろんif (grade.equals(Grade.A)) 形式のステートメントを大量に書くこともできるのですが、それは時間の浪費でしかありません。Tigerでは便利なことに、昔ながらのswitchステートメントに列挙サポートを追加したのです。ですから簡単に使うことができ、しかも皆さんが既に知っていることに非常にうまく収まるのです。リスト6は、これをどうやってのけるかを示しています。

リスト6. 列挙でswitchを行う
public void testSwitchStatement(PrintStream out) throws IOException {
  StringBuffer outputText = new StringBuffer(student1.getFullName());
  switch (student1.getGrade()) {
    case A: outputText.append(" excelled with a grade of A");
      break;   case B: // fall through to C
    case C: outputText.append(" passed with a grade of ")
                .append(student1.getGrade().toString());
      break;
    case D: // fall through to F
    case F:
      outputText.append(" failed with a grade of ")
                .append(student1.getGrade().toString());
      break;
    case INCOMPLETE:
      outputText.append(" did not complete the class.");
      break;
  }
  out.println(outputText.toString());
}

ここで列挙値はswitchステートメントに渡され(getGrade()Gradeのインスタンスを戻すことを忘れないでください)、そして各case文節が特定な値を処理します。この値はenumプレフィックス無しに与えられますが、これはcase Grade.Aを書く代わりにcase Aを書く必要があることを意味します。これを行わないと、コンパイラーはプレフィックスのついた値を受け付けません。

これでswitchステートメントを使う基本的な構文は理解できたはずですが、他にも知っておくべきことが幾つかあります。

まずswitchで考える

皆さんのご想像の通り、列挙やswitchではdefaultステートメントを使うことができます。リスト7はこの使い方を示しています。

リスト7. defaultブロックを追加する
public void testSwitchStatement(PrintStream out) throws IOException {
  StringBuffer outputText = new StringBuffer(student1.getFullName());
  switch (student1.getGrade()) {
    case A: outputText.append(" excelled with a grade of A");
      break;   case B: // fall through to C
    case C: outputText.append(" passed with a grade of ")
                .append(student1.getGrade().toString());
      break;
    case D: // fall through to F
    case F:
      outputText.append(" failed with a grade of ")
                .append(student1.getGrade().toString());
      break;
    case INCOMPLETE:
      outputText.append(" did not complete the class.");
      break;
default:
      outputText.append(" has a grade of ")
                .append(student1.getGrade().toString());
      break;
  }
  out.println(outputText.toString());

上のコードを見ると気がつくと思いますが、具体的にcaseステートメントで処理されない列挙値は、代わりにdefaultステートメントで処理されています。これはいつでも使うべき手法です。なぜかを説明しましょう。皆さんのグループの中にいる一人のプログラマーが、(皆さんに伝えることなく)Grade 列挙をリスト8に示すような形に変更したとします。

リスト8. Grade列挙に値を加える
package com.oreilly.tiger.ch03;
public enum Grade {
  A, B, C, D, F, INCOMPLETE, WITHDREW_PASSING, WITHDREW_FAILING
};

この新しいバージョンのGradeをリスト6のコードで使ってしまうと、この2つの新しい値は無視されてしまいます。もっと悪いことに、皆さんはエラーを見ることもないのです! こうした場合では、何らかの汎用defaultステートメントがあることが非常に重要になります。リスト7の処理は優雅なものではないかも知れませんが、少なくとも、値が入っていて、そうした値を考慮する必要があることを知らせる作りにはなっています。こうしておけばアプリケーションが実行を続け、値を無視せず、後で何らかのアクションを取るように指示までしてくれるようになります。これは良いコーディングと言えます。

列挙とコレクション

public static finalの手法を使ったコーディングに慣れている人であれば恐らく、マップへのキーとして列挙値を使うように既に移行しているでしょう。この意味が分からない人は、リスト9を見てください。これはAntビルド・ファイルを処理する時によく登場する、一般的なエラー・メッセージの例です。

リスト9. Antテータス・コード
package com.oreilly.tiger.ch03;
public enum AntStatus {
  INITIALIZING,
  COMPILING,
  COPYING,
  JARRING,
  ZIPPING,
  DONE,
  ERROR
}

各テータス・コードに対して人が読めるようなエラー・メッセージを割り当てておくことで、Antがステータス・コードの一つを提供した時には、適切なエラー・メッセージを参照でき、そのメッセージをコンソールにエコー・バックできるようになります。これはMapの素晴らしい使い方です。Mapの各キーが列挙値の一つであり、そして各値はそのキーに対するエラー・メッセージになっているのです。リスト10はこれがどのように動作するのかを示しています。

リスト10. 列挙のMap
public void testEnumMap(PrintStream out) throws IOException {
  // Create a map with the key and a String message
  EnumMap<AntStatus, String> antMessages =
    new EnumMap<AntStatus, String>(AntStatus.class);
  // Initialize the map
  antMessages.put(AntStatus.INITIALIZING, "Initializing Ant...");
  antMessages.put(AntStatus.COMPILING,    "Compiling Java classes...");
  antMessages.put(AntStatus.COPYING,      "Copying files...");
  antMessages.put(AntStatus.JARRING,      "JARring up files...");
  antMessages.put(AntStatus.ZIPPING,      "ZIPping up files...");
  antMessages.put(AntStatus.DONE,         "Build complete.");
  antMessages.put(AntStatus.ERROR,        "Error occurred.");
  // Iterate and print messages
  for (AntStatus status : AntStatus.values() ) {
    out.println("For status " + status + ", message is: " +
                antMessages.get(status));
  }
}

このコードはgenerics(参考文献)と、新しいEnumMap構成体の両方を使って新しいマップを作ります。また列挙型はそのClassオブジェクトを通して、Mapの値の型(この場合は単純な文字列です)と共に提供されます。このメソッドの出力をリスト11に示します。

リスト11. リスト10の出力
[echo] Running AntStatusTester...
[java] For status INITIALIZING, message is: Initializing Ant...
[java] For status COMPILING, message is: Compiling Java classes...
[java] For status COPYING, message is: Copying files...
[java] For status JARRING, message is: JARring up files...
[java] For status ZIPPING, message is: ZIPping up files...
[java] For status DONE, message is: Build complete.
[java] For status ERROR, message is: Error occurred.

この先は

列挙はsetと一緒に使うこともできます。またTigerでは新しいEnumMap構成体とほとんど同じように、新しいSet実装EnumSetを提供しており、これを使うとビット単位の操作ができるようになります。さらに列挙にメソッドを追加してインターフェースの実装に使い、値特有のクラス・ボディー(列挙の特定な値に特定なコードが付加されます)と呼ばれるものを定義することができます。これらの機能はこの記事の範囲外ですが、他の記事で詳しく説明されています(参考文献)。

大いに使ってください、でも乱用は避けてください

どんな言語でもそうですが、新しいバージョンを学ぶ時の危険性は、新しい構文構造に熱中しがちなことです。そうすると、途端にコードの8割がgenericsとアノテーションと列挙、となってしまいます。ですから列挙も、使って意味のある部分にだけ使うようにしてください。では意味のある部分とはどこなのでしょう。一般的に、定数が使われているところ、つまり現在は定数の切り換えにswitchコードを使っているような場所であれば、使って適当と言うことができます。もし定数が単一の値(例えば靴のサイズの上限や、樽の中に入れられる猿の上限など)であれば、そのままにしておくべきです。逆に一連の値を定義する場合で、こうした値のどれか一つが特定のデータ型に使われる場合には、列挙が最適と言えるでしょう。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=232294
ArticleTitle=列挙型を使う
publish-date=11092004