Java基础-系列化反序列化




为什么要序列化

打游戏,需要存档,当关闭电脑后,下次打开还在 游戏进度 游戏角色名称等等一些属性,而内存数据一般都是在我们关闭电源或者关闭软件就会清零,这为什么还会在呢,这就是将我们的进程,游戏名称等进行序列化存在硬盘中,这个过程就是对象的持久化,也就是我们今天要讲的对象序列化。对象的序列化逆过程就叫做反序列化,反序列化也很好理解就是将硬盘中的信息读取出来形成对象
而在计算机编程中 数据的传输和存储需要序列化原因是

  • 传输:计算机语言处理的是二进制数据,将我们传输的内容 比如带有请求参数的对象序列化,转换为二进制数据,能更好的进行处理
    网络传输需要序列化是因为网络传输的数据必须是二进制数据,而程序处理的是对象,序列化将对象转换为二进制数据以便传输
  • 存储 只从计算机进程来看,如果有十万次请求,十万个session,这样内存可能就不够用,那边我们可以将部分session序列化后存入到硬盘中,需要用了就从硬盘中反序化使用,说白了**,存储就数据从内存序列化后的字节流内容存入到硬盘**

什么是序列化 反序列化

在java中

  • 把对象转换为字节序列的过程称为对象的序列化
  • 把字节序列恢复为对象的过程称为对象的反序列化
    在这里插入图片描述
    1.根据某种序列化算法,将对象序列化(结果可能是:字节序列、json串等,取决于使用的序列化算法)
    2.将序列化的结果通过流写入到载体中 (文件、数据库、redis、网络等 )
    3.当要用到这些对象的时候,程序通过输入流从载体中读取出序列化的流信息,根据对应的反序列化算法,重建对象

系列化,反序列化的作用

  • 序列化最重要的作用:在数据传输和存储对象时.保证对象的完整性和可传递性,,对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
  • 反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象
    总结:核心作用就是对象状态的保存和重建。(整个过程核心点就是字节流中所保存的对象状态及描述信息)

序列化优点

①将对象转为字节流存储到硬盘上,当JVM停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间(永久性保存对象)。
②序列化成字节流形式的对象可以进行网络传输(二进制形式),方便了网络传输。
③通过序列化可以在进程间传递对象

不序列化也可以进行网络传输,那网络传输为什么需要序列化呢?

网络传输需要序列化是因为网络传输的数据必须是二进制数据,而程序处理的是对象,序列化将对象转换为二进制数据以便传输

为什么网络传输时对象要序列化,而字符串就不用序列化

网络传输会将对象转换成字节流传输,序列化可以将一个对象转化成一段字符串编码,以便在网络上传输或者做存储处理,使用时再进行反序列
而字符串不用序列化的原因是如果你看过javaSE的源码,你就知道,字符串是已经实现了Serializable接口的,所以它已经是序列化了的

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

系列化使用及注意事项

系列化使用场景:

1、当想把的内存中的对象状态保存到一个文件中或者数据库中表(内存到数据库)
2、当想用套接字在网络上传送对象(网络传输)(或者不同线程之间通讯)
3、当想通过RMI传输对象的时候(分布式传输)
序列化的目的有两个,第一个是便于存储,第二个是便于传输
我们一般的实体类不需要程序员再次实现序列化的时候,请想两个问题:
第一:存储媒体里面,是否是有其相对应的数据结构?
第二:这个实体类,是否需要远程传输(或者两个不同系统甚至是分布式模块之间的调用)

序列化过程中相关注意事项

a)序列化时,只对对象的状态进行保存,而不管对象的方法;
b)当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
d)并非所有的对象都可以序列化,不是所有的类都需要序列化,有很多原因了,比如:

  • 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行RMI传输(RMI远程文件传输) 等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。又或者一些特殊属性的数据 比如账户密码 用户信息等,这些序列化被存储或者传输都可以通过反序列化泄露
  • 资源分配方面的原因,比如socket(网络通信连接的一端称为一个socket。网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API)),thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且也是没有必要这样实现,说白了网络通信 线程这些都是通道,我们对通道内内容进行序列化,通道本身是传输使用的是不需要序列化的,序列化可以任意生成实例而不受限制,如果有的对象生成的实例是受限制的,比如只能生成10个实例,或者是单例的,这个很难保证

Java序列化和反序列化是Java中的两项重要技术。它们可以帮助您将对象持久化到磁盘或网络,并在需要时将其还原到内存中。Java序列化和反序列化在许多场景中都有应用,包括网络传输、RMI和对象持久化。在使用Java序列化和反序列化时,您需要注意安全性、性能和兼容性等问题

为什么保存到文件中要序列化

在对对象流进行读写操作时,最大问题就是对象引用问题以及带来的后续问题

  • 对象引用问题:假如我有两个类,分别是A和B,B类中含有一个指向A类对象的引用,现在我们对两个类进行实例化{ A a = new A(); B b = new B(); },这时在内存中实际上分配了两个空间,一个存储对象a,一个存储对象b,接下来我们想将它们写入到磁盘的一个文件中去,就在写入文件时出现了问题!因为对象b包含对对象a的引用,所以系统会自动的将a的数据复制一份到b中,这样的话当我们从文件中恢复对象时(也就是重新加载到内存中)时,内存分配了三个空间,而对象a同时在内存中存在两份
  • 数据一致性问题:与上述引用问题相关,数据一致性问题是由于对象引用导致的多个对象副本的存在而产生的。这使得维护对象数据的一致性变得复杂和困难

序列化机制的解决方案:

  • 对象编号:序列化机制为所有保存到磁盘的对象分配一个唯一的序列号。当保存一个对象时,系统首先检查该对象是否已经被保存。如果之前已经保存过,那么只需写入一个标记,表明这是一个与已保存的具有特定序列号的对象相同的数据。这样就避免了重复数据的存储和潜在的数据不一致问题41。
  • transient关键字:对于某些不想被序列化的数据字段,可以在定义时给它们加上transient关键字。这样,在序列化过程中,序列化机制会跳过这些字段,不将它们写入文件,也不会尝试恢复它们41。
  • 自定义序列化方法:对于那些既想序列化又无法直接序列化的数据字段,序列化机制提供了自定义的readObject和writeObject方法。这些方法是私有的,并不由开发者显式调用,而是由序列化机制自动调用。通过这些方法,可以手动控制数据字段的序列化和反序列化过程

前端序列化

前端请求参数到后端序列化过程

1.前端发起请求:前端通过浏览器向后端发送HTTP请求,可以使用Fetch、Axios、Ajax等库来发起请求
2.请求头的设置:请求头包含了关于浏览器、客户端所使用的操作系统、浏览器版本、请求的客户端支持什么类型等信息。其中,Content-Type是请求头中非常重要的一部分,它指定了请求体的格式。常见的Content-Type包括application/x-www-form-urlencoded、multipart/form-data和application/json2。
3.请求参数的序列化:前端在发送请求时,需要将请求参数进行序列化。如果使用application/x-www-form-urlencoded格式,参数会被编码为key1=value1&key2=value2…的形式。如果使用application/json格式,参数会被编码为JSON字符串2。
4.请求传递给Web服务器:前端请求通过网络通信发送给Web服务器,一般使用HTTP协议1。
5.后端接收并处理请求:Web服务器接收到前端发送来的请求后,根据请求URL、请求方法等信息,将请求路由到指定的后端处理程序。后端处理程序根据请求的具体类型及内容,在处理过程中可能需要读写数据库、调用其他API等操作1。
6.请求参数的反序列化:后端接收到请求后,需要将请求参数进行反序列化。如果前端使用的是application/x-www-form-urlencoded格式,后端可以通过request.getParameter()方法获取参数。如果使用的是application/json格式,后端通常需要使用相应的库(如Spring MVC中的@RequestBody注解)来解析JSON字符串2。
7.后端处理并返回响应:后端处理程序完成处理后,将返回JSON、HTML、XML等类型的响应内容1
8**.响应传递给前端**:后端程序处理完成后,将响应内容传递给Web服务器。Web服务器收到后端返回的响应内容后,将响应内容封装成HTTP响应格式再返回给前端

前端请求参数 json/xml的数据传递的转化:

在数据传输(也可称为网络传输)前,先通过序列化工具类将Java对象序列化为json/xml文件。
在数据传输(也可称为网络传输)后,再将json/xml文件反序列化为对应语言的对象

Java实现序列化和反序列化

序列化Serializable接口

public interface Serializable {
}

从代码里面看出其实这是一个空接口,并没有需要去实现的方法,相当于一个标识,可以被序列化。一个java中的类只有实现了Serializable接口,它的对象才是可序列化的,不使用则会抛出异常

外部化Externalizable接口

public interface Externalizable extends java.io.Serializable {
	//writeExternal(ObjectOutput out))的方法进行序列化
    void writeExternal(ObjectOutput out) throws IOException;
    
    // readExternal(ObjectInput in)的方法进行反序列化
	void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

从代码中可以看出 Externalizable 继承了Serializable ,但是它定义了两个方法,
比如我们定义一个Students 成员变量name age等
①ObjectOutputStream调Student对象的writeExternal(ObjectOutput out))的方法进行序列化
②ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化

Serializable接口进行序列化代码示例

public class Boy implements Serializable {
    private static final long serialVersionUID = 1L;
    private  transient String name ;//声明name此属性不被序列化
    private int age ;
    public Boy(String name,int age){	// 通过构造设置内容
        this.name = name ;
        this.age = age ;
    }
    public String toString() {    // 覆写toString()方法
        return "姓名:" + this.name + ";年龄:" + this.age;
    }
}
public class BoyTest {
    public static void main(String[] args) throws Exception {
        loadItem();
        saveItem();
    }
    private static void loadItem() throws Exception {
        FileOutputStream ostream = new FileOutputStream("boy.txt");
        ObjectOutputStream p = new ObjectOutputStream(ostream);
        p.writeObject(new Boy("张三",22));
        p.close();
    }

    private static void saveItem() throws Exception {
        FileInputStream inputStream = new FileInputStream("boy.txt");
        ObjectInputStream p = new ObjectInputStream(inputStream);
        Boy boy = (Boy)p.readObject();
        p.close();
        System.out.println(boy);
    }
}

打印台显示
在这里插入图片描述
如果我们把Boy里面的transient 修饰符拿掉,那么打印内容应该是 姓名:张三;年龄: 22
一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问

还有就是如果我们如果在Boy类没有实现implements Serializable 接口,那么就会执行就会报错,报错原因就是boy没有序列化

外部化Externalizable接口进行序列化代码示例

public class Girl implements Externalizable {
    private String name = "小芳";
    private int age  = 18;
    public Girl(){
    }
   // 进行序列化
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("开始执行writeExternal方法");
        out.writeObject("这是一个输出序列化:");
        out.write(20);//在序列化的数据最后随便加个数值20以区分 对象自定义的age = 18
        System.out.println("已经往指定文件或其余目的地要保存的属性信息");
    }
    // 进行反序列化
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("开始执行readExternal方法");
        name = (String)in.readObject();
        age = in.read();  //把数字20加进内存来
        System.out.println(name+age);
        System.out.println("已经在文件其余地方读取被保存的信息这一步");
    }
    public void putOut(){ //测试
        System.out.println(name+age);
    }
}

再写一个进行操作的读写文件的测试类

public class AppTest {
    private void saveGame() {
        Girl m = new Girl();
        if (m != null) {
            try {
                m.putOut();//最初的对象属性是 小芳 18
                FileOutputStream ostream = new FileOutputStream("t.txt");
                ObjectOutputStream p = new ObjectOutputStream(ostream);
                p.writeObject(m); //重写的writeExternal()自动执行,将我们定义的变量加进t.txt
                p.flush();
                ostream.close();
            } catch (IOException ioe) {
                System.out.println("Error saving file:");
                System.out.println(ioe.getMessage());
            }
        }
    }
    private void loadGame() {
        try {
            FileInputStream instream = new FileInputStream("t.txt");
            ObjectInputStream p = new ObjectInputStream(instream);
            Girl m = (Girl) p.readObject();//重写的readExternal()自动执行,读取t.txt内容到内存,也就是输入流
            m.putOut();
            instream.close();
        } catch (Exception e) {
            System.out.println("Error loading file:");
            System.out.println(e.getMessage());
        }
    }
    public static void main(String[] args) {
        new AppTest().saveGame();
        new AppTest().loadGame();
    }
}

在这里插入图片描述
从打印内容显示
第一步 初始化对象变量是定义的对象成员变量
第二步 输出到文件t.txt,这里内容就变成我们重写的writeExternal方法里面可
第三步 从文件他t.xt输入到内存的内容就是readExternal
但是初始化对象的变量还是 小芳 18,这个并没有改变,我们只是在缓存到t.txt文件内容时候进行了改变下内容,代码里面打印显示出来,这样好区分这个writeExternal方法执行改变的步骤节点和内容

再有就是在实体类Girl成员变量加上transient修饰符 看下打印内容

public class Girl implements Externalizable {
    private String name = "小芳";
    private transient int age  = 18;
    public Girl(){
    }

运行Apptest 发现打印内容不变,可见transient关键字对实现Externalizable接口的类的变量没有影响

transient用处 以及和Serializable接口,Externalizable接口关联使用关系

该关键字通过上述代码和Serializable接口,Externalizable接口 搭配使用可以发现

  • 当一个类实现Serializable接口时,所有的属性和方法都会自动序列化。使用transient关键字来阻止字段被序列化
  • 当一个类实现Externalizable接口时,没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,因此,transient关键字对于Externalizable接口的类的变量并没有影响,根据Java官方文档,当类实现Externalizable接口时,序列化机制不会自动处理任何字段,包括被transient修饰的字段。这意味着,无论字段是否为transient,都需要在writeExternal和readExternal方法中显式地处理。因此,transient关键字在这种情况下确实不会起作用,是否序列化某个字段完全由开发者在这两个方法中的实现决定

哪些情况常见transient,使用如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient(暂态的) [ˈtrænzɪənt] 关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化

java序列化(Serializable)和外部化(Externalizable)的主要区别

  • 定义展示上:Serializable接口没有实现任何方法,而Externalizable接口是继承了Serializable接口并且定义了两个方法writeExternal和readExternal
  • 功能处理上:任何需要序列化的类实现Serializable接口的 不需要序列化的字段是需要添加transient处理防止序列化的,否则默认序列化。而对于实现Externalizable接口的进行序列化的内容,是在重写writeExternal方法中进行的,需要序列化的成员变量是开发者自己具体实现决定的,transient这个关键字没有对Externalizable接口没有用
  • 可扩展性上:Serializable接口没有具体实现方法,JVM负责大部分的序列化和反序列化过程。Externalizable接口有具体方法,允许开发者通过重写这两个方法完全控制序列化和反序列化的实现细节,当然难度也更大些
  • 性能上:由于Serializable接口使用反射机制,因此在性能上可能会比Externalizable慢。Externalizable通过提供自定义的序列化和反序列化方法,可以提高性能
  • 安全上:由于Serializable接口会默认序列化所以非静态、非瞬态字段,由jvm,而Externalizable需要开发者具体实现所有的序列化细节 所以Externalizable安全性更高些

序列化与反序列化的两种方式

网上很多说三种 算比较细分的,但就两种

  • 实现Serializable接口
    所需类:ObjectInputStream()和ObjectOutputStream()
    方法: readObject()和writeObject();
    从内存输出到文件
        ObjectOutputStream p = new ObjectOutputStream(ostream);
        p.writeObject(new Boy("张三",22));

从文件读取输入到内存

        FileInputStream inputStream = new FileInputStream("boy.txt");
        ObjectInputStream p = new ObjectInputStream(inputStream);
        Boy boy = (Boy)p.readObject();
  • 实现Externalizable接口的
    在重写的writeExternal和readExternal方法中实现序列化和反序列化
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject("这是一个输出序列化:");
        out.write(20);//在序列化的数据最后随便加个数值20以区分 对象自定义的age = 18
    }
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String)in.readObject();
        age = in.read();  //把数字20加进内存来
    }

在输入输出流 读写这个 对象时候,自动化执行重写的writeExternal和readExternal方法

serialVersionUID

serialVersionUID 是什么 有什么用

serialVersionUID 是一个用于标识序列化类版本的特殊字段。它是一个长整型数值,通常在实现 Serializable 接口的类中使用

  • 手动指定:开发人员可以显式地在类中声明 private static final long serialVersionUID 字段,并手动赋予一个长整型数值
private static final long serialVersionUID = 1L;
  • 自动生成:如果未手动指定 serialVersionUID,Java 编译器将根据类的结构自动生成一个 serialVersionUID。生成算法通常基于类的字段、方法、父类等信息,以确保类结构发生变化时,serialVersionUID 会发生变化
// 自动生成的 serialVersionUID 示例
private static final long serialVersionUID = -1234567890123456789L;

serialVersionUID 是什么 有什么用

serialVersionUID 的存在是为了处理序列化和反序列化过程中的版本兼容性问题。当一个类被序列化后,它的字节表示可能会存储在磁盘上或通过网络传输到不同的 JVM(Java 虚拟机)。在这种情况下,如果类的结构发生了变化,例如添加了新的字段或方法,那么反序列化时就可能出现版本不一致的问题,用于确保序列化和反序列化的一致性
在 Java 中,serialVersionUID 并不是强制必须显式声明的,不是强制必须要写的。如果类实现了 Serializable 接口但是不写serialVersionUID的话,但存在类结构变化导致反序列化失败的风险,程序仍能编译和运行,但存在潜在风险


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值