Hatena::ブログ(Diary)

happynowの日記

2012-05-05

enum 定数の switch 文の実装

switch 文の case ラベルがint や short のプリミティブ型の final 定数であれば、その即値が switch 文にインライン展開される。
したがって、定数の値が変更されれば、その switch 文を使うクライアントコードの再コンパイルが必要になる。

ところが、switch 文で列挙定数を使っている場合、列挙クラスのソースコードの修正によって、
列挙定数の序数が変わったとしても、クライアントコードを再コンパイルする必要はない。
列挙定数の場合は、プリミティブ型の定数と違って、バイナリ互換性の問題はない。

これが不思議だったので、バイトコードを調べてみた。

まず、次が検証用の列挙定数のサンプルコードである。

// Planet.java
public enum Planet {
    SUN, MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN
}

クライアントコードは以下のとおり。

// Main.java
public class Main {
    public static void main(String[] args) {
        switch (Planet.EARTH) {
        case JUPITER:
            System.out.println("Jupiter");
            break;
        case VENUS:
            System.out.println("Venus");
            break;
        case EARTH:
            System.out.println("Earth");
            break;
        }
    }
}

Main.java をコンパイルすると、
コンパイラは、列挙定数の序数と case ラベルの間でマッピングを行うため、バイトコードに static 配列を追加する。
このマッピング用の配列は、 static 初期化子で初期化される。

初期化処理では、列挙定数の数を問い合わせ、すべての列挙定数を格納できるサイズで配列を動的に作成するが、
実際に格納するマッピングデータは case ラベルに使用されている定数のものだけである。

以下は、コンパイラによって変換されたコードとほぼ等価な Java コードである。
簡略化のため例外処理等は省略している。

public class Main {
    // コンパイラは、列挙定数の序数からcaseラベルへのマッピングのための配列を自動的に追加する.
    static final int $Map[];

    static {
        // マッピング用の配列をインスタンス化する.サイズはすべての列挙定数の個数.
        $Map = new int[Planet.values().length];

        // switch 文のラベルに記されている順番で 1 から付番してマッピング用のデータを作成する.
        $Map[Planet.JUPITER.ordinal()] = 1;
        $Map[Planet.VENUS.ordinal()]   = 2;
        $Map[Planet.EARTH.ordinal()]   = 3;
    }

    public static void main(String[] args) {

        int ordinal = Planet.EARTH.ordinal();

        int index = $Map[ordinal];

        switch(index) {
        case 1:
            System.out.println("Jupiter");
            break;
        case 2:
            System.out.println("Venus");
            break;
        case 3:
            System.out.println("Earth");
            break;
        }
    }
}

なお、バイトコードの解析には、jad コマンドを利用。
コマンド jad Main.class で逆コンパイルされたコードは以下のとおり。
ここで case ラベルに使用されている列挙定数の取得に失敗したとき、例外を無視しているのに注目。
これによって、列挙クラスで当該の列挙定数が削除された後も switch 文は問題なく動作する。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   Main.java

import java.io.PrintStream;

public class Main
{

    public Main()
    {
    }

    public static void main(String args[])
    {
        static class _cls1
        {

            static final int $SwitchMap$Planet[];

            static 
            {
                $SwitchMap$Planet = new int[Planet.values().length];
                try
                {
                    $SwitchMap$Planet[Planet.JUPITER.ordinal()] = 1;
                }
                catch(NoSuchFieldError nosuchfielderror) { }
                try
                {
                    $SwitchMap$Planet[Planet.VENUS.ordinal()] = 2;
                }
                catch(NoSuchFieldError nosuchfielderror1) { }
                try
                {
                    $SwitchMap$Planet[Planet.EARTH.ordinal()] = 3;
                }
                catch(NoSuchFieldError nosuchfielderror2) { }
            }
        }

        switch(_cls1..SwitchMap.Planet[Planet.EARTH.ordinal()])
        {
        case 1: // '\001'
            System.out.println("Jupiter");
            break;

        case 2: // '\002'
            System.out.println("Venus");
            break;

        case 3: // '\003'
            System.out.println("Earth");
            break;
        }
    }
}

コマンド jad -dis Main.class で逆コンパイルされたコードは以下のとおり。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) disassembler 
// Source File Name:   Main.java

import java.io.PrintStream;

public class Main
{

    public Main()
    {
    //    0    0:aload_0         
    //    1    1:invokespecial   #1   <Method void Object()>
    //    2    4:return          
    }

    public static void main(String args[])
    {
    //    0    0:getstatic       #2   <Field int[] Main$1.$SwitchMap$Planet>
    //    1    3:getstatic       #3   <Field Planet Planet.EARTH>
    //    2    6:invokevirtual   #4   <Method int Planet.ordinal()>
    //    3    9:iaload          
    //    4   10:tableswitch     1 3: default 66
    //                   1 36
    //                   2 47
    //                   3 58
    //    5   36:getstatic       #5   <Field PrintStream System.out>
    //    6   39:ldc1            #6   <String "Jupiter">
    //    7   41:invokevirtual   #7   <Method void PrintStream.println(String)>
    //    8   44:goto            66
    //    9   47:getstatic       #5   <Field PrintStream System.out>
    //   10   50:ldc1            #8   <String "Venus">
    //   11   52:invokevirtual   #7   <Method void PrintStream.println(String)>
    //   12   55:goto            66
    //   13   58:getstatic       #5   <Field PrintStream System.out>
    //   14   61:ldc1            #9   <String "Earth">
    //   15   63:invokevirtual   #7   <Method void PrintStream.println(String)>
    //   16   66:return          
    }

    // Unreferenced inner class Main$1

/* anonymous class */
    static class _cls1
    {

        static 
        {
        //    0    0:invokestatic    #1   <Method Planet[] Planet.values()>
        //    1    3:arraylength     
        //    2    4:newarray        int[]
        //    3    6:putstatic       #2   <Field int[] $SwitchMap$Planet>
        // try 9 20 handler(s) 23
        //    4    9:getstatic       #2   <Field int[] $SwitchMap$Planet>
        //    5   12:getstatic       #3   <Field Planet Planet.JUPITER>
        //    6   15:invokevirtual   #4   <Method int Planet.ordinal()>
        //    7   18:iconst_1        
        //    8   19:iastore         
        //    9   20:goto            24
        // catch NoSuchFieldError
        //   10   23:astore_0        
        // try 24 35 handler(s) 38
        //   11   24:getstatic       #2   <Field int[] $SwitchMap$Planet>
        //   12   27:getstatic       #6   <Field Planet Planet.VENUS>
        //   13   30:invokevirtual   #4   <Method int Planet.ordinal()>
        //   14   33:iconst_2        
        //   15   34:iastore         
        //   16   35:goto            39
        // catch NoSuchFieldError
        //   17   38:astore_0        
        // try 39 50 handler(s) 53
        //   18   39:getstatic       #2   <Field int[] $SwitchMap$Planet>
        //   19   42:getstatic       #7   <Field Planet Planet.EARTH>
        //   20   45:invokevirtual   #4   <Method int Planet.ordinal()>
        //   21   48:iconst_3        
        //   22   49:iastore         
        //   23   50:goto            54
        // catch NoSuchFieldError
        //   24   53:astore_0        
        //   25   54:return          
        }

        static final int $SwitchMap$Planet[];
    }

}

実装についてのより詳細な記事が次のリンク先に書かれている。
Java列挙型メモ(Hishidama’s Java enum Memo)

トラックバック - http://d.hatena.ne.jp/happynow/20120505/1336227066