Java多种方式自定义序列化
2016-12-09 16:04
302 查看
原文地址:http://blog.csdn.net/huzhigenlaohu/article/details/51674513
我们知道,通过实现
通过如下的代码
编译过后,运行命令
就会打印出
在这个示例中,我借助了管道来连接输出流与输入流:
但是有时候,可能是由于独特的序列化需求、性能方面的考虑又或是希望在对象反序列化后能够执行其他操作,我们需要重写这个默认的序列化实现。这时候,我们可以通过如下两个方法来实现。
这两个私有方法分别实现了对象的序列化与反序列化操作,完整例子如下。
此时,
与
使用
这里,我们发现除了添加了
当使用
由于
首先必须实现java.io.Serializable,即使子类实现了此接口,父类仍需实现此接口,除非子类对父类状态进行了自定义的序列化处理。
1. 实现接口java.io.Externalizable
void writeExternal(ObjectOutput
out) throws IOException;
void readExternal(ObjectInput
in) throws IOException, ClassNotFoundException;
注意实现此接口,对象必须有public声明的空构造器。
2. 实现方法writeObject和readObject
private void writeObject(ObjectOutputStream os) throws IOException
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException
它们有个好处是可以调用以下接口先进行默认的Java序列化,再进行自定义的序列化。
ObjectOutputStream.defaultWriteObject()和ObjectInputStream.defaultReadObject()
实现这些方法接口,对象可不需要空构造器,且可以是private的。
注意如果实现了接口java.io.Externalizable,这两个方法且父类的这两方法都不会被调用。否则父类的这两方法先被调用,然后才是子类。
JDK源码参考: java.util.ArrayList
3. 实现方法writeReplace和readResolve
private Object writeReplace()
private Object readResolve()
通过它们可以返回不同的类型。
对于writeReplace,仍然需要对新类型进行序列化。
对于readResolve,其调用在readExternal或readObject之后。
JDK源码参考: java.util.EnumSet
4. 默认序列化自定义包括关键字transient和静态字段名serialPersistentFields
transient 用于指定哪个字段不被默认序列化,如public transient int a;
serialPersistentFields 用于指定哪些字段需要被默认序列化,如
Java代码
private static final ObjectStreamField[] serialPersistentFields =
{
new ObjectStreamField("name", String.class),
new ObjectStreamField("a", Integer.TYPE)
};
如果同时定义了serialPersistentFields与transient,transient会被忽略。
5. serialVersionUID
序列化的时候会读写serialVersionUID并做出校验,没做如下定义的话会根据类签名信息进行生成,包括类名、非私有构造器、非私有类方法、非私有静态类属性,但不包括继承层次,也就是继承层次发生变化也不会报类错误,但是父类的状态信息将得不到反序列化。
private static final long serialVersionUID = 2184568476863030694L;
6. 相关异常
java.io.NotSerializableException
java.lang.ClassNotFoundException
java.io.InvalidClassException (如serialVersionUID校验失败,原生字段类型不一致等)
java.lang.ClassCastException (如自定义属性类型或自身类型不一致)
java.io.StreamCorruptedException
java.io.EOFException (自定义序列化时试图读取更多的消息)
序列化机制对比
实现Serializable接口:
系统自动存储必要的信息;
Java内建支持,抑郁实现,只需要实现该接口即可,无需任何代码支持;
性能略差;
实现Externalizable接口:
程序员决定存储那些信息;
仅仅提供两个空方法,实现该接口必须为两个空方法提供实现;
性能略好;
对象序列化注意点
对象的类名、Field(包括基本类型、数组、对其它对象的引用)都会被序列化;方法、static Field(即静态Field)、transient Field(也被称为瞬态Field)都不会被序列化;
实现Serializable接口如果需要让某个Field不被序列化,则在该Field前加transient修饰符,而不是static关键字,虽然static关键字可以达到这个效果;
保证序列化对象的Field类型也是可序列化,否则需要使用transient关键字来修饰该Field,要不然该类是不可序列化的;
反序列化对象时必须有序列化对象的class文件;
当通过文件、网络来读取序列化的对象时,必须按实际写入顺序读取;
版本
反序列化Java对象时,必须提供该对象的class文件,随着项目的升级,系统的class文件也会升级。如果保持两个class文件兼容性:
Java的序列化机制允许为序列化类提供一个private static final的serialVersonUID值,该Field值用于标识该Java类的序列化版本;
最好在每个要序列化的类中加入private static final long serialVersionUID这个Field,具体数值自己定义。这样,即使对象被序列化之后,它所对应的类修改了,该对象也依然可以被正确反序列化;
如果不显示定义serialVersionUID值:
该Field值将由JVM根据类的相关信息计算,而修改后的类计算结果与修改前的类计算结果往往不同,从而造成对象的反序列化因为类版本的不兼容失败;
不利于程序在不同的JVM之间移植,因为不同的编译器计算该Field的值计算策略可能不同,从而造成虽然类完全没有改变,但是因为JVM不同,也会出现序列化版本不兼容而无法正确反序列化的现象;
我们知道,通过实现
Java.io.Serializable接口可以使得该类的实例能够被序列化。例如如下的
Person类,
import java.io.Serializable; public class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return String.format("My name is %s, and I'm %d years old.", name, age); } }
通过如下的代码
import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Serializing { public static void main(String ...flags) throws Exception { if (flags.length != 1) { System.exit(1); return; } String flag = flags[0]; if ("-o".equals(flag)) { Person alice = new Person("Alice", 10); ObjectOutputStream out = new ObjectOutputStream(System.out); out.writeObject(alice); } else if ("-i".equals(flag)) { ObjectInputStream in = new ObjectInputStream(System.in); Person person = (Person) in.readObject(); System.out.println(person); } else { System.err.printf("unknown flag: %s%n", flag); System.exit(1); } } }
编译过后,运行命令
java -cp target/classes/ Serializing -o | java -cp target/classes/ Serializing -i
就会打印出
My name is Alice, and I'm 10 years old.
在这个示例中,我借助了管道来连接输出流与输入流:
Person类的
alice实例先是通过对象输出流(基于
System.out)输出,再由对象输入流(基于
System.in)读入。在对象的序列化过程,
ObjectOutputStream.writeObject与
ObjectInputStream.readObject方法将对象以Java默认的序列化方式实现了
alice实例与二进制流之间的转换。
但是有时候,可能是由于独特的序列化需求、性能方面的考虑又或是希望在对象反序列化后能够执行其他操作,我们需要重写这个默认的序列化实现。这时候,我们可以通过如下两个方法来实现。
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException
这两个私有方法分别实现了对象的序列化与反序列化操作,完整例子如下。
import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.nio.charset.Charset; public class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return String.format("My name is %s, and I'm %d years old.", name, age); } private void writeObject(ObjectOutputStream out) throws IOException { int strLen = (name == null) ? -1 : name.length(); out.writeInt(strLen); if (strLen > 0) { out.write(name.getBytes(Charset.forName("UTF-8"))); } out.writeInt(age); } private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { int strLen = in.readInt(); if (strLen <= -1) { name = null; } else if (strLen == 0) { name = ""; } else { byte[] strBytes = new byte[strLen]; in.readFully(strBytes); name = new String(strBytes, Charset.forName("UTF-8")); } age = in.readInt(); } }
此时,
ObjectOutputStream.writeObject以及
ObjectInputStream.readObject就会分别使用我们自定义的私有
writeObject以及
readObject方法来做序列化,而不是使用用默认的序列化实现。由于这两个方法是私有的,那就意味着
Person的子类序列化方法既不会继承,也不会覆盖。也就是说,对于整个继承层次中的类,都会从父类至子类依次调用序列化操作。但是有的时候,我们希望子类复用父类的序列化实现,又或者子类重写父类的序列化实现,那么这时候我们就需要用到
java.io.Externalizable接口了。
与
java.io.Serializable接口中的那两个私有方法类似,
java.io.Externalizable接口具有两个公有的抽象方法:
public void writeExternal(ObjectOutput out) throws IOException
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
使用
Externalizable接口的
Person类如下。
import java.io.*; import java.nio.charset.Charset; public class Person implements Externalizable { private String name; private int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return String.format("My name is %s, and I'm %d years old.", name, age); } @Override public void writeExternal(ObjectOutput out) throws IOException { int strLen = (name == null) ? -1 : name.length(); out.writeInt(strLen); if (strLen > 0) { out.write(name.getBytes(Charset.forName("UTF-8"))); } out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { int strLen = in.readInt(); if (strLen <= -1) { name = null; } else if (strLen == 0) { name = ""; } else { byte[] strBytes = new byte[strLen]; in.readFully(strBytes); name = new String(strBytes, Charset.forName("UTF-8")); } age = in.readInt(); } }
这里,我们发现除了添加了
Externalizable接口的两个公有方法之外,还有一处发生了变化,
Person添加了一个默认的构造函数。
当使用
Externalizable进行反序列化构造对象时,会调用该类的默认构造函数进行构造。如果没有默认构造函数,那么将会在反序列化的时候抛出
java.io.InvalidClassException。
由于
Externalizable使用的是接口的方式进行序列化,所以对于整个继承层次中的类,序列化时只会调用叶节点的类。如果想要在父类的基础上扩展子类的序列化,那么就需要在子类的方法中调用父类的方法。所以,相比使用
Serializable的私有方法自定义序列化,
Externalizable更加的可控。
首先必须实现java.io.Serializable,即使子类实现了此接口,父类仍需实现此接口,除非子类对父类状态进行了自定义的序列化处理。
1. 实现接口java.io.Externalizable
void writeExternal(ObjectOutput
out) throws IOException;
void readExternal(ObjectInput
in) throws IOException, ClassNotFoundException;
注意实现此接口,对象必须有public声明的空构造器。
2. 实现方法writeObject和readObject
private void writeObject(ObjectOutputStream os) throws IOException
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException
它们有个好处是可以调用以下接口先进行默认的Java序列化,再进行自定义的序列化。
ObjectOutputStream.defaultWriteObject()和ObjectInputStream.defaultReadObject()
实现这些方法接口,对象可不需要空构造器,且可以是private的。
注意如果实现了接口java.io.Externalizable,这两个方法且父类的这两方法都不会被调用。否则父类的这两方法先被调用,然后才是子类。
JDK源码参考: java.util.ArrayList
3. 实现方法writeReplace和readResolve
private Object writeReplace()
private Object readResolve()
通过它们可以返回不同的类型。
对于writeReplace,仍然需要对新类型进行序列化。
对于readResolve,其调用在readExternal或readObject之后。
JDK源码参考: java.util.EnumSet
4. 默认序列化自定义包括关键字transient和静态字段名serialPersistentFields
transient 用于指定哪个字段不被默认序列化,如public transient int a;
serialPersistentFields 用于指定哪些字段需要被默认序列化,如
Java代码
private static final ObjectStreamField[] serialPersistentFields =
{
new ObjectStreamField("name", String.class),
new ObjectStreamField("a", Integer.TYPE)
};
如果同时定义了serialPersistentFields与transient,transient会被忽略。
5. serialVersionUID
序列化的时候会读写serialVersionUID并做出校验,没做如下定义的话会根据类签名信息进行生成,包括类名、非私有构造器、非私有类方法、非私有静态类属性,但不包括继承层次,也就是继承层次发生变化也不会报类错误,但是父类的状态信息将得不到反序列化。
private static final long serialVersionUID = 2184568476863030694L;
6. 相关异常
java.io.NotSerializableException
java.lang.ClassNotFoundException
java.io.InvalidClassException (如serialVersionUID校验失败,原生字段类型不一致等)
java.lang.ClassCastException (如自定义属性类型或自身类型不一致)
java.io.StreamCorruptedException
java.io.EOFException (自定义序列化时试图读取更多的消息)
序列化机制对比
实现Serializable接口:
系统自动存储必要的信息;
Java内建支持,抑郁实现,只需要实现该接口即可,无需任何代码支持;
性能略差;
实现Externalizable接口:
程序员决定存储那些信息;
仅仅提供两个空方法,实现该接口必须为两个空方法提供实现;
性能略好;
对象序列化注意点
对象的类名、Field(包括基本类型、数组、对其它对象的引用)都会被序列化;方法、static Field(即静态Field)、transient Field(也被称为瞬态Field)都不会被序列化;
实现Serializable接口如果需要让某个Field不被序列化,则在该Field前加transient修饰符,而不是static关键字,虽然static关键字可以达到这个效果;
保证序列化对象的Field类型也是可序列化,否则需要使用transient关键字来修饰该Field,要不然该类是不可序列化的;
反序列化对象时必须有序列化对象的class文件;
当通过文件、网络来读取序列化的对象时,必须按实际写入顺序读取;
版本
反序列化Java对象时,必须提供该对象的class文件,随着项目的升级,系统的class文件也会升级。如果保持两个class文件兼容性:
Java的序列化机制允许为序列化类提供一个private static final的serialVersonUID值,该Field值用于标识该Java类的序列化版本;
最好在每个要序列化的类中加入private static final long serialVersionUID这个Field,具体数值自己定义。这样,即使对象被序列化之后,它所对应的类修改了,该对象也依然可以被正确反序列化;
如果不显示定义serialVersionUID值:
该Field值将由JVM根据类的相关信息计算,而修改后的类计算结果与修改前的类计算结果往往不同,从而造成对象的反序列化因为类版本的不兼容失败;
不利于程序在不同的JVM之间移植,因为不同的编译器计算该Field的值计算策略可能不同,从而造成虽然类完全没有改变,但是因为JVM不同,也会出现序列化版本不兼容而无法正确反序列化的现象;
相关文章推荐
- Java多种方式自定义序列化
- Java 多种文件读写方式
- java中多种方式读文件,追加文件内容,对文件的各种操作
- java多种方式操作文件
- java序列化方式性能比较
- java序列化方式性能比较
- Java 多种文件读写方式
- Java使用自定义的tableModel,设置可编辑方式
- Java序列化的机制和原理,以及自定义序列化问题
- C#对象的浅拷贝,深拷贝及利用序列化等多种方式实现深拷贝
- java序列化方式性能比较
- Java技巧:序列化的两种方式
- 生产者/消费者问题的多种Java实现方式
- JAVA连接数据库的方式有多种
- java 读取页面源码 的多种方式
- JAVA学习提高之----Java的多种方式读写Properties文件
- java中多种方式读文件,追加文件内容,对文件的各种操作
- 【Java】运用多种方式读取文件内容,包括按字节、字符和按行为单位读取文件内容
- java之多种时间格式化方式
- Java使用自定义的tableModel,设置可编辑方式