2009年3月22日星期日

Literal Pitfall

我们过去经常使用这样的方式定义常量, 比如我最不喜欢的java.util.Calendar类里面定义月份有
public static final int     APRIL  =   3
public static final int     MAY    =   4
public static final int     JUNE   =   5
...

我相信很多人也是这样定义常量或者枚举型。其实这样会有一个很严重的问题,和编译器的行为有关系。VM Spec 2.17.4中描述类初始化的发生条件时提到ClassA的某个常量字段比如ClassA.MAX被访问的时候不会导致ClassA类被初始化。

2.17.4 Initialization
........
A class or interface type T will be initialized immediately before one of the following occurs:

    * T is a class and an instance of T is created.

    * T is a class and a static method of T is invoked.

    * A nonconstant static field of T is used or assigned. A constant field is one that is (explicitly or implicitly) both final and static, and that is initialized with the value of a compile-time constant expression. A reference to such a field must be resolved at compile time to a copy of the compile-time constant value, so uses of such a field never cause initialization.

原因是如果ClassB引用ClassA.MAX,编译器会把ClassA.MAX的常量值复制到ClassB的常量池中。这样显然效率更高。

我们做一个实验,有两个类
public class ConstClass {
    public static final int TEST = 5;
}

public class RefClass {
    public static void main(String[] args) {
        System.out.println(ConstClass.TEST);
    }
}
编译后,执行RefClass明显应该打印5,我们用javap看一下pesudo code:
public class RefClass extends java.lang.Object{
public RefClass();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   iconst_5
   4:   invokevirtual   #3; //Method java/io/PrintStream.println:(I)V
   7:   return

}

这里你会看到iconst_5,RefClass并没有让VM加载ConstClass,事实上,你删除ConstClass.class也没有关系。
问题来了,如果你这样定义常量或者枚举值,将来如果ClassA.MAX的值你需要更改,那么你必须重新编译所有引用过这个值的类!那些类需要重新编译,这是非常难预测的,尤其是被频繁使用的API.

如何克服呢?有两种方式,一种是提供一个getTEST()来返回常量值,比如把刚才的类改成
public class ConstClass {
    public static final int TEST = 5;
    public static int getTEST() {
        return TEST;
    }
}
public class RefClass {
    public static void main(String[] args) {
        System.out.println(ConstClass.getTEST());
    }
}
由于getTEST()是static,编译器可能会inline他,因此效率不会太低

还有一种方式,是jdk1.5之后提供的enum
让我们重新写这两个类
public enum ConstClass2 {
    TEST(5);    
    private int value;
    ConstClass2(int v) {
        this.value = v;
    }
    public int getValue() {
        return this.value;
    }
}

public class RefClass2 {
    public static void main(String[] args) {
        System.out.println(ConstClass2.TEST.getValue());
    }
}
在用javap看看引用类
public class RefClass2 extends java.lang.Object{
public RefClass2();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   getstatic       #3; //Field ConstClass2.TEST:LConstClass2;
   6:   invokevirtual   #4; //Method ConstClass2.getValue:()I
   9:   invokevirtual   #5; //Method java/io/PrintStream.println:(I)V
   12:  return

}
你会发现这次5没有被复制到引用类的常量池,相反getstatic代替了iconst_,相当于ClassA.MAX会被解释成ClassA.getMAX(),这样效果其实和上面说的另外一种方法getTEST()是类似的。

其实enum还有其他的好处,JDK guide中说 (http://java.sun.com/j2se/1.5.0/docs/guide/language/enums.html)

  • Not typesafe - Since a season is just an int you can pass in any other int value where a season is required, or add two seasons together (which makes no sense).
  • No namespace - You must prefix constants of an int enum with a string (in this case SEASON_) to avoid collisions with other int enum types.
  • Brittleness - Because int enums are compile-time
    constants, they are compiled into clients that use them. If a new
    constant is added between two existing constants or the order is
    changed, clients must be recompiled. If they are not, they will still
    run, but their behavior will be undefined.
  • Printed values are uninformative - Because they are
    just ints, if you print one out all you get is a number, which tells
    you nothing about what it represents, or even what type it is.

其中第三点就是本文描述的问题,另外typesafe也是个问题,比如你完全可以把Integer.MAX_VALUE 传给 Calendar.set(Integer.MAX_VALUE, somevalue),另外没有命名空间而且打印出来也很不友好。

所以,总之,还是enum吧?

没有评论: