2009年3月23日星期一

Don't cache Singleton object in a serializable object

不要在可序列化对象中缓存Singleton

如果你有一个对象其中某个字段保存了一个对Singleton的引用,那么这个对象在序列化读取后会导致在同一个虚拟机里Singleton对象有两个。比如下面的例子

public class Test3 {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        System.out.println(ABC.getInstance());
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        new ObjectOutputStream(out).writeObject(ABC.getInstance());
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()));
        ABC abc = (ABC) in.readObject();
        System.out.println(abc);
        
    }
}

class ABC implements Serializable {
    private ABC() {}
    private static ABC instance = new ABC();
    public static ABC getInstance() {
        return instance;
    }
}

你会发现,打印出的两个ABC的instance是不一样的。解决方法很简单,在ABC加入这个方法
    protected Object readResolve() {
        return instance;
    }

这样可以重置instance到这个虚拟机中的Singleton实例,还可以把引用instance的字段作为transient字段节省IO时间。
但是这不是最好的办法,最好就是,如果你知道ABC是Singleton,那么就永远不要把ABC赋值到你的对象成员变量里,getInstance()因为是static方法,编译器通常会做inline的,频繁调用不会导致频繁方法栈操作,所以缓存它意义不大。

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吧?