Java 外化与瞬态

Java Externalization vs Transient

我在考虑外部化的目的,因为您可以简单地将 属性 标记为 transient 并阻止其序列化。但是,经过进一步研究,我发现如果您需要在 运行 时间决定需要什么,这种方法(即标记为 transient)可能并不理想。从理论上讲,这对我来说很有意义。但是,实际上我看不出外部化如何更 运行 时间友好。我的意思是,您仍然必须在 class 的定义期间决定 writeExternal()readExternal() 中需要或不需要的内容。那么,如何更 运行 时间友好?

强调这一点的文件如下,

If everything is automatically taken care by implementing the Serializable interface, why would anyone like to implement the Externalizable interface and bother to define the two methods? Simply to have the complete control on the process. OKay... let's take a sample example to understand this. Suppose we have an object having hundreds of fields (non-transient) and we want only few fields to be stored on the persistent storage and not all. One solution would be to declare all other fields (except those which we want to serialize) as transient and the default Serialization process will automatically take care of that. But, what if those few fields are not fixed at design tiime instead they are conditionally decided at runtime. In such a situation, implementing Externalizable interface will probably be a better solution. Similarly, there may be scenarios where we simply don't want to maintain the state of the Superclasses (which are automatically maintained by the Serializable interface implementation).

public class Foo implements Externalizable{
    private long userID;
    private String userName;
    private char[] userPassword;
    private int age;

    private boolean shouldSavePassword;

    public void setSavePassword(boolean shouldSavePassword){
        this.shouldSavePassword = shouldSavePassword;
    }

    void writeExternal(ObjectOutput out) throws IOException{
        out.writeObject(userID);
        out.writeObject(userName);
        out.writeObject(shouldSavePassword);

        if(shouldSavePassword){
            out.writeObject(userPassword);
        }

        out.writeObject(age);
    }

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException{
        userID = in.readLong();
        userName = (String) in.readObject();
        shouldSavePassword = readBoolean();

        if(shouldSavePassword){
            userPassword = (char[]) in.readObject();
        }

        age = in.readInt();
    }
}

请注意字段 userPassword 如何仅根据 shouldSavePassword 的运行时值进行序列化。如果您声明了该字段为 transient,那么您已经决定是否在编译时序列化 属性,无法在运行时更改(除非通过反射)。

Externalizable 的灵活性还允许您确定自己的序列化方案,在对象敏感时根据需要对其进行加密。

另一个用例可能是将单向散列附加到 class 末尾以获得最大可靠性的选项。一个字段可以决定是否保存散列(因为它是额外的计算)。

最重要的是,transient 不会让您对如何完成对象的序列化进行任何运行时控制,只是该字段将被序列化或不被序列化(作为编译时参数).


免责声明:上面给出的示例是一个糟糕的密码保存方案,请勿将其用于任何生产应用程序。明文密码在通过bcrypt、PBKDF#2、scrypt等PBPDF后应保存

我想指出,在比较 SerializableExternalizable 方法时,还有其他 advantages/disadvantages 需要考虑。

外化速度更快

在序列化过程中,JVM 总是首先检查 class 是否可外部化。如果是这种情况,那么它将使用 read/writeExternal 方法。 (有道理,对)

Externalizable classes 需要较少的递归,因为您可以精确地识别您需要的数据。它还会产生更紧凑的输出(更少的字节),这将我们带到下一点......

外化输出更紧凑

如果你比较实际的输出,它看起来像这样: object 的 header 包含一个标记,标记 class 是 Serializable 还是 Externalizable.

OBJECT
CLASSDESC
  Class Name: "MyClassName"
  Class UID:  ...
  Class Desc Flags: SERIALIZABLE or EXTERNALIZABLE

如果它只是 SERIALIZABLE,则将跟随一个字段列表(如定义),然后是实际数据。这对每个序列化的 object.

重复
  Field Count: ...
  // followed by an bunch of declarations of objects
  Field type: object
  Field name: "fieldName"
  Class name: "Ljava/lang/String;"

 // followed by the actual data
 STRING: "foo"
 STRING: "bar"
 float: 123456

可外部化 objects 不包含字段和数据列表,它们只包含按您保存顺序排列的编码数据。

  EXTERNALIZABLE: [00 AA 00 BC ... ]

外化更灵活

如果您保存购物清单,那么您只需要产品名称,对吗?

public class ShoppingList implements Externalizable {
  String name;
  List<Product> productList;     

  @Override
  public void writeExternal(ObjectOutput pOutput) throws IOException
  {
    out.writeUTF(name);
    for (Product product : productList)
    {
      // save only product id
      out.writeUTF(product.getEanCode());
    }
  }
  ...
}

但是如果你正在做帐单,那么你也想节省价格吧?

public class Bill implements Externalizable {
  String name;
  List<Product> productList;     

  @Override
  public void writeExternal(ObjectOutput pOutput) throws IOException
  {
    out.writeUTF(name);
    for (Product product : productList)
    {
      // save product id and price
      out.writeUTF(product.getEanCode());
      out.writeInt(product.getPrice());
    }
  }
  ...
}

因此,在某些情况下价格是暂时的,而在某些情况下则不是。您将如何使用 transient 关键字解决此问题? -- 我会让你弄清楚这一点。 这种灵活性在仅使用 transient 关键字时确实是缺乏的。

设计注意事项

然而,也存在一些危险。 Externalizable objects 只能为 objects 使用 public 默认构造函数 (无参数的 public 构造函数)实现。

这使得 不可能使 non-static 成为内部 classes Externalizable。问题在于 JVM 在运行时修改构造函数,并在编译期间添加对 parent class 的引用。因此,您不能为 non-static 内部 class.

设置默认的 no-argument 构造函数

您还必须考虑将来修改您的object的可能性(例如添加non-transient字段)。可序列化 classes 可能存在向后兼容性问题,但本身不需要更改代码。 Externalizable classes 将需要在您的 read/write 方法中更改代码,但有更多选项来处理兼容性问题。

还有一件事。如果您选择此 "technology" 来在不同的应用程序之间进行通信 ,那么请不要这样做。你要的是JAXB。它不那么紧凑,但更反式parent,没有兼容性问题,而且同样灵活。

隐藏功能

为了完整,还有一件事使这个话题变得有点复杂。实际上完全可以在不使用 Externalizable 接口的情况下使用 read/write 方法。在引入 Externalizable 之前,可以定义 private writeObjectreadObject 方法。但真的,你不应该再使用那种方法了。