JAVA序列化

发布时间:2024年01月12日

??对象序列化(object serialization)是java支持的通用机制,可以将任何对象写出到流中,并在之后将其回读。简单来说,就是可以将对象数据保存为文件,甚至可以通过网络传输,在这之后或者别的主机上恢复当前保存的数据状态。

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

序列化方式

序列化方式:Serializable接口和Externalizable接口两种方式。
1.Seriallizable接口
Serializable接口是一个标记接口,没有方法或字段。一旦实现了此接口,就标志该类的对象就是可序列化的。

public interface Serializable {
}

2.Externalizable接口
Externalizable继承了Serializable接口,还定义了两个抽象方法:writeExternal()和readExternal(),如果开发人员使用Externalizable来实现序列化和反序列化,需要重写writeExternal()和readExternal()方法

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

Java序列化常用API

java.io.ObjectOutputStream
java.io.ObjectInputStream
java.io.Serializable
java.io.Externalizable

java.io.ObjectOutputStream类

表示对象输出流,它的writeObject(Object obj)方法可以对指定obj对象参数进行序列化,再把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream

表示对象输入流,它的readObject()方法,从输入流中读取到字节序列,反序列化成为一个对象,最后将其返回。

序列化ID

Java提供了两种方式来生成序列化ID:默认方式和自定义方式。
1.如果类没有显式声明serialVersionUID字段,Java会根据类的结构信息自动生成一个序列化ID。生成算法通常是基于类的名称、字段、方法等生成一个哈希值。
2.如果类显式声明了serialVersionUID字段,Java会使用该字段的值作为序列化ID。

import java.io.Serializable;

class Person implements Serializable {
//默认方式生成序列化ID
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    // constructor, getters, setters, etc.
}

class Person implements Serializable {
//自定义方式生成ID
    private static final long serialVersionUID = 123456789L;
    private String name;
    private int age;

    // constructor, getters, setters, etc.
}

??实现Serializable接口时,不是必须要有Serializable id,但建议为每个可序列化的类都提供一个Serializable id。这个id可以保证在序列化和反序列化过程中,对象的唯一性和一致性。如果没有提供Serializable id,Java会自动生成一个id,但这样可能会导致在不同的JVM中生成的id不一致,从而导致序列化和反序列化失败。因此,为了保证可序列化类的兼容性和稳定性,建议为每个可序列化的类都显式地提供一个Serializable id。

在进行Java序列化时需要注意如下事项:

(1)类要实现序列化功能,只需实现java.io.Serializable接口即可
(2)在进行序列化和反序列化时必须保持序列化ID的一致,一般使用private static final long serialVersionUID定义序列化ID
(3)序列化并不保存静态变量
(4)在需要序列化父类变量时,父类也需要实现Serilizable接口
(5)使用Transient关键字可以阻止该变量被序列化,在被反序列化后,transient变量的值被设置为对应类型的初始值。例如,int类型变量的是0,Object类型变量的值是null
具体的序列化实现代码如下:

import java.io.Serializable;
//通过实现Serializable接口定义可序列化的Worker类
public class Worker implements Serializable{
    //定义序列化的ID
    private static final long serialVersionUID = 123456789L;
    //name属性将被序列化
    private String name;
    //transient修饰的变量不会被序列化
    private transient int salary;
    //静态变量属于类信息,不属于对象的状态,因此不会被序列化
    static int age=100;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }

}

注意:transient修饰的属性和static修饰的静态属性不会被序列化。
??对象通过序列化后在网络上传输,基于网络安全,我们可以在序列化前将一些敏感字段(用户名、密码、身份证号)使用密钥进行加密,在反序列化后再基于密钥对数据进行解密。这样即使数据在网络中被劫持,由于缺少密钥也无法对数据进行解析,这样可以在一定程度上保持序列化对象的数据安全。
??我们可以基于JDK原生的ObjectOutputStream和ObjectInputStream类实现对象的序列化及反序列化,并调用其writeObject()和readObject()方法实现自定义序列化策略。具体的实现代码如下:

import java.io.*;
import java.lang.reflect.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        //序列化数据到磁盘
        long serializationstartTime = System.currentTimeMillis();
        FileOutputStream fos = new FileOutputStream("work.out");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        for(int i=0;i<5000000;i++){
            Worker testObject  = new Worker();
            testObject.setName("alex"+i);
            oos.writeObject(testObject);
        }
        oos.flush();
        oos.close();
        long serializationEndTime = System.currentTimeMillis();
        System.out.println(String.format("java serialization user time:%d",(serializationEndTime-serializationstartTime)));
        //反序列化磁盘数据并解析数据状态
        FileInputStream fis = new FileInputStream("work.out");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Worker worker = null;
        try{
            while((worker = (Worker) ois.readObject())!=null){
                //worker为反序列化后的对象
            }
        }catch(EOFException e){ //在文件读取完成时会抛出EOFException

        }
        long deserializationEndTime = System.currentTimeMillis();
        System.out.println(String.format("java deserialization use timme:%d",(deserializationEndTime-serializationEndTime)));

    }

}

实际开发中实现对象的序列化,通常使用的是第三方工具,而不是JDK原生的ObjectOutputStream和ObjectInputStream类。

序列化的应用:

1.序列化机制可以将对象保存到硬盘上,减轻内存压力,也起到了持久化的作用。
2.序列化机制是Java对象实现在RPC(远程过程调用)或者网络中传输。
注:序列化在实际使用过程中除了使用Java的序列化技术来实现,还可以使用FastJson等序列化框架来实现。

相关问题

1.serialVersionUID有什么用?

??JAVA序列化的机制是通过判断类的serialVersionUID来验证版本是否一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同,反序列化成功,如果不相同,就抛出InvalidClassException异常。

2.为什么需要手动指定serialVersionUID

??在Java中,当一个类实现了Serializable接口并需要进行序列化和反序列化操作时,Java会自动生成一个默认的serialVersionUID。但是,由于默认的serialVersionUID是基于类的内部结构自动生成的,因此当类的内部结构发生改变时,serialVersionUID的值也会发生变化。这就会导致序列化和反序列化操作时版本不匹配的问题,从而无法正确地反序列化对象。
??为了避免这个问题,可以手动指定一个稳定的serialVersionUID。手动指定serialVersionUID可以确保在类的内部结构发生变化时,serialVersionUID的值不会改变,从而保证对象的序列化和反序列化操作的正确性。
??需要注意的是,如果两个类的serialVersionUID相同,但是它们的实现并不兼容(比如字段数量或类型不同),则在反序列化时也会出现问题。因此,即使serialVersionUID相同,也不能保证两个类可以相互转换。

注意:实际开发中不要随意修改serialVersionUID,阿里巴巴开发手册中,第四章OOP规约的第13条解释如下:

【强制】序列化类新增属性时,请不要修改serialVersionUID字段,避免反序列失败;如果 完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID值。说明:注意serialVersionUID不一致会抛出序列化运行时异常。

4.为什么不推荐jdk自带的序列化

  • 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
  • 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
  • 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

5.在 Java 中,Serializable 和 Externalizable 有什么区别

??Externalizable继承了Serializable,给我们提供 writeExternal() 和 readExternal() 方法, 让我们可以控制 Java的序列化机制, 不依赖于Java的默认序列化。正确实现 Externalizable 接口可以显著提高应用程序的性能。

6.序列化时,如何让某些成员不要序列化?

??可以用transient关键字修饰,它可以阻止修饰的字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如int型的值会被设置为 0,对象型初始值会被设置为null
??static静态变量和transient 修饰的字段是不会被序列化的。静态(static)成员变量是属于类级别的,而序列化是针对对象的。transient关键字修字段饰,可以阻止该字段被序列化到文件中。

7.如果要序列化的不同对象,其属性都指向了同一个对象,怎么办?
??对此,Java核心卷2的解释是:每个对象都是用一个序列号(serial number)来保存的。也就是说,序列化的每个对象关联的都是一个序列号,而不再是运行时的内存地址。
处理的具体算法为:
保存对象

  • 每个对象引用都关联一个序列号;
  • 对每个对象,第一次遇到时,保存其输出对象到输出流中;
  • 如果某个对象之前已经被保存过,那么只写出“与之前保存过的序列号为x的对象相同”;

恢复对象
通过读回恢复对象,整个过程反过来。

  • 对于输入流中的对象,在第一次遇到其序列号时,构建它,并使用流中数据初始化它,然后记录这个顺序号和新对象之间的关联。

  • 当遇到“与之前保存过的序列号为x的对象相同”标记时,获取与这个顺序号相关联的对象引用。

8.如果子类实现了子类实现了Serializable接口,父类没有实现Serializable接口的话,父类不会被序列化。反序列化时子类的父类属性就会丢失。
9.序列化是针对对象的,故static成员变量属于类级别不能被序列化。如果某个序列化类的成员变量是对象类型,则该对象类型的类必须实现序列化。

文章来源:https://blog.csdn.net/weixin_44145526/article/details/135556957
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。