文章目录
概述
如果没有实现Serializable接口而进行序列化操作就会抛出NotSerializableException异常。
能够序列化的字段:属性变量、父类的属性变量(父类也需要实现Serializablie接口)
不能序列化的字段:静态变量、父类的属性变量、关键字transient修饰的变量、没有实现Serializable接口的对象属性
对字符串进行序列化
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Test {
public static void main(String args[]) throws Exception {
String obj = "ls "; // 原始字符串,供写入文件用
// 将序列化对象写入文件object.txt中
FileOutputStream fos = new FileOutputStream("aa.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(obj);
os.close();
// 从文件object.txt中读取数据
FileInputStream fis = new FileInputStream("aa.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
// 通过反序列化恢复对象obj
String obj2 = (String) ois.readObject();
System.out.println(obj2); // 输出ls ,证明读取的就是当初写入的字符串对象
ois.close();
}
}
对java对象进行序列化
对java对象进行序列化需要实现Serializable接口
Person.java
class Person implements Serializable {
// 序列化ID
private static final long serialVersionUID = -5809782578272943999L;
private int age; // 省略get,set方法
private String name; // 省略get,set方法
private String sex; // 省略get,set方法
}
Test2.java
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.text.MessageFormat;
public class Test2 {
public static void main(String[] args) throws Exception {
SerializePerson();// 序列化Person对象
Person p = DeserializePerson();// 反序列Perons对象
System.out.println(MessageFormat.format("name={0},age={1},sex={2}", p.getName(),
p.getAge(), p.getSex())); //通过输出,验证写入的对象,被重新构造,属性也是相同的
}
/**
* 序列化Person对象
*/
private static void SerializePerson() throws FileNotFoundException, IOException {
Person person = new Person();
person.setName("ssooking");
person.setAge(20);
person.setSex("男");
// ObjectOutputStream 对象输出流,将Person对象存储到Person.txt文件中,完成对Person对象的序列化操作
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("Person.txt")));
oo.writeObject(person);
System.out.println("Person对象序列化成功!");
oo.close();
}
/**
* 反序列Perons对象
*/
private static Person DeserializePerson() throws Exception, IOException {
FileInputStream fis = new FileInputStream("Person.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Person person = (Person) ois.readObject();
ois.close();
System.out.println("Person对象反序列化成功!");
return person;
}
}
复写readObject和writeObject
仅修改Person.java ,复用Test2
class Person implements Serializable {
// 序列化ID
private static final long serialVersionUID = -5809782578272943999L;
...
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException { // 自定义反序列化实现
System.out.println("readObject execute");
is.defaultReadObject();
}
private void writeObject(ObjectOutputStream is) throws IOException, ClassNotFoundException { // 自定义反序列化实现
System.out.println("writebject execute");
is.defaultWriteObject();
}
注意,复写readObject和writeObject时,前缀是private,不能是其他任何类型,否则不生效。
复写readObject
和writeObject
作用是定制序列化的过程,这个例子中,我们使用了defaultWriteObject
和defaultWriteObject
,作用是默认的读写逻辑,按照某种顺序(未细究,可能是字段名称的字符串顺序)逐一把所有的字段都写入或读出,我们来验证下作用:
继续改造Person.java
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException { // 自定义反序列化实现
System.out.println("not get name value yet :" + this.name);
is.defaultReadObject();
System.out.println("already get name value :" + this.name);
}
执行结果:
Person对象序列化成功!
not get name value yet :null //此时尚未赋值
already get name value :ssooking //执行defaultReadObject后,获取值
Person对象反序列化成功!
name=ssooking,age=20,sex=男
总结:defaultReadObject的作用大致等价于下面的代码
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException { // 自定义反序列化实现
this.age = is.readInt();
this.name = (String) is.readObject();
this.sex = (String) is.readObject();
}
private void writeObject(ObjectOutputStream os) throws IOException, ClassNotFoundException { // 自定义反序列化实现
os.writeInt(age);
os.writeObject(name);
os.writeObject(sex);
}
执行结果:
Person对象序列化成功!
Person对象反序列化成功!
name=ssooking,age=20,sex=男
注意:自定义写入或读取过程时,需要注意写入顺序和读写顺序要对应,比如先写入writeInt,读取时就要先readInt,否则会错位或类型解析错误。
模拟一个错位的例子,name和sex颠倒了:
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException { // 自定义反序列化实现
this.age = is.readInt();
this.sex = (String) is.readObject(); //先设置sex
this.name = (String) is.readObject();
}
private void writeObject(ObjectOutputStream os) throws IOException, ClassNotFoundException { // 自定义反序列化实现
os.writeInt(age);
os.writeObject(name); //先写name
os.writeObject(sex); //在写sex
}
执行结果:
Person对象序列化成功!
Person对象反序列化成功!
name=男,age=20,sex=ssooking //姓名变为“男”
为何复写readObject和writeObject
Serializable
接口是一个空接口,没有定义任何的方法和属性,所以Serialiazable
接口的作用就是起到一个标识的作用,源码如下:
public interface Serializable {
}
Serializable
接口既然仅是标识的作用,readObject
和writeObject
也不是Object
类的方法,那么究竟原理是什么?
答案就在ObjectOutputStream
和 ObjectInputStream
,在执行序列化操作的时候,利用反射,调用了对象的readObject
和writeObject
。
JDK1.8 ObjectOutputStream源码解析:
我们知道序列化时调用 os.writeObject(obj)
,那么我们就从writeObject
入手,内部调用了writeObject0
:
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
....
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());
}
}
前面都是在做各种检查,实际有效的代码就是从上面代码判断语句开始,分别针对不同类型的对象分别执行不同的序列化方法。而对于对象类型是比较复杂的,也就是writeOrdinaryObject
方法,逻辑如下:
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
...
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj); //写入实现了Externalizable接口的对象
} else {
writeSerialData(obj, desc); //写入实现了Serializable
}
最终按实现了Externalizable
接口或Serializable
接口分别执行writeExternalData
和writeSerialData
方法,writeSerialData
方法如下:
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
//判断该类是否自定义类writeObject方法,如果重写了该方法则按重写的逻辑处理
if (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
//通过反射的方式执行自定义的writeObejct方法
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
//如果没有自定义writeObject方法则按默认的方法写入属性数据
defaultWriteFields(obj, slotDesc);
}
}
}
看到这里,基本上知道了答案: writeSerialData方法利用反射,对实现了Serializable 接口并重写了readObject方法的对象特殊处理。
注意:
1·、writeObject和readObject方法中的序列化和反序列化字段必须完全一致,也就是序列化字段的数量和顺序(writeObejct可以多write几个字段,但是顺序必须在后面,readObject不可多字段,否则解析会抛异常)
2、序列化和反序列化 writeObject和readObject方法必须同时重写,如果只重写一个,那么会抛异常,因为序列化之后的是字节序列,是严格按字节序列的顺序来解析的。
其实也不是必须同时重写,比如只想序列化,但不反序列化,只重写writeObject不会报错;当仅重写了writeObject,但是writeObject内部代码是defaultWriteObject或者没有调用defaultWriteObject,但实际顺序和defaultWriteObject一致,也不会报错。简单来说,就是保证顺序一致,就不会错。
3、writeObejct和readObject都必须是private修饰的,public修饰的不起作用
问题总结
1.序列化之前和反序列化之后的对象可能是同一个对象吗?
有可能,反序列化是通过对象的构造方法进行初始化的,所以正常情况下不会是之前的同一个对象,而且序列化之前的对象可能都已经被垃圾回收的;
但是在单例模式下是可以通过自定义反序列化逻辑进行操作的,详情如下第二条。
2.序列化和反序列化破坏单例模式的解决?
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SingletonUser implements Serializable {
private static final long serialVersionUID = 1L;
private static SingletonUser user = new SingletonUser(); //单例模式
public static SingletonUser getInstance() { //获取唯一实例的public方法
return user;
}
private SingletonUser() { //私有构造方法
}
// private Object readResolve() { //先注释掉,作用是保证单例
// return user;
// }
public static void main(String[] args) {
// 1.通过单例类的静态方法获取单例
SingletonUser user = SingletonUser.getInstance();
try {
// 2.序列化单例对象
serializer(user);
// 3.反序列化获取单例对象
SingletonUser user2 = derializer();
System.out.println(user);
System.out.println(user2);
} catch (Exception e) {
e.printStackTrace();
}
}
/** 序列化对象 */
private static void serializer(SingletonUser user) throws Exception {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File(
"D:\\test\\user.txt")));
outputStream.writeObject(user);
outputStream.close();
}
/** 反序列化对象 */
private static SingletonUser derializer() throws Exception {
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File(
"D:\\test\\user.txt")));
SingletonUser user = (SingletonUser) inputStream.readObject();
inputStream.close();
return user;
}
}
执行结果:
mytest.serio.SingletonUser@10dea4e
mytest.serio.SingletonUser@ed1f14
不出所料,通过序列化之后再反序列化之后的对象不是同一个对象,但是这就破坏了单例模式,因为单例模式就是应该只能创建一个对象的,但是通过序列化操作之后对象就会可以被创建多个。所以此时就需要通过自定义序列化的方式来解决破坏单例模式。
解决办法如下:在单例类中定义方法readResolve
方法,并且返回Object
对象。
private Object readResolve(){ //此时反序列化后,只有一个实例
return user;
}
但是如果返回返回的不是Object对象,而是SingletonUser对象,则会无效,如下:
private SingletonUser readResolve(){ //无效,仍然是2个实例
return user;
}
3.实现了Serializable接口的serialVersionUID作用是什么?
在反序列化时,JVM需要知道所属的class文件,在序列化的时候JVM会记录class文件的版本号,默认是JVM自动生成,也可以手动定义。反序列化时JVM会按版本号找指定版本的class文件进行反序列化,
如果class文件有版本号在序列化和反序列化时不一致就会导致反序列化失败,会抛异常提示版本号不一致,如:
Exception in thread "main" java.io.InvalidClassException: com.lucky.demo.base.seralizer.demo.User; local class incompatible: stream classdesc serialVersionUID = 512, local class serialVersionUID = 2276725928587165175
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)