Перечисление Java и дополнительные файлы классов

Я заметил enums ввести много дополнительных файлов классов (класс $1) после компиляции, увеличивая общий размер. Кажется, он привязан к каждому классу, который даже использует перечисление, и они часто дублируются.

Почему это происходит и есть ли способ предотвратить это, не удаляя перечисление.

(Причиной для вопроса является то, что пространство для меня дороже)

РЕДАКТИРОВАТЬ

При дальнейшем рассмотрении проблемы Sun Javac 1.6 создает дополнительный синтетический класс каждый раз, когда вы используете переключатель Enum. Он использует какой-то тип SwitchMap. Этот сайт содержит больше информации, и здесь рассказывается, как анализировать, что делает Javac.

Дополнительный физический файл кажется дорогой платой каждый раз, когда вы используете переключатель enum!

Интересно, что компилятор Eclipe не создает эти дополнительные файлы. Интересно, единственное решение - это переключать компиляторы?

5 ответов

Решение

Я был просто немного об этом поведении, и этот вопрос появился, когда гуглил. Я думал, что поделюсь немного дополнительной информацией, которую я узнал.

javac 1.5 и 1.6 создают дополнительный синтетический класс каждый раз, когда вы используете переключатель enum. Класс содержит так называемую "карту переключателей", которая отображает индексы перечисления для переключения номеров таблицы переходов. Важно отметить, что синтетический класс создается для класса, в котором происходит переключение, а не для класса enum.

Вот пример того, что генерируется:

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 или же tableswitch Инструкция JVM. Он преобразует каждое значение перечисления в соответствующий индекс от 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 используется, если есть три или более случая переключения, поскольку он выполняет более эффективный поиск в постоянном времени по сравнению с lookupswitchлинейный поиск. Технически говоря, javac мог бы опустить весь этот бизнес с помощью синтетической карты переключения, когда он использует lookupswitch,

Предположение: у меня нет компилятора Eclipse для тестирования, но я думаю, что он не работает с синтетическим классом и просто использует lookupswitch, Или, возможно, для этого требуется больше переключателей, чем было проверено с оригинальным аскером, прежде чем он "вырастет" до tableswitch,

Я полагаю, что это сделано для предотвращения поломки ключей при изменении порядка перечисления, не перекомпилируя класс с переключателем. Рассмотрим следующий случай:

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() распечатал бы "Один" для ДВОИХ, и наоборот.

Файлы $1 и т. Д. Появляются, когда вы используете функцию "реализация метода для каждого экземпляра" перечислений Java, например:

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

    public abstract boolean foo();
}

Выше будут созданы три файла классов, один для базового перечислимого класса и один для YEA и NAY для хранения различных реализаций foo().

На уровне байт-кода перечисления являются просто классами, и для того чтобы каждый экземпляр перечисления реализовывал метод по-разному, для каждого экземпляра должен быть свой класс,

Однако это не учитывает дополнительные файлы классов, сгенерированные для пользователей перечисления, и я подозреваю, что они являются просто результатом анонимных классов и не имеют ничего общего с перечислениями.

Таким образом, чтобы избежать создания таких дополнительных файлов классов, не используйте реализации методов для каждого экземпляра. В случаях, таких как выше, где методы возвращают константы, вы можете вместо этого использовать открытое окончательное поле, установленное в конструкторе (или частное поле с открытым получателем, если хотите). Если вам действительно нужны методы с разной логикой для разных экземпляров enum, то вы не можете избежать дополнительных классов, но я бы посчитал это довольно экзотической и редко необходимой функцией.

В Java перечисления на самом деле являются просто классами с добавленным синтаксическим сахаром.

Поэтому каждый раз, когда вы определяете новое перечисление, компилятор Java создаст для вас соответствующий файл Class. (Независимо от того, насколько просто перечисление).

Нет способа обойти это, если не использовать перечисления.

Если пространство является премиальным, вы всегда можете просто использовать константы.

Учитывая, что такое поведение Java известно не всем Java-разработчикам, я создал несколько видеороликов, объясняющих, как работают операторы Switch в Java.

  1. Переключитесь с помощью перечислений - https://www.youtube.com/watch?v=HlsPHEB_xz4
  2. Переключатель с помощью струн - https://www.youtube.com/watch?v=cg9O815FeWY
  3. О TableSwitch и LookupSwitch - https://www.youtube.com/watch?v=OHwDczHbPcw
  4. Переключить выражение в Java 13 - https://www.youtube.com/watch?v=suFn87Irpb4

Это может не дать прямого ответа на вопрос. Однако он определенно отвечает на вопрос, как работают операторы switch в Java.

Насколько я знаю, учитывая перечисление Operation вы получите дополнительные файлы классов, исключая очевидные Operation.classи по одному на каждое перечисление, если вы используете abstract method как этот:

enum Operation {

   ADD {
      double op(double a, double b) { 
          return a + b;
      }
   },

   SUB {
      double op(double a, double b) { 
          return a - b;
      }
   };

   abstract double op(double a, double b);
}
Другие вопросы по тегам