Java 枚举和其他类文件

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

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

(问题的原因是空间对我来说很重要)

剪辑

关于进一步调查这个问题,Sun 的 每次在 Enum 上使用开关时,Java1.6都会创建一个额外的合成类。它使用某种交换地图。这个站点有一些更多的信息,给你告诉你如何分析 Javac 正在做什么。

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

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

15970 次浏览

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

因此,当您定义一个新的枚举时,Java 编译器将为您创建一个对应的 Class 文件。(无论枚举多么简单)。

除了不使用枚举外,没有其他方法可以解决这个问题。

如果空间是一个溢价,您总是可以使用常量代替。

当您使用 Java 枚举的“每实例方法实现”特性时,会出现 $1 etc 文件,如下所示:

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


public abstract boolean foo();
}

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

在字节码级别上,枚举只是类,为了让每个枚举实例以不同的方式实现一个方法,每个实例都需要一个不同的类,

但是,这并没有考虑到为枚举用户生成的其他类文件,我怀疑这些只是匿名类的结果,与枚举没有任何关系。

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

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

我只是被这种行为咬了一口,这个问题在谷歌搜索时出现了。我觉得我应该分享一下我发现的额外信息。

Javac 1.5和1.6每次在枚举上使用开关时都会创建一个额外的合成类。该类包含一个所谓的“开关映射”,它将枚举索引映射为开关表跳转编号。重要的是,合成类是为发生开关的类创建的,没有是枚举类。

下面是一个生成的例子:

Java

public enum EnumClass { VALUE1, VALUE2, VALUE3 }

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

然后使用这个开关映射为 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,因为它执行更有效的恒定时间查找相对于 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()将为两个打印出“一”,反之亦然。

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

  1. 使用 Enums-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 中 switch 语句的工作原理。