程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • JavaSE - 基础篇

    • Java环境搭建
    • Java基础语法
    • Java数据类型
    • Java常量和变量
    • Java进制和存储
    • Java运算符
    • Java流程控制
    • Java数组
    • Java面向对象上
    • Java面向对象下
    • Java异常机制
    • Java枚举
    • Java反射机制
    • Java代理模式
    • Java泛型
    • Java序列化
      • 1. 概述
      • 2. 定义
      • 3. 作用
      • 4. 如何实现
        • 4.1 实现Serializable接口
        • 简单实现
        • 版本 serialVersionUID
        • 继承及引用对象序列化
        • Java序列化算法
        • 自定义序列化
        • 4.2 实现Externalizable接口
    • Java多线程详解
    • Java线程池相关
  • Java
  • JavaSE - 基础篇
scholar
2024-02-03
目录

Java序列化

# 1. 概述

什么是序列化?什么是反序列化?为什么需要序列化?如何序列化?应该注意什么?本文将从这几方面来论述。

# 2. 定义

什么是序列化?什么是反序列化?

序列化: 把Java对象转换为字节序列的过程。 反序列化:把字节序列恢复为Java对象的过程。

# 3. 作用

为什么需要序列化?

  • 我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
  • 当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。

综上,可以得出对象的序列化和反序列化主要有两种用途:

  • 把对象的字节序列永久地保存到磁盘上。(持久化对象)
  • 可以将Java对象以字节序列的方式在网络中传输。(网络传输对象)

# 4. 如何实现

如何序列化和反序列化?

如果要让某个对象支持序列化机制,则其类必须实现下面这两个接口中任一个。

  • Serializable

    public interface Serializable {
    }
    
    1
    2
  • Externalizable

    public interface Externalizable extends java.io.Serializable {
       void writeExternal(ObjectOutput out) throws IOException;
       void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
     }
    
    1
    2
    3
    4

# 4.1 实现Serializable接口

# 简单实现

如果是对序列化的需求非常简单,没有对序列化过程控制的需求,可以简单实现Serializable接口即可。 从Serializable的源码可知,其是一个标记接口,无需实现任何方法。例如我们有如下的Student类

public class Student implements Serializable {  
    private String name;
    private int age;

    public Student(String name,int age)
    {
        System.out.println("有参数构造器执行");
        this.name=name;
        this.age=age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

序列化: 那么我们如何将此类的对象序列化后保存到磁盘上呢?

  1. 创建一个 ObjectOutputStream 输出流oos
  2. 调用此输出流oos的writeObject()方法
private static void serializ()
{
    try (ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("object.txt"));)
    {
       Student s=new Student("ben",18);
       oos.writeObject(s);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
1
2
3
4
5
6
7
8
9
10

上面代码将Sutent 的一个实例对象序列化到了一个文本文件中。

反序列化:我们如从文本文件中将此对象的字节序列恢复成Student对象呢?

  1. 创建一个ObjectInputStream 输入流ois

  2. 调用此输入流ois的readObject()方法。

     private static void deSerializ()
     {
         try(ObjectInputStream ois=new ObjectInputStream(new FileInputStream("object.txt"));)
         {
             Student s= (Student) ois.readObject();
             System.out.println(s.toString());
         }catch (Exception e)
         {
             e.printStackTrace();
         }
     }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    Node: 当反序列化的时候并没有调用Student的构造函数,说明反序列化机制无需通过构造器来构建Java对象,这就给实现了序列化机制的单例模式造成了麻烦。

# 版本 serialVersionUID

由于反序列化Java对象的时候,必须提供该对象的class文件,但是随着项目的升级class文件文件也会升级,Java如何保证兼容性呢?答案就是 serialVersionUID。每个可以序列化的类里面都会存在一个serialVersionUID,只要这个值前后一致,即使类升级了,系统仍然会认为他们是同一版本。如果我们不显式指定一个,系统就会使用默认值。

public class Student implements Serializable {
    private static final long serialVersionUID=1L;
    ...
}
1
2
3
4

我们应该总是显式指定一个版本号,这样做的话我们不仅可以增强对序列化版本的控制,而且也提高了代码的可移植性。因为不同的JVM有可能使用不同的策略来计算这个版本号,那样的话同一个类在不同的JVM下也会认为是不同的版本。

那么我们如何维护这个版本号呢?

  • 只修改了类的方法,无需改变serialVersionUID;
  • 只修改了类的static变量和使用transient 修饰的实例变量,无需改变serialVersionUID;
  • 如果修改了实例变量的类型,例如一个变量原来是int改成了String,则反序列化会失败,需要修改serialVersionUID;如果删除了类的一些实例变量,可以兼容无需修改;如果给类增加了一些实例变量,可以兼容无需修改,只是反序列化后这些多出来的变量的值都是默认值。

# 继承及引用对象序列化

当要序列化的类存在父类的时候,直接或者间接福来,其父类也必须可以序列化。

当要序列化的类中引用了其他类的对象,那么这些对象的类也必须是可序列化的,如下面代码中的Teacher 类也必须是可以序列化的

public class Student implements Serializable {    
    private Teacher teacher;
    ...
}
1
2
3
4

# Java序列化算法

Java序列化遵循以下算法:

  • 所有序列化过的,包括磁盘中的的实例对象都有一个序列化编号
  • 当试图序列化一个对象时,程序会先检查该对象是否已经被序列化过,当对象在本次虚拟机中从未被序列化过,则系统将其序列化为字节序列并输出
  • 如果某个对象在本次虚拟机中已经序列化过,则直接输出这个序列化编号

鉴于以上的算法可能会造成一个潜在的问题:当序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会输出字节序列,而第二次调用时仅仅输出一个序列化编号,即使我们改变了这个对象的一些属性,这些改变后的属性也不会序列化到磁盘上,这点在开发中需要非常注意。下面我们看一下代码:

private static void reSerialize()
{
    try(ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("student.txt"));
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("student.txt"));)
    {
        Student s=new Student("ben",18);
        oos.writeObject(s);
        Student rs1= (Student) ois.readObject();

        s.setAge(32);
        oos.writeObject(s);
        Student rs2= (Student) ois.readObject();

        System.out.println("两个对象是否相等:"+ (rs1==rs2));
        System.out.println("希望年龄变为32:"+rs2.getAge());
    }catch (Exception e)
    {
        e.printStackTrace();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

输出结果:

两个对象是否相等:true
希望年龄变为32:1812
1
2

从输出结果可以看出,修改前后反序列化出来的两个对象时绝对相等的,输出的其实是第一个对象,而且我们队年龄做的修改也没有生效。

# 自定义序列化

  • 通过tansient阻止实例变量的序列化。

    Java默认会序列化所有的实例变量,如果我们不想序列化某一个实例变量,就可以使用tansient这个关键字修饰。

    private transient String name;
    
    1
  • 通过 writeObject() 与 readObject() 方法控制序列化过程 只需要为实现了Serializable接口的类提供两个如下签名的方法,就可完全控制序列化和发序列化过程。

    private void writeObject(ObjectOutputStream out) throws IOException
    private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException
    
    1
    2

    例如我们给前面介绍的Student类添加两个如下方法。

    private void writeObject(ObjectOutputStream out) throws IOException{
       out.writeObject("hello "+name);
       out.writeInt(age);
    }
    
    private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException{
       name= (String) in.readObject();
       age=in.readInt();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    那么反序列化后name 属性的值就会加上hello 前缀。

  • 通过 writeReplace() 方法控制序列化过程

    为实现了Serializable接口的类提供 如下签名的方法

    Any-Access-Modifier Object writeReplace() throws ObjectStreamException
    
    1

    该方法在开始序列化writeObject()之前执行,所以可以在序列化对象之前对要序列化的对象做一些处理,甚至完全替换掉原来的对象。 例如下面的代码无论被序列化的对象是什么,反序列化出来的对象总是一个字符串“总有刁民想害朕”。

    private Object writeReplace() throws ObjectStreamException{
      return "总有刁民想害朕";
    }
    
    1
    2
    3
  • 通过 readResolve() 方法控制反序列化过程

    为实现了Serializable接口的类提供 如下签名的方法

    Any-Access-Modifier Object readResolve() throws ObjectStreamException
    
    1

    该方法在反序列化readObject()后执行,所以可以在反序列化后对获得的对象做一些处理,甚至完全替换为其他对象。例如下面代码无论反序列化后得到的对象是什么,都会被替换成一个字符串”昏君人人得而诛之”。

    private Object readResolve() throws ObjectStreamException{
        return "昏君人人得而诛之";
    }
    
    1
    2
    3

    这个函数在单例类实现序列化时特别有用,通过前面的介绍 我们知道,通过序列化可以不使用构造函数而获取一个类的实例,这样的话一个单例类就会存在两个实例了,就失去效用了。那么如何解决这个问题呢?

    1、最好是使用枚举enum来构建一个单例,这是最好的方法,解决了序列化以及反射生成实例的问题。

    public enum Singleton {
        INSTANCE;
    }
    
    1
    2
    3

    2、如果只是解决由于序列化导致的单例破坏问题,可以使用readResolve()方法解决,如下代码所示:

    public class Singleton implements Serializable{
        public static final Singleton INSTANCE = new Singleton();
        private Singleton() {
        }
        protected Object readResolve() {
            return INSTANCE;
        }
        ...
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# 4.2 实现Externalizable接口

如果采用这种方式的话,序列化过程必须完全由程序员自己完成,看如下代码:

public class Teacher implements Externalizable{
    private String name;
    private Integer age;
    public Teacher(String name,Integer age){
        System.out.println("有参构造");
        this.name = name;
        this.age = age;
    }
    //setter、getter方法省略

    //编写自己的序列化逻辑
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject("hello:"+name); //将name加上前缀
        out.writeInt(age);  //注掉这句后,age属性将不能被序化
    }

    //编写自己的反序列化逻辑
    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        name = ((StringBuffer) in.readObject()).reverse().toString();
        age = in.readInt();  
    }

    @Override 
    public String toString() {  
        return "[" + name + ", " + age+ "]";  
    } 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

可见Externalizable将序列化和反序列化的工作完全交给了程序员,那样的好处就是自由度变大,如果碰上牛逼程序员,效率也会提升,碰上傻逼程序员就真的傻逼了。鉴于多年编程经验,一般情况下还是使用Serializable较为稳妥,和开发效率比起来,性能就是个屁,不然Java之类的语言也不会打败C++。

编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
Java泛型
Java多线程详解

← Java泛型 Java多线程详解→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式