对象序列化和Serializable接口
对象序列化
对象序列化是指把对象保存为二进制字节码文件,反序列化是指把二进制字节码文件转换成Java对象。
比如有一个类Person,实现了序列化接口Serializable
@Setter @Getter @AllArgsConstructor public class Person implements Serializable { private String name; private Integer age; }
就可以通过ObjectInputStream 和 ObjectOutputStream 对该对象进行读写。
//序列化 FileOutputStream fileOutputStream = new FileOutputStream("person"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(new Person("厂长", 27)); objectOutputStream.close(); //反序列化 FileInputStream fileInputStream = new FileInputStream("person"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); Person person = (Person) objectInputStream.readObject();
objectInputStream.close();
System.out.println(person); System.out.println(person.getName()+"——"+person.getAge());
objectOutputStream输出了该Person类对象的二进制文件,objectInputStream则通过读取二进制文件获取到了该对象的信息。
可以看到,序列化是实现对象持久化的一种方式,这种方式可以将对象信息通过二进制文件形式保存到磁盘或者传输网络。
commons-lang包提供了序列化工具类SerializationUtils,通过serialize()和deserialize()方法可以实现序列化和反序列化。
public static void serialize(Serializable obj, OutputStream outputStream)
可以看到传入的对象需要是Serializable 接口的实现类。
Serializable接口
Serializable接口是一种标记接口。标记接口指的是,没有任何方法和属性的接口。它仅表明它的实现类属于一个特定的类型,使其可以用instanceof进行类型查询。
if(person instanceof Serializable)
其他常用的标记接口还有Cloneable接口等。
在进行序列化时,会判断要被序列化的类是否是String、Enum、Array或Serializable类型,否则会抛出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) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
实现Serializable接口的类,表明该类是可序列化的。如果该类的某些字段不需要被序列化,可以使用transient修饰。
transient int age
关于序列化类的继承问题:
如果父类实现了Serializable,由于子类会继承父类实现的接口,所有子类不需要再实现Serializable。
如果父类没有实现Serializable,而子类实现了,那么只有子类中的成员会被序列化,父类成员会丢失掉。
注意:由于static成员不属于对象实例,所以也不会被序列化,序列化是针对对象的。
另一个经常出现的使用场景就是,使用redisTemplate将对象存储为hash类型,需要将要存储的对象实现可序列化。
redis的hash类型都是以string类型存储的,通过序列化的方式就可以将对象以二进制字节码的形式作为string类型存储。
<HK> byte[] rawHashKey(HK hashKey) { Assert.notNull(hashKey, "non null hash key required"); return this.hashKeySerializer() == null && hashKey instanceof byte[] ? (byte[])((byte[])hashKey) : this.hashKeySerializer().serialize(hashKey); }
可以看到,如果传入的hashKey可以通过this.hashKeySerializer().serialize()方法转换成byte[]类型的。
保证序列化前后数据的正确性
1.反序列化对象和序列化前的对象的全类名(包名+类名)必须一致,否则会抛出ClassNotFoundException。
2.反序列化后对象的serialVersionUID需要和和序列化之前对象的serialVersionUID一致,否则会抛出InvalidClassException。
serialVersionUID
被序列化的类需要加入版本号serialVersionUID,用于在反序列化过程中验证序列化对象是否和接收该对象的类的版本号一致。
一般会显式设为private static final long serialVersionUID = 1L,或者 系统自动生成一个64位的哈希字段,其根据类名、类及其属性修饰符、接口及接口顺序、属性、静态初始化、构造器生成,任何一项的改变都会导致系统生成的serialVersionUID变化。
如果没有显式地定义serialVersionUID,Java序列化机制会根据编译的类(就是上面涉及到的类相关的属性),唯一的自动生成一个serialVersionUID作序列化版本比较用,
如果类结构没有发生任何变化,serialVersionUID也不会发生变化;
但如果类结构改变了,比如属性名改变了,属性的修饰符改变了等,都会导致serialVersionUID不匹配,从而无法实现反序列化。
( 经测试,注释是不会影响类结构的,这似乎和编译器有关,网上的一些结论似乎是空格注释都会影响)
此外,不同java编译器也可能会生成不同的serialVersionUID 值,产生不应该出现的InvalidClassException。
所以,如果希望不通过编译来强制划分类的版本,而且想要实现序列化接口的类能够兼容先前版本,就需要显示的声明serialVersionUID,只要序列化的类和反序列化传入的字节流能够保证serialVersionUID一致,就能够实现反序列化。
注:所谓兼容,举例来说,如果反序列化传入的类比原来实现序列化的类多一个字段,那么这个字段会被忽视;如果反序列化传入的类比原来实现序列化的类少一个字段,这个字段会被赋值为对应类型的默认值。
*自定义序列化
如果需要,可以使用自定义的读写方法,这个场景比较少见,网上说如果实现序列化的类是单例模式的话,应该通过自定义序列化的方式,指定同一个单例。
可以通过两种方式自定义序列化策略。
一种是在实现了Serializable接口的类中,定义两个方法
private void writeObject(ObjectOutputStream stream) throws IOException private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException
ObjectInput和OutputStream在序列化/反序列化时候通过反射检查该类是否存在writeObject() 和 readObject()方法,
如果存在则会调用对象类里的writeObject 和 readObject 方法进行序列化操作。
*具体实现是通过将对象传入ObjectStreamClass的lookupStreamClass()方法中
ObjectStreamClass clDesc = ObjectStreamClass.lookupStreamClass(objClass);
在其中的createClassDesc(cl) 方法中,通过反射检查bean中是否有重写
result.methodWriteObject = findPrivateMethod(cl, "writeObject", WRITE_PARAM_TYPES);
result.methodReadObject = findPrivateMethod(cl, "readObject", READ_PARAM_TYPES);
另一种方式是实现了Externalnalizable接口
writeExternal(ObjectOutput out)) 和 readExternal(ObjectInput in)
这个接口也用的比较少,暂时不细写了。