いけむランド

はてダからやってきました

String Switch について調べてみた

Java7 で言語仕様への追加を提案されている String Switch について書かれている記事があったので読んでみた。


現在の (少なくとも Java6 までの) Java 言語仕様では switch 文には case 文の値に整定数 (byte, char, short, int) しかとることができないため、文字列比較の場合は if 文を並べる必要がある。

そこで PHP のように case 文の値に文字列定数も使用できるようにしたいという提案が String Switch である。以下に例 (元記事と同じ) を示す。

void process(String s)
{
  switch (s) {
  case "quux":
    processQuux(s);
    // fall-through
  case "foo":
  case "bar":
    processFooOrBar(s);
    break;
  case "baz":
    processBaz(s);
    // fall-through
  default:
    processDefault(s);
    break;
  }
}


上記のコードをコンパイルする時に最初に考えないといけないことはどのようなバイトコードにできるかである。
ほとんどの場合、switch 文は JVM 命令の lookupswitch または tableswitch にコンパイルされるが、いずれの命令もジャンプテーブルのインデックスは整定数であるため、この命令を使う場合は case 文の値が整定数でなければならない。
JVM にジャンプテーブルのインデックスに文字列定数 (コンスタントプールのインデックス) をとるような新命令を追加することでも対応できそうだが、JVM 命令セットへの追加は互換性という観点からあまり望ましくないだろう。


そこで提案では case 文の値である文字列定数を整定数にするために hashCode() を使用するようにしているが、異なる文字列が同じハッシュ値を返す可能性があること (いわゆる衝突) を考慮する必要がある。その点を考慮して、既存のコンパイラでもコンパイルできるように変形されたコード (こちらも元記事と同じ) を以下に示す。

void process(String s)
{
  boolean $take_default = false;
  boolean $fallthrough  = false;
  $default_label: {
    switch (s.hashCode()) { // cause NPE if s is null
    case 3482567: // "quux".hashCode()
      if (!s.equals("quux")) { // "quux" 以外の文字列でも hashCode() == 3482567 となる可能性があるため
        $take_default = true;
        break $default_label;
      }
      processQuux(s);
      $fallthrough = true;
    case 101574: // "foo".hashCode()
      if (!$fallthrough && !s.equals("foo")) { // fall through の場合も考慮する必要があるため
        $take_default = true;
        break $default_label;
      }
      $fallthrough = true;
    case 97299:  // "bar".hashCode()
      if (!$fallthrough && !s.equals("bar")) {
        $take_default = true;
        break $default_label;
      }
      processFooOrBar(s);
      break;
    case 97307: // "baz".hashCode()
      if (!s.equals("baz")) {
        $take_default = true;
        break $default_label;
      }
      processBaz(s);
      $fallthrough = true;
    default:
      $take_default = true;
      break $default_label;
    }
  }
  if ($take_default) {
    processDefault(s);
  }
}


ただし、この提案では文字列定数の hashCode() はコンパイル時に必ず一意に決まるとは限らないことが考慮されていない。具体的には「JRE のヴァージョンにより、hashCode() の処理が異なる」ということである。詳細は以下のエントリを参照されたい。


他の案としては hashmap を使用したジャンプテーブルが考えられる。ただし、こちらは hashCode() の処理に依存しないものの、実行にかなりコストがかかると思われる。

void process(String s)
{
  HashMap<String,Integer> $table = new HashMap<String,Integer>();
  $table.put("quux", 1);
  $table.put("foo", 2);
  $table.put("bar", 3);
  $table.put("baz", 4);

  if ($table.containsKey(s)) {
    switch ($table.get(s)) {
    case 1:
      processQuux(s);
      // fall-through
    case 2:
    case 3:
      processFooOrBar(s);
      break;
    case 4:
      processBaz(s);
      // fall-through
    default:
      processDefault(s);
      break;
    }
  } else {
    processDefault(s);
  }
}


String Switch は有用であると思われるが、enum でも近いことはできるため、そちらを利用すれば良いだろうという意見もある。