ObjectOutputStream和ObjectInputStream对对象进行序列化和反序列化

1 Java序列化和反序列化简介#

Java序列化是指把对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为java对象的过程。
我们把对象序列化成有序字节流,保存到本地磁盘或者Redis等媒介中,或者直接通过网络传输进行远程方法调用(RMI)来使用,在使用的时候再进行反序列化来得到该对象

2 通过Serializable实现序列化#

在Java中,只要一个类实现了了java.io.Serializable接口,那么这个类就可以被序列化。
查看Serializable接口,会发现该接口中并没有任何代码,这个实现Serializable接口仅仅作为一个标志。

2.1 ObjectOutputStream和ObjectInputStream对对象进行序列化和反序列化

通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化。调用ObjectOutputStream对象的writeObject输出可序列化对象,调用ObjectInputStream对象的readObject()得到序列化的对象。

Copy
public class Person implements Serializable { //序列化并不保存静态变量 static String a = "hello"; static int b = 200; int id; String name; //只定义有参构造器 public Person(int id, String name) { System.out.println("反序列化会调用构造器吗?"); this.id = id; this.name = name; } @Override public String toString() { return "Person{" + "id=" + id + ", name='" + name + '\'' + '}'; } }

在Person中,只定义了有参构造器。

Copy
public class SerializableTest { public static void main(String[] args) throws Exception { dome1(); dome2(); } private static void dome1() throws Exception{ File f = new File("test.txt"); if(!f.exists()) f.createNewFile(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(f)); objectOutputStream.writeObject(new Person(1, "Tom")); objectOutputStream.close(); } private static void dome2() throws Exception{ File f = new File("test.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(f)); Object object = objectInputStream.readObject(); System.out.println(object.toString()); objectInputStream.close(); } }

执行结果:

Copy
反序列化会调用构造器吗? Person{id=1, name='Tom'}

可以看到只会打印一次“反序列化会调用构造器吗?”,说明反序列化并不会使用构造器来生成对象,跟踪反序列化代码,最终执行到:

Copy
obj = desc.isInstantiable() ? desc.newInstance() : null;

desc.newInstance()方法原理是利用反射创建了一个对象,本质是调用非序列化父类的无参构造器
如果该类是Serializable类型的,则调用该类的第一个非Serializable类型的父类的无参构造方法。这里最终会调用Object类的无参构造器,在JDK1.8中,Object中没有构造器,系统会自动生成一个无参构造器。

我们添加一个Parent类,Parent没有实现Serializable,再让Person类继承它,执行dome1和dome2方法:

Copy
public class Parent { public Parent(){ System.out.println("父类构造器"); } }

执行结果:

Copy
父类构造器 反序列化会调用构造器吗? 父类构造器 Person{id=1, name='Tom'}

可以看到反序列化调用了没有实现Serializable的Parent类的构造器,我们再将Parent 实现Serializable,执行dome1和dome2方法:

Copy
父类构造器 反序列化会调用构造器吗? Person{id=1, name='Tom'}

发现并没有调用Parent类的构造器了,这里会继续向上找到Object类,并调用构造方法创建对象,并逐层向下去通过反射设置可以被反序列化的属性。
我们不去继承Parent类,重新执行dome1和dome2方法:

找到test.txt文本,打开可以看出静态变量并不会被序列化

2.2 serialVersionUID

这里我们在Person在增加两个属性如下,其中email使用**transient **修饰:

Copy
public class Person implements Serializable { //序列化并不保存静态变量 static String a = "hello"; static int b = 200; int id; String name; int age; transient String email; //只定义有参构造器 public Person(int id, String name, int age, String email) { this.id = id; this.name = name; this.age = age; this.email = email; } @Override public String toString() { return "Person[" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", email='" + email + '\'' + ']'; } }

然后只运行dome2(),测试能否可以从上次保存的文本内容中反序列化成功?

从异常信息中可以明显看出问题:本地保存和读取的流中 serialVersionUID不匹配。
serialVersionUID是记录class文件的版本信息的,这个数字是JVM通过一个类的类名、成员、包名、工程名算出的一个数字。
而这时候序列化文件中记录的serialVersionUID与项目中的不一致,即找不到对应的类来反序列化。从而抛出InvalidClassException,InvalidClassException是一个IOException。
解决方法:给需要序列化的类指定一个serialVersionUID,在序列化与反序列化的时候,JVM都不会再自己算这个class的serialVersionUID了
我们给Person类加上serialVersionUID,来重新序列化,然后再添加两个属性,再进行反序列可以得到执行结果:

Copy
Person[id=1, name='Tom', age=0, email='null']

可以看到反序列成功了,所以如果在类中指定了serialVersionUID,后期可以修改这个类,还是可以成功的反序列化,若我们保存数据在Redis中,后期添加了类属性,在没删除缓存的情况下,还是可以从Redis中获取对象。

我们改造dome1,然后运行dome1():

Copy
private static void dome1() throws Exception{ File f = new File("test.txt"); if(!f.exists()) f.createNewFile(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(f)); objectOutputStream.writeObject(new Person(1, "Tom", 25, "78451248788@qq.com")); objectOutputStream.close(); }

找到test.txt文本,可以看到age属性,但是email属性没有,这是因为transient修饰的原因,transient修饰的属性,是不会被序列化的
所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
这里提一点,从 email='null' 可以看出String类型是被当成引用类型的

再添加dome3和dome4方法,并运行:

Copy
private static void dome3() throws Exception{ File f = new File("test.txt"); if(!f.exists()) f.createNewFile(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(f)); Person person = new Person(1, "Tom", 25, "78451248788@qq.com"); //写入两次 objectOutputStream.writeObject(person); objectOutputStream.writeObject(person); objectOutputStream.close(); } private static void dome4() throws Exception{ File f = new File("test.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(f)); //读取两次 Object object1 = objectInputStream.readObject(); Object object2 = objectInputStream.readObject(); Person p1 = (Person) object1; Person p2 = (Person) object2; System.out.println(p1 == p2); objectInputStream.close(); }

执行结果:

Copy
true

可以看出Java序列化同一对象,并不会将此对象序列化多次得到多个对象。

2.3 自定义序列化,写入时替换对象writeReplace#

在序列化时,会先调用此方法,再调用writeObject方法。此方法可将任意对象代替目标序列化对象,在我们需要改变序列化对象时使用。
在person中添加writeReplace方法:

Copy
private Object writeReplace(){ List<Object> list = new ArrayList<>(); list.add(id); list.add(name); list.add(age); list.add(email); return list; }

这里将对象替换成一个List,在序列化的时候会调用writeReplace将对象换成List再存入文本中,反序列的时候获得的不再是person对象,而是List对象。

2.4 自定义反序列化,恢复对象时替换readResolve#

反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。readResolve在readeObject后调用。
我们注释上面的writeReplace方法,添加readResolve方法,执行方法:

Copy
private Object readResolve()throws ObjectStreamException { return "恢复对象时直接替换结果"; }

执行结果:

Copy
恢复对象时直接替换结果

可以看到结果已经被替换了。writeObject与readObject需成对实现,而writeReplace与readResolve则不需要成对出现,一般单独使用。

2.5 readResolve保护单例和枚举类型#

定义单例对象:

Copy
public class Singleton implements Serializable { private Singleton(){} private static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } }

添加dome5方法,并执行:

Copy
private static void dome5() throws Exception{ File f = new File("test.txt"); if(!f.exists()) f.createNewFile(); Singleton singleton = Singleton.getInstance(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(f)); objectOutputStream.writeObject(singleton); objectOutputStream.close(); //反序列化一次 ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(f)); Singleton s1 = (Singleton) objectInputStream.readObject(); objectInputStream.close(); //反序列化两次 ObjectInputStream objectInputStream2 = new ObjectInputStream(new FileInputStream(f)); Singleton s2 = (Singleton) objectInputStream2.readObject(); objectInputStream.close(); System.out.println(singleton == s1); System.out.println(s1 == s2); }

执行结果:

Copy
false false

在单例方法中添加readResolve方法,直接返回原单例对象,再执行dome5方法:

Copy
public class Singleton implements Serializable { private Singleton(){} private static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } private Object readResolve(){ return instance; } }

执行结果:

Copy
true true

可以看到反序列出来还是原来的单例对象,保护了单例。

posted @   homeSicker  阅读(705)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示
CONTENTS