web-dev-qa-db-ja.com

Java列挙型および追加のクラスファイル

コンパイル後に合計サイズが肥大化した後、enumsが多くの追加のクラスファイル(Class $ 1)を導入することに気づきました。列挙型を使用するすべてのクラスに関連付けられているようで、これらはしばしば重複しています。

なぜこれが発生するのでしょうか。列挙型を削除せずにこれを防ぐ方法はありますか。

(質問の理由は、スペースが私にとって貴重であるということです)

[〜#〜]編集[〜#〜]

この問題をさらに調査すると、SunのJavac 1.6は、Enumのスイッチを使用するたびに追加の合成クラスを作成します。ある種のSwitchMapを使用します。 This サイトにはさらに情報があり、 here はJavacが実行していることを分析する方法を示しています。

追加の物理ファイルは、列挙型のスイッチを使用するたびに支払うのは高額のようです。

興味深いことに、Eclipeのコンパイラはこれらの追加ファイルを生成しません。唯一の解決策はコンパイラを切り替えることだろうか?

62
Pool

私はこの振る舞いに少しだけ触れました、そしてこの質問はグーグルの時に現れました。私が見つけた追加情報を少し共有したいと思いました。

javac 1.5および1.6は、列挙型でスイッチを使用するたびに、追加の合成クラスを作成します。このクラスには、列挙型インデックスをスイッチテーブルジャンプ番号にマップする、いわゆる「スイッチマップ」が含まれています。重要なのは、切り替えが発生するクラスではなく列挙型クラスに対して合成クラスが作成されることです。

生成されるものの例を次に示します。

EnumClass.Java

public enum EnumClass { VALUE1, VALUE2, VALUE3 }

EnumUser.Java

public class EnumUser {
    public String getName(EnumClass value) {
        switch (value) {
            case VALUE1: return "value 1";
            // No VALUE2 case.
            case VALUE3: return "value 3";
            default:     return "other";
        }
    }
}

合成EnumUser $ 1.class

class EnumUser$1 {
    static final int[] $SwitchMap$EnumClass = new int[EnumClass.values().length];

    static {
        $SwitchMap$EnumClass[EnumClass.VALUE1.ordinal()] = 1;
        $SwitchMap$EnumClass[EnumClass.VALUE3.ordinal()] = 2;
    };
}

次に、このスイッチマップを使用して、lookupswitchまたはtableswitchJVM命令のインデックスを生成します。各列挙値を1から[スイッチケースの数]までの対応するインデックスに変換します。

EnumUser.class

public Java.lang.String getName(EnumClass);
  Code:
   0:   getstatic       #2; //Field EnumUser$1.$SwitchMap$EnumClass:[I
   3:   aload_1
   4:   invokevirtual   #3; //Method EnumClass.ordinal:()I
   7:   iaload
   8:   lookupswitch{ //2
                1: 36;
                2: 39;
                default: 42 }
   36:  ldc     #4; //String value 1
   38:  areturn
   39:  ldc     #5; //String value 3
   41:  areturn
   42:  ldc     #6; //String other
   44:  areturn

tableswitchは、スイッチケースが3つ以上ある場合に使用されます。これは、lookupswitchの線形検索よりも効率的な定数時間ルックアップを実行するためです。技術的に言えば、javacは、lookupswitchを使用する場合、合成スイッチマップを使用してこのビジネス全体を省略できます。

推測:テスト用のEclipseのコンパイラは手元にありませんが、合成クラスを気にせず、単にlookupswitchを使用していると思います。または、tableswitchに「ugprades」する前にテストした元のaskerよりも多くのスイッチケースが必要になる可能性があります。

58
John Kugelman

$ 1などのファイルは、次のように、Javaの列挙型の「インスタンスごとのメソッド実装」機能を使用すると発生します。

public enum Foo{
    YEA{
        public void foo(){ return true };
    },
    NAY{
        public void foo(){ return false };
    };

    public abstract boolean foo();
}

上記では、3つのクラスファイルが作成されます。1つは基本列挙型クラス用で、もう1つはYEAとNAY用で、foo()のさまざまな実装を保持します。

バイトコードレベルでは、列挙型は単なるクラスであり、列挙型インスタンスごとにメソッドを異なる方法で実装するには、インスタンスごとに異なるクラスが必要です。

ただし、これは列挙型のユーザー用に生成された追加のクラスファイルを考慮しておらず、これらは匿名クラスの結果であり、列挙型とは何の関係もないと思います。

したがって、このような余分なクラスファイルが生成されないようにするために、インスタンスごとのメソッド実装を使用しないでください。メソッドが定数を返す上記のような場合は、代わりにコンストラクターで設定されたパブリックfinalフィールド(または、必要に応じてパブリックゲッターを持つプライベートフィールド)を使用できます。列挙型インスタンスごとにロジックが異なるメソッドが本当に必要な場合は、余分なクラスを回避することはできませんが、かなりエキゾチックで、めったに必要とされない機能だと思います。

6

これは、列挙型の順序が変更された場合にスイッチが破損するのを防ぎ、スイッチを使用してクラスを再コンパイルしないために行われると思います。次の場合を考えてみましょう。

_enum A{
    ONE, //ordinal 0
    TWO; //ordinal 1
}
class B{
     void foo(A a){
         switch(a){
              case ONE:
                   System.out.println("One");
                   break;
              case TWO:
                   System.out.println("Two");
                   break;
         }
     }
}
_

スイッチマップがないと、foo()は大まかに次のように変換されます。

_ void foo(A a){
         switch(a.ordinal()){
              case 0: //ONE.ordinal()
                   System.out.println("One");
                   break;
              case 1: //TWO.ordinal()
                   System.out.println("Two");
                   break;
         }
     }
_

Caseステートメントはコンパイル時定数でなければならないため(メソッド呼び出しではないなど)。この場合、Aの順序を切り替えると、foo()はTWOに対して「One」を出力し、その逆も同様です。

5
TDJoe

Javaでは、列挙は実際にはいくつかの構文糖衣構文が適用された単なるクラスです。

したがって、新しい列挙型を定義するときはいつでも、Javaコンパイラーが対応するクラスファイルを作成します(列挙型がどれほど単純であっても)。

これを回避する方法はありません。それ以外の場合は、列挙を使用しません。

スペースが重要な場合は、代わりに常に定数を使用できます。

1
vicsz