java怎样实现对象的序列化与反序列化 java序列化操作的基础方法

Java中实现对象序列化与反序列化的核心是通过实现serializable接口将对象转换为字节流并恢复,其中被transient和Static修饰的字段以及父类未实现serializable时的非静态字段不会被序列化,因此在序列化过程中这些字段的状态不会被保存或恢复,从而确保敏感信息不被持久化、共享状态不被重复记录,并正确处理继承关系下的对象重建,最终保证序列化机制的安全性与一致性。

java怎样实现对象的序列化与反序列化 java序列化操作的基础方法

Java中实现对象的序列化与反序列化,核心在于将一个Java对象的状态转换为字节流,以便存储或在网络中传输,之后再将字节流恢复为对象。这听起来有点像魔法,但实际上是Java I/O体系里一个非常基础且强大的功能。简单来说,就是把活生生的对象“冻结”成数据,需要时再“解冻”复活。

解决方案

要让一个Java对象具备序列化能力,最直接的方法就是让它的类实现

java.io.Serializable

接口。这个接口是个标记接口,它本身没有任何方法需要实现,仅仅是告诉jvm:“嘿,我这个类的对象可以被序列化了!”。

序列化过程主要用到

java.io.ObjectOutputStream

,它能将对象写入一个输出流(比如文件流或网络流)。反序列化则使用

java.io.ObjectInputStream

,从输入流中读取字节并重构对象。

立即学习Java免费学习笔记(深入)”;

我们来看一个基础的例子:

import java.io.*;  // 1. 定义一个可序列化的类 class User implements Serializable {     private static final long serialVersionUID = 1L; // 推荐显式定义     private String name;     private int age;     private transient String password; // 标记为transient的字段不会被序列化      public User(String name, int age, String password) {         this.name = name;         this.age = age;         this.password = password;     }      @Override     public String toString() {         return "User{name='" + name + "', age=" + age + ", password='" + password + "'}";     }      // 省略getter/setter     public String getName() { return name; }     public int getAge() { return age; }     public String getPassword() { return password; } }  public class SerializationDemo {     public static void main(String[] args) {         User user = new User("张三", 30, "mySecretPass");         String filePath = "user.ser"; // 序列化文件路径          // 序列化         try (FileOutputStream fileOut = new FileOutputStream(filePath);              ObjectOutputStream out = new ObjectOutputStream(fileOut)) {             out.writeObject(user);             System.out.println("User对象已序列化到 " + filePath);             System.out.println("原始对象: " + user);         } catch (IOException i) {             i.printStackTrace();         }          // 反序列化         User deserializedUser = NULL;         try (FileInputStream fileIn = new FileInputStream(filePath);              ObjectInputStream in = new ObjectInputStream(fileIn)) {             deserializedUser = (User) in.readObject();             System.out.println("User对象已从 " + filePath + " 反序列化。");             System.out.println("反序列化对象: " + deserializedUser);             // 注意:transient字段的值为默认值             System.out.println("反序列化后密码: " + deserializedUser.getPassword()); // 会是null         } catch (IOException i) {             i.printStackTrace();         } catch (ClassNotFoundException c) {             System.out.println("User class not found");             c.printStackTrace();         }     } }

运行这段代码,你会发现

password

字段在反序列化后变成了

null

,这正是

transient

关键字的作用。

Java对象序列化时,哪些字段会被忽略?

这问题问得挺好,因为不是所有字段都会乖乖地被序列化。在Java的默认序列化机制中,有几种情况下的字段是会被“跳过”的:

首先,也是最常见的,就是被

transient

关键字修饰的字段。

transient

直译过来是“瞬态的”、“临时的”,用它来标记一个字段,就等于告诉JVM:“这个字段我不想让它参与序列化过程,它的值不应该被保存下来。” 为什么会有这种需求呢?可能是这个字段存储的是敏感信息(比如密码),不希望它以明文形式出现在序列化数据中;也可能是这个字段是运行时动态计算出来的,或者它引用了一个不能被序列化(或不应该被序列化)的对象,比如一个线程、一个文件句柄,或者一个数据库连接。序列化后,这些

transient

字段的值会被恢复成其类型的默认值(对象类型为

null

,基本类型为0、

false

等)。

其次,

static

(静态)字段也不会被序列化。这其实很好理解,静态字段属于类本身,而不是属于类的某个特定对象实例。序列化保存的是对象的状态,而静态字段是所有对象共享的,它不构成任何一个对象实例的独特状态。所以,无论你序列化多少个对象实例,静态字段的值都不会被保存或恢复。它在反序列化时,依然会保持其在当前JVM中的静态值。

最后,如果一个类的父类没有实现

Serializable

接口,那么该父类的非

transient

、非

static

字段也不会被序列化。当子类被序列化时,只有子类自己的可序列化字段会被保存。反序列化时,父类的字段会调用父类的无参构造器来初始化。这也是为什么如果你的父类没有无参构造器,而子类又实现了

Serializable

,反序列化时可能会抛出

InvalidClassException

,因为它找不到合适的构造器来初始化父类部分。

理解这些“例外”非常重要,它能帮助我们更好地设计可序列化的类,避免数据泄露或不必要的复杂性。

为什么需要给可序列化类定义serialVersionUID?

噢,

serialVersionUID

,这玩意儿初看起来有点神秘,但它在Java序列化机制中扮演着一个至关重要的角色,尤其是在你对类结构进行修改之后。简单来说,它就像是类的一个“版本号”或者“指纹”,用来在序列化和反序列化时进行兼容性检查。

当你序列化一个对象时,JVM会把这个对象的

serialVersionUID

也写入到序列化流中。当你在另一个JVM或者同一个JVM的不同时间点尝试反序列化这个对象时,JVM会读取流中的

serialVersionUID

,然后拿它与当前类定义中的

serialVersionUID

进行比较。

如果这两个ID不匹配,JVM就会认为“哎呀,这个对象当初序列化的时候,它的类定义和现在的不一样了!”然后,它会毫不留情地抛出

java.io.InvalidClassException

异常。这意味着你无法成功反序列化那个对象。

那么,什么时候这个ID会不匹配呢?当你对一个已经实现了

Serializable

接口的类进行修改时,比如添加、删除、修改字段,或者改变了字段的类型,甚至改变了类名、接口实现等,如果没有显式定义

serialVersionUID

,JVM会根据类的结构自动生成一个。一旦类结构发生变化,自动生成的

serialVersionUID

就会不同。这就是问题所在:你修改了类,即使是很小的改动,之前的序列化数据可能就无法被新的类版本反序列化了。

所以,显式定义

private static final long serialVersionUID = 1L;

(或者任何你觉得合适的版本号)就变得非常重要。一旦你手动定义了它,即使你后续对类进行了添加字段、删除字段等兼容性修改,只要你不改变这个

serialVersionUID

的值,JVM在反序列化时就不会因为版本号不匹配而报错。当然,这只是解决了版本号匹配的问题,数据兼容性(比如新加的字段在老数据中是默认值,老数据中的字段在新类中被删除了怎么办)还需要你自己去处理,比如通过

readObject()

writeObject()

方法进行自定义序列化逻辑。

我个人习惯是,只要一个类实现了

Serializable

,我就会立即给它加上

serialVersionUID

,通常从

1L

开始。这就像是给你的代码买了一份保险,在未来类结构变化时,能避免很多不必要的兼容性问题。

Java序列化在实际开发中有哪些应用场景?

Java序列化在日常开发中其实无处不在,尽管很多时候我们可能没有直接去调用

ObjectOutputStream

ObjectInputStream

,但它常常作为底层机制被各种框架和库所利用。

最直观的一个应用场景就是对象的持久化。如果你想把一个Java对象的状态保存到磁盘上,以便程序关闭后下次启动还能恢复,序列化就是最直接的方式。比如,一个简单的配置对象,或者一个游戏中的存档,都可以通过序列化直接写入文件。虽然现在更流行用jsonxml或数据库来存储数据,但对于一些简单、内部使用且Java特有的对象,序列化依然很方便。

接着,网络传输是序列化的一个大舞台。在分布式系统中,服务之间经常需要传递对象。Java的RMI(远程方法调用)就是基于序列化来实现的,当客户端调用远程服务器上的一个方法时,参数对象会被序列化后传输到服务器,结果对象也会被序列化后传回客户端。除了RMI,很多消息队列(如kafkarabbitmq)在传输Java对象时,也可能在底层使用序列化机制,或者允许你自定义序列化器来处理Java对象。

缓存也是序列化经常出现的地方。当我们需要将对象存储到内存以外的缓存系统(如redis、memcached)中时,对象必须被转换成字节流。虽然这些缓存系统通常支持多种序列化格式(如JSON、Protobuf),但Java原生的序列化也是一种选择,尤其是在纯Java环境下的内部缓存。

再比如,跨进程通信。在同一个机器上,不同的Java进程之间如果需要传递复杂的Java对象,序列化也是一种有效的方式,比如通过管道或者共享内存。

甚至在某些情况下,深拷贝一个对象,你也可以通过先将对象序列化到内存字节数组,再反序列化回来实现。这虽然不是序列化的主要目的,但确实是一种实现深拷贝的“旁门左道”,因为它会创建一个全新的对象图,而不是简单地复制引用。

当然,现在随着微服务和异构系统变得越来越普遍,JSON、Protocol Buffers等跨语言的序列化协议变得更加流行,因为它们解决了Java序列化固有的跨语言兼容性问题。但即便如此,在纯Java的生态系统内部,或者特定场景下,Java原生的序列化依然有着不可替代的地位。它简单、直接,对于那些不需要跨语言交互的内部数据结构来说,是相当高效的。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享