序列化,Java的实现方式
source link: https://segmentfault.com/a/1190000040957611
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
当程序创建的对象,在程序终止后仍要存在,并在程序下次运行时重建对象,且拥有与程序上次运行时所拥有的信息相同。序列化可以将对象写入字节流,反序列化就是将字节流恢复为对象,如下图所示。
Java 中提供了一种通用序列化机制,实现将对象写出到输出流中,并在之后将其读回。
对象序列化
定义一个 Student
类,如下所示。
public class Student implements Serializable { private static final long serialVersionUID = -4496225960550340595L; private String name; private Integer age; private Double score; ...getter与setter... @Override public String toString() { return new StringJoiner(", ", Student.class.getSimpleName() + "[", "]") .add("name='" + name + "'") .add("age=" + age) .add("score=" + score) .toString(); } }
在程序中使用 Student
类可以创建实例来表示某一学生。
Student s = new Student(); s.setName("小赵"); s.setAge(24); s.setScore(98.5);
但是,在程序结束后,该实例会被销毁,如果想在下次运行时拥有与上次运行时相同的信息,使用 ObjectOutputStream
实例将 Student
实例序列化保存到文件中。
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxx")); out.writeObject(s); out.close();
对象序列化才使用
writeObject/readObject
方法,基本类型需要使用writeInt/readInt
或writeDouble/readDouble
这样的方法,对象流类都实现了DataInput/DataOutput
接口。
保存到文件中后,想要使用的话就可以通过创建 ObjectInputStream
实例并调用 readObject()
方法来获取。
ObjectInputStream in = new ObjectInputStream(new FileInputStream("xxx")); Student saved = (Student) in.readObject(); in.close();
Serializable
Java 想要使一个类能被序列化,就需要像 Student
一样,实现 Serializable
接口。
public interface Serializable { }
该接口没有任何方法需要实现,只是作为类能被序列化的标记。但是没有该接口的话,序列化会报 java.io.NotSerializableException
异常。源码中序列化如下所示。
if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { // 判断是否实现了 Serializable 接口 writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } }
当然,实现 Serializable
接口的可序列的类,在序列化时,会默认将类中所有信息序列化,如果想要控制序列化对象,可以使用 transient
关键字标识需要关闭序列化的字段,如下所示。
public class Student implements Serializable { private static final long serialVersionUID = -4496225960550340595L; private String name; private transient Integer age; private Double score; ··· } // 序列化前:Student[name='小赵', age=24, score=98.5] // 反序列化后:Student[name='小赵', age=null, score=98.5]
这样使用 transient
进行标记,就可以在对象序列化时跳过。
使用 Serializable
的默认序列化会降低改变类实现的灵活性,增加 Bug
和安全漏洞的可抗性,测试也会增加负担等,因此想使用 Serializable
实现序列化时要考虑清除。
Externalizable
除了 Serializable
接口外,Java 还提供了继承 Serializable
接口的 Externalizable
接口,并且还要实现两个方法。
public class Student implements Externalizable { ... @Override public void writeExternal(ObjectOutput out) throws IOException { } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { } }
Java 提供的 Externalizable
接口可以控制对象序列化时,不想被其序列化的信息,和反序列时,不被反序列化的信息。
@Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); out.writeObject(score); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // 反序列顺序要与序列化时的顺序一致,否则会报 java.lang.ClassCastException 异常 name = (String) in.readObject(); score = (Double) in.readObject(); }
这样就不会将 Student
中的 age
序列化了,对序列化过程进行控制。
readObject
和writeObject
方法是私有的,并且只能被序列化机制调用。与此不同的是,readExternal
和writeExternal
方法是公共的。特别是,readExternal
还潜在地允许修改现有对象的状态。
serialVersionUID
在实现 Serializable
接口的可序列化的类里,都需要显示的增加下面这行代码。
private static final long serialVersionUID = -4496225960550340595L;
该静态数据域用于显示声明序列版本 UID
,并在序列化机制中,通过判断 serialVersionUID
来验证版本一致性。因此,反序列时,只要 serialVersionUID
与本地相应实体类的 serialVersionUID
一致,就可以反序列化,实现兼容,否则会报 java.io.InvalidClassException
异常。因此,只要 serialVersionUID
不变,序列化就可以读入这个类的对象的不同版本。
如果被序列化的对象具有在当前版本中所有没有的数据域,反序列化时会忽略额外的数据;如果当前版本具有在被序列化的对象所没有的数据域,那么新添加的域将被设置成它们的默认值。
显式的声明 serialVersionUID
也会带来小小的性能好处。如果没有提供显式的序列版本 UID
,编译器会选择一个摘要算法,并在运行时通过高开销的计算过程来产生一个序列版本 UID
,只要这个类有改动,得到的 UID
也就会变化,到时对象输入流将会拒绝反序列具有不同序列版本 UID
。因此,在可序列化的类中声明显式的 serialVersionUID
。
当你确定默认的序列化形式就满足了当前环境,还必须提供一个 readObject
方法添加验证或其他行为以保证约束关系和安全性。
private void readObject(ObjectInputStream in); private void writeObject(ObjectOutputStream out);
之后,数据域就再也不会被自动序列化,取而代之的是调用这些方法。
如上所示,当反序列化时,如果学生分数不在 0~100
之间,就是错误的数值,可以保护性地编写 readObject()
方法,维护其约束条件。
public class Student implements Serializable { ... private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // 调用默认反序列化方法 if (score < 0 || score > 100) throw new IllegalArgumentException("The score is between 0 and 100."); // 判断如果学生成绩有问题,抛出异常,终止操作。 } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); // 调用默认序列化方法 } }
枚举序列化
当目标对象唯一时,可使用枚举实现序列化,如下所示。
public enum Week { MONDAY, // 星期一 TUESDAY, // 星期二 WEDNESDAY, // 星期三 THURSDAY, // 星期四 FIRDAY, // 星期五 SATURDAY, // 星期六 SUNDAY; // 星期日 }
为了保证枚举类型及其定义的枚举变量在 VM
中是唯一的,Java规定了枚举常量的序列化是通过ObjectOutputStream
将枚举的 name()
方法返回的值做序列化;反序列化时,通过 ObjectInputStream
从流中读取常量名称,然后调用 java.lang.Enum.valueOf()
方法获得反序列化常量,并将常量的枚举类型和收到的常量名称作为参数传递。
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); throw new IllegalArgumentException( "No enum constant " + enumType.getCanonicalName() + "." + name); }
编译器在序列化和反序列化时,会忽略枚举类型定义的任何类特定的 writeObject
、readObject
、ReadObjectNodeData
、writeReplace
和readResolve
方法。类似地,也会忽略任何 serialPersistentFields
或 serialVersionUID
字段声明,所有枚举类型都具有规定的 serialVersionUID 0L
。记录枚举类型的可序列化字段和数据是不必要的,因为发送的数据类型没有变化。
在序列化和反序列化时,如果目标对象是唯一的,那么你必须加倍当心,这通常会在实现单例和类型安全的枚举时发生。
但在枚举之前,是通过使用 static final
来表示枚举类型,如下所示。
public class Week { public static final Week MONDAY = new Week(1); public static final Week TUESDAY = new Week(2); public static final Week WEDNESDAY = new Week(3); public static final Week THURSDAY = new Week(4); public static final Week FIRDAY = new Week(5); public static final Week SATURDAY = new Week(6); public static final Week SUNDAY = new Week(7); private int value; private Week(int v) { this.value = v; } }
但是,为这个类实现 Serializable
接口变成可序列化的类后,默认序列化机制就不再适用,任何 readObject
方法都会返回一个新键的实例。
public class Week implements Serializable { public static final Week MONDAY = new Week(1); ··· } Week w = FIRDAY; ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxx/ioweek.txt")); out.writeObject(w); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("xxx/ioweek.txt")); Week s = (Week) in.readObject(); in.close();
反序列化获取的 Week
对象与序列化的 Week
对象比较 w == s
为 false
,新键的实例与该类初始化时创建的实例不同,说明即使构造器私有,序列化机制也可以创建新实例。
readResolve
特性允许用 readObject
创建的实例代替另一个实例。因此,在 Week
类中定义 readResolve
方法,如下所示。
private Object readResolve() throws ObjectStreamException { if (value == 1) return Week.MONDAY; if (value == 2) return Week.TUESDAY; if (value == 3) return Week.WEDNESDAY; if (value == 4) return Week.THURSDAY; if (value == 5) return Week.FIRDAY; if (value == 6) return Week.SATURDAY; if (value == 7) return Week.SUNDAY; throw new ObjectStreamException(); }
定义 readResolve
方法后,反序列化时新键的对象会通过调用 readResolve
方法返回的值称为 readObject
的返回值,因此 w == s
返回的就是 true
。
序列化实现克隆
可序列化的类可以使用序列化机制实现对象克隆。如下所示,为可序列化的 Student
类提供克隆方法。
public class Student implements Cloneable, Serializable { ... @Override public Object clone() throws CloneNotSupportedException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); try { // save the object to a byte array try ( ObjectOutputStream out = new ObjectOutputStream(bout) ) { out.writeObject(this); } // read a clone of the object from the byte array try ( ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray())) ) { return in.readObject(); } } catch (IOException | ClassNotFoundException e) { CloneNotSupportedException ex = new CloneNotSupportedException(); ex.initCause(e); throw ex; } } }
如上所述,类想实现 clone
方法,需要实现 Cloneable
接口,之后调用该 clone
方法即可。使用 ByteArrayOutputStream
将数据保存到字节数组中,而不必将对象写出到文件中。
Student s = new Student(); s.setName("小赵"); s.setAge(24); s.setScore(98.5); Student sc = (Student) s.clone();
但这种方式也会比复制或克隆数据域的克隆方法要慢得多。当然,Java 序列化也是有风险的,最好是避免在程序中使用。
欢迎关注公众号「海人为记」,期待与你共同进步!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK