我特别关心在 switch 语句中使用枚举。我将使用一个程序来计算一个非常长的数组中有限符号集的出现次数。
先定义一些常量,
static final int NUMSYMBOLS = Integer.MAX_VALUE/100; // size of array
// Constants for symbols ZERO ... NINE
static final int ZERO_I =0, ONE_I =1, TWO_I =2, THREE_I =3, FOUR_I =4;
static final int FIVE_I =5, SIX_I =6, SEVEN_I =7, EIGHT_I =8, NINE_I =9;
以及相应的枚举。
enum Symbol {
ZERO (0), ONE (1), TWO (2), THREE (3), FOUR (4),
FIVE (5), SIX (6), SEVEN (7), EIGHT (8), NINE (9);
final int code;
Symbol(int num) {
code = num;
}
public final int getCode() {
return code;
}
}
枚举具有由构造函数设置的字段代码。我们将在测试中使用此代码
稍后,这可以产生一些加速。
符号集合存储在一个数组中,以及一个对应的 int 数组。
Symbol[] symbolArray;
int[] intArray;
符号在方法中进行计数。
void testEnum() {
for(int i=0;i<NUMSYMBOLS;++i) {
Symbol sym = symbolArray[i];
switch(sym) {
case ZERO: ++numZero; break;
case ONE: ++numOne; break;
case TWO: ++numTwo; break;
case THREE: ++numThree; break;
case FOUR: ++numFour; break;
case FIVE: ++numFive; break;
case SIX: ++numSix; break;
case SEVEN: ++numSeven; break;
case EIGHT: ++numEight; break;
case NINE: ++numNine; break;
default: break;
}
}
}
和类似的整数方法。
void testInteger() {
for(int i=0;i<NUMSYMBOLS;++i) {
int num = intArray[i];
switch(num) {
case ZERO_I: ++numZero; break;
case ONE_I: ++numOne; break;
case TWO_I: ++numTwo; break;
case THREE_I: ++numThree; break;
case FOUR_I: ++numFour; break;
case FIVE_I: ++numFive; break;
case SIX_I: ++numSix; break;
case SEVEN_I: ++numSeven; break;
case EIGHT_I: ++numEight; break;
case NINE_I: ++numNine; break;
default:
break;
}
}
}
我们可以使用 Enum 中的代码来提高切换效率。
void testEnumCode() {
for(int i=0;i<NUMSYMBOLS;++i) {
Symbol sym = symbolArray[i];
switch(sym.getCode()) { // Uses the code here
case ZERO_I: ++numZero; break;
case ONE_I: ++numOne; break;
case TWO_I: ++numTwo; break;
case THREE_I: ++numThree; break;
case FOUR_I: ++numFour; break;
case FIVE_I: ++numFive; break;
case SIX_I: ++numSix; break;
case SEVEN_I: ++numSeven; break;
case EIGHT_I: ++numEight; break;
case NINE_I: ++numNine; break;
default:
break;
}
}
}
这三种方法各运行 10 次。给出以下时间。
总计 enum 2,548,251,200ns 代码 2,330,238,900ns int 2,043,553,600ns
百分比 enum 100% code 91% int 80%
显着缩短了使用整数的时间。使用代码字段可以在枚举和整数之间提供中间时间。
这些时间上的差异很容易被周围的代码消失。例如,使用 ArrayList 而不是数组会使时间差异消失
完全地。
使用Enum.ordinal() 方法还有另一种选择。这具有类似于使用 getCode() 的性能。 Is it good practice to use ordinal of enum? 讨论了这种方法的原因和位置。
在我的应用程序中,一个反向波兰计算器,这个循环和 switch 语句,是程序的核心,运行了数百万次,它出现在性能分析中。
枚举用于操作码:PUSH、POP 等,每个命令都包含一个带有附加参数的操作码。
enum OpCode {
PUSH(0), POP(1), ...;
private final int code;
OpCode(int n) { code=n; }
public int getCode() { return code; }
}
class Command {
OpCode op;
int code;
String var;
Command (OpCode op,String name) {
this.op = op;
this.code = op.getCode();
this.var = name;
}
}
构建命令列表可以使用枚举,而不需要知道实际的 int 值。
Command com = new Command(OpCode.PUSH,"x");
对于代码的非关键部分,我们可以在开关中使用枚举。在 Command 的 toString() 方法中说。
public String toString() {
switch(op) {
case PUSH:
return "Push "+var;
....
}
}
但关键部分可以使用代码。
public void evaluate(Command com) {
switch(com.code) {
case 0:
stack.push(com.var);
break;
....
}
}
为了那额外的性能。
switch 语句的字节码很有趣。在 int 示例中,swicth 语句编译为:
private void testInteger(int);
Code:
0: iload_1
1: tableswitch { // 0 to 9
0: 56
1: 69
2: 82
3: 95
4: 108
5: 121
6: 134
7: 147
8: 160
9: 173
default: 186
}
56: aload_0
57: dup
58: getfield #151 // Field numZero:I
61: iconst_1
62: iadd
63: putfield #151 // Field numZero:I
66: goto 186
69: aload_0
70: dup
71: getfield #153 // Field numOne:I
74: iconst_1
75: iadd
76: putfield #153 // Field numOne:I
79: goto 186
....
tableswitch 命令根据值有效地在代码中向前跳转。
使用代码(或序数)的开关代码类似。只需额外调用 getCode() 方法。
private void testCode(toys.EnumTest$Symbol);
Code:
0: aload_1
1: invokevirtual #186 // Method toys/EnumTest$Symbol.getCode:()I
4: tableswitch { // 0 to 9
0: 60
1: 73
2: 86
3: 99
4: 112
5: 125
6: 138
7: 151
8: 164
9: 177
default: 190
....
仅使用枚举代码更复杂。
private void testEnum(toys.EnumTest$Symbol);
Code:
0: invokestatic #176
// Method $SWITCH_TABLE$toys$EnumTest$Symbol:()[I
3: aload_1
4: invokevirtual #179 // Method toys/EnumTest$Symbol.ordinal:()I
7: iaload
8: tableswitch { // 1 to 10
1: 64
2: 77
3: 90
4: 103
5: 116
6: 129
7: 142
8: 155
9: 168
10: 181
default: 194
}
这里首先调用了一个新方法 $SWITCH_TABLE$toys$EnumTest$Symbol:()
此方法创建一个数组,将序数值转换为开关中使用的索引。基本上相当于
int[] lookups = get_SWITCH_TABLE();
int pos = array[sys.ordinal()];
switch(pos) {
...
}
切换表创建方法,在第一次调用时计算一次表,并在每次后续调用时使用同一个表。因此,与整数情况相比,我们看到了两个非常简单的函数调用和一个额外的数组查找。