【问题标题】:Java enum and additional class filesJava 枚举和其他类文件
【发布时间】:2021-03-19 15:11:46
【问题描述】:

我注意到enums 在编译后引入了许多额外的类文件(Class$1)使总大小膨胀。它似乎附加到甚至使用枚举的每个类中,而且这些通常是重复的。

为什么会发生这种情况,有没有办法在不删除枚举的情况下防止这种情况发生。

(问题的原因是空间对我来说非常宝贵)

编辑

在进一步调查该问题时,Sun 的 Javac 1.6 会在您每次在 Enum 上使用开关时创建一个额外的合成类。它使用某种 SwitchMap。 This 站点有更多信息,here 告诉你如何分析 Javac 在做什么。

每次在枚举上使用开关时,额外的物理文件似乎要付出高昂的代价!

有趣的是,Ecipe 的编译器不会生成这些附加文件。我想知道是否唯一的解决方案是切换编译器?

【问题讨论】:

  • Class$n 类文件是匿名内部类。我大量使用了枚举,但没有看到这一点。你能发布一个源文件吗?
  • 你担心什么样的“臃肿”?希望不是磁盘空间。 :) 如果是下载大小,我可以推荐.pack200.gz(前几天我天真地打包了 CORBA 作为一个实验——最著名的 Java bloats 都在 48K 以下)。
  • 是的,它是applet 打包Jar 文件的下载大小。我正在尝试采取措施在可能的情况下对其进行压缩(没有任何源代码级别的更改)。 pack200 适用于这种情况吗?

标签: java enums


【解决方案1】:

在 Java 中,枚举实际上只是添加了一些语法糖的类。

所以每当你定义一个新的枚举时,Java 编译器都会为你创建一个对应的 Class 文件。 (不管枚举多么简单)。

没有办法解决这个问题,除了不使用枚举。

如果空间很宝贵,您总是可以只使用常量。

【讨论】:

    【解决方案2】:

    当您使用 Java 枚举的“按实例方法实现”功能时,会出现 $1 等文件,如下所示:

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

    上面将创建三个类文件,一个用于基本枚举类,一个用于 YEA 和 NAY,以保存 foo() 的不同实现。

    在字节码层面,枚举只是类,为了让每个枚举实例实现不同的方法,每个实例都需要有不同的类,

    但是,这并没有考虑为枚举用户生成的额外类文件,我怀疑这些只是匿名类的结果,与枚举无关。

    因此,为了避免生成此类额外的类文件,请不要使用每个实例的方法实现。在上述方法返回常量的情况下,您可以使用在构造函数中设置的公共 final 字段(如果您愿意,也可以使用带有公共 getter 的私有字段)。如果你真的需要为不同的枚举实例提供不同逻辑的方法,那么你就无法避免额外的类,但我认为它是一个相当奇特且很少需要的功能。

    【讨论】:

    • 嗨,感谢您的回答,但在这种情况下并非如此(请参阅上面的编辑)。
    • 非常有趣 - 我想说,在这种情况下,有必要将您的发现作为单独的答案发布并接受。
    【解决方案3】:

    据我所知,给定一个名为 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);
    }
    

    【讨论】:

      【解决方案4】:

      我只是被这种行为所吸引,并且在谷歌搜索时出现了这个问题。我想我会分享一些我发现的额外信息。

      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";
              }
          }
      }
      

      综合枚举用户$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;
          };
      }
      

      然后使用此开关映射为lookupswitchtableswitch 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 用于如果有三个或更多 switch 案例,因为它执行比lookupswitch 的线性搜索更有效的恒定时间查找。从技术上讲,当 javac 使用 lookupswitch 时,它可以通过合成开关映射省略整个业务。

      推测:我手头没有 Eclipse 的编译器可供测试,但我想它不会打扰合成类,而只是使用 lookupswitch。或者,在它“升级”到tableswitch 之前,它可能需要比测试的原始询问器更多的开关案例。

      【讨论】:

      • 一则轶事:我今天遇到了这个(特别是 Eclipse 的 [JDK 8] 编译器没有这样做)以及这种行为:coderanch.com/t/457290/Testing/Test-class-public-constructor -- 这意味着在本地运行的测试有些失败在 Jenkins 上莫名其妙,因为后者的配置不够严格,并且编译器的行为与本地编译器不同。由于我们无法完全控制我们的构建,我们可能不得不暂时避免在单元测试中切换枚举。
      【解决方案5】:

      我相信这样做是为了防止在更改枚举的顺序时开关中断,而不是用开关重新编译类。考虑以下情况:

      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;
               }
           }
      }
      

      如果没有 switch 映射,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() 将打印出“One”为 TWO,反之亦然。

      【讨论】:

        【解决方案6】:

        考虑到并非所有 Java 开发人员都知道 Java 的这种行为,我制作了一些视频来解释 Java 中的 Switch 语句如何工作。

        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

        这可能无法直接回答问题。但是,它确实回答了 Java 中 switch 语句的工作原理。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2022-08-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多