java基础——序列化

Posted 路半_知风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java基础——序列化相关的知识,希望对你有一定的参考价值。

为什么需要序列化

 我们知道,java程序在运行时,对象是在对上创建的,如果程序停止了,那么这个对象也不复存在了。当我们需要将对象存储在硬盘上时,就需要序列化的技术了。

序列化就是一种将对象转换成字节序列的过程。反序列化就是将字节序列代表的对象恢复成原来的样子。通过序列化与反序列化,我们可以实现进程间的通信。

序列化使用场景:

1、持久化存储某个对象。

2、进程间的通信(包括网络中传输对象)。

序列化的实现方式

如果要序列化某个对象,那么这个类应该实现 Serializable 接口或者 Externalizable 接口之一。

实现 Serializable  接口

Serializable 接口是一个空接口,里面没有任何的方法,只要一个对象实现了该接口,那么这个对象就可以被序列化。

序列化步骤:

1、创建 ObjectOutputStream 输出流对象, 指定对象字节流的输出位置。

2、通过 writeObject() 将对象的字节流写入文件。

3、关闭输出流。

反序列化步骤:

1、创建一个 ObjectInputStream 输入流,指定需要读取的文件。

2、通过 readObject() 获取已经序列化的对象。

package javaIO;

import java.io.Serializable;

//创建一个 Student 类实现 Serializable 接口
public class Student implements Serializable{
    
    private String name;
    private int num;
    
    //只写了带参的构造函数, 没有提供无参的构造函数
    public Student(String name, int num) {
        super();
        this.name = name;
        this.num = num;
        System.out.println("反序列化是否调用了构造函数?");
    }
    
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getNum() {
        return num;
    }
    public void setNum(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "Student [name=" + name + ", num=" + num + "]";
    }
}
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TestSerializable {
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        
        // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\\\javatest\\\\student.txt"));
        
        Student student = new Student("Zz-feng", 1001);
        
        //通过 writeObject() 将对象的字节流写入文件
        out.writeObject(student);
        
        //关闭输出流
        out.close();
        
        // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\\\javatest\\\\student.txt"));
        
        //通过 readObject() 将以序列化的对象恢复
        Object object = in.readObject();
        
        System.out.println(object);  //输出  Student [name=Zz-feng, num=1001]
        
        //关闭输入流
        in.close();
    }
}

通过上面的例子我们看到,在反序列的过程中,并没有调用类的构造函数。反序列化的对象是由 JVM 自己生成的,不需要调用构造方法。

成员变量是引用的序列化

如果一个可序列化的类的成员不是基本类型,也不是String类型,而是一个引用类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。

public class Person{
    //省略相关属性与方法
}
public class Teacher implements Serializable {

    private String name;
    private Person person;

    public Teacher(String name, Person person) {
        this.name = name;
        this.person = person;
    }

     public static void main(String[] args) throws Exception {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
            Person person = new Person("路飞", 20);
            Teacher teacher = new Teacher("雷利", person);
            oos.writeObject(teacher);
        }
    }
}

我们看到程序直接报错,因为Person类的对象是不可序列化的,这导致了Teacher的对象不可序列化。

类的静态变量不会被序列化。

同一对象序列化多次

同一对象序列化多次,会将这个对象序列化多次吗?答案是否定的。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TestSerializable {
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        
        // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\\\javatest\\\\student.txt"));
        
        Student s1 = new Student("Zz-feng", "1001");
        Student s2 = new Student("mytest", "1002");
        Student s3 = new Student("mytest", "1002");
        
        //通过 writeObject() 将对象的字节流写入文件
        out.writeObject(s1);
        out.writeObject(s2);
        out.writeObject(s3);
        out.writeObject(s1);
        out.writeObject(s2);
        out.writeObject(s3);
        
        //关闭输出流
        out.close();
        
        // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\\\javatest\\\\student.txt"));
        
        //通过 readObject() 将以序列化的对象恢复
        Object read_s1 = in.readObject();
        Object read_s2 = in.readObject();
        Object read_s3 = in.readObject();
        Object read_s11 = in.readObject();
        Object read_s22 = in.readObject();
        Object read_s33 = in.readObject();
        
        System.out.println(read_s1 == read_s2);        // false
        System.out.println(read_s2 == read_s3);        // false
        
        System.out.println(read_s1 == read_s33);    // false        
        System.out.println(read_s2 == read_s33);    // false
        
        System.out.println(read_s1 == read_s11);    // true
        System.out.println(read_s2 == read_s22);    // true
        
        //关闭输入流
        in.close();
    }
}

从输出结果来看,同一个对象只会序列化一次,并不是每次调用 writeObject() 方法都会序列化一个对象。

序列化过程

1、如果一个对象已经被序列化过,那么这个对象会保存一个序列化编码号,并将序列化编号一起保存在输出的序列化文件中。

2、当程序试图序列化一个对象时,首先会检查该对象是否已经存在序列化编号,如果有,则直接输出编号到序列化文件中。否则进行序列化。

序列化存在的问题

根据序列的过程,如果一个对象已经被序列化了,那么再次调用 writeObject() 方法并不会再次序列化该对象。如果该对象的属性是可以 set() 等方式修改的,那么反序列化得到的对象并不会显示被修改的内容。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TestSerializable {
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        
        // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\\\javatest\\\\student.txt"));
        
        Student s1 = new Student("Zz-feng", "1001");
        Student s2 = new Student("mytest", "1002");
        
        //通过 writeObject() 将对象的字节流写入文件
        out.writeObject(s1);
        
        s1.setName("newName");
        s1.setNum("newNum");
        
        out.writeObject(s1);
        out.writeObject(s1);
        
        //关闭输出流
        out.close();
        
        // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\\\javatest\\\\student.txt"));
        
        //通过 readObject() 将以序列化的对象恢复
        Object read_s1 = in.readObject();
        Object read_s11 = in.readObject();
        
        System.out.println(read_s1);    // Student [name=Zz-feng, num=1001]
        System.out.println(read_s11);    // Student [name=Zz-feng, num=1001]
        
        //关闭输入流
        in.close();
    }
}

自定义序列化

有些时候,我们有这样的需求,某些属性不需要序列化。使用 transient 关键字选择不需要序列化的字段。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TestSerializable {
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        
        // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\\\javatest\\\\student.txt"));
        
        Student s1 = new Student("Zz-feng", "1001", 18, "香波地岛");
        
        //通过 writeObject() 将对象的字节流写入文件
        out.writeObject(s1);
        
        out.writeObject(s1);
        
        //关闭输出流
        out.close();
        
        // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\\\javatest\\\\student.txt"));
        
        //通过 readObject() 将以序列化的对象恢复
        Student read_s1 = (Student)in.readObject();
        
        System.out.println(read_s1);               // Student [name=Zz-feng, num=1001]
        System.out.println(read_s1.getAge());      // 0
        System.out.println(read_s1.getAddress());  // null
        
        //关闭输入流
        in.close();
    }
}

通过上面的代码我们发现了一个问题,尽管 transient  可以决定哪些内容是不需要序列化的,但在反序列化的过程中,JVM会忽视那些没有被序列化的部分,这样我们并不能完整的获得这个对象。对于 transient  修饰的成员变量会赋默认值。

对象的某些信息可能是敏感信息,比如银行卡号等,如果直接序列化可能存在信息泄露的风险,如果使用 transient  不进行序列化,那么反序列化时又得不到这个对象的完整信息。为了解决这些问题,我们可以重写以下三个方法,从而对序列化的信息进行加密,防止信息泄露:

1、private  void  writeObject(ObjectOutputStream  out)     //序列化时会调用此方法

2、private  void  readObject(ObjectInputStream  in)      //反序列化时会调用此方法

3、private  void  readObjectNoData()     //当序列化的类版本和反序列化的类版本不同时,或者 ObjectInputStream 流被修改时,会调用此方法。

Serializable 接口是一个空接口,上面的三个方法都不是必需的,但一般我们会重写前面两个。

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

//创建一个 Student 类实现 Serializable 接口
public class Student implements Serializable{
    
    private String name;
    private String num;
    private transient int age;
    private transient String address;
    
    
    //自定义序列化
    private void writeObject(ObjectOutputStream out) throws IOException {
        
        //只序列化以下3个成员变量
        out.writeObject(name);
        out.writeInt(age);
        
        //写入反序后的信息,当然我们也可以使用其他加密方式。这样别人打开文件,看到的就不是真正的信息,更安全。
        out.writeObject(new StringBuffer(address).reverse());
    }

    //自定义反序列化。注意:read()的顺序要和write()的顺序一致。比如说序列化时写的顺序是name、age、address,反序列化时读的顺序也要是name、age、address
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        
        //readObject()返回的是Object,要强制类型转换
        this.name= (String)in.readObject();      
        this.name=(String)in.readObject();
        
        //反序才得到真正的信息
        StringBuffer pwd=(StringBuffer)in.readObject();
        this.address=pwd.reverse().toString();
    }
}
public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Student s1=new Student ("张三", "1234", 18, "空岛");

        //序列化
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./obj.txt"));
        //调用我们自定义的writeObject()方法
        out.writeObject(s1);
        out.close();

        //反序列化
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("./obj.txt"));
        //调用自定义的readObject()方法
        Student s = (Student )in.readObject();     
        in.close()

        //测试
        System.out.println(s.getAge());    //18
        System.out.println(s.getName());   //张三
        System.out.println(s.getAddress());   //空岛
    }
} 

除了上面的方法外,我们还可以使用 private/default/protected/public  Object  writeReplace(){......} 或者 private/default/protected/public  Object  writeReplace(){......}。这两个方法返回一个 Object 对象,重写该方法,可以做一些格式化处理。

//implements Serializable
class User implements Serializable{
    private int id;
    private String name;
    private String password;

    public User(int id,String name,String password){
        this.id=id;
        this.name=name;
        this.password=password;
    }

    //......

    private Object writeReplace(){
        String info="请编号为"+id+"的客户"+name+"到1号柜台办理业务。";
        return info;
    }
}
//implements Serializable
class User implements Serializable{
    private int id;
    private String name;
    private String password;
    //......其他成员变量

    public User(int id,String name,String password){
        this.id=id;
        this.name=name;
        this.password=password;
    }

    //...........

    //用指定的对象替换掉反序列化读取的对象
    private Object readResolve(){
        String info="请编号为"+id+"的客户"+name+"到1号柜台办理业务。";
        return info;
    }

}

writeReplace:在序列化时,会先调用此方法,再调用 writeObject 方法。此方法可将任意对象代替目标序列化对象。

readResolve:在反序列化读取对象后,会自动调用此方法,将读取的对象替换为指定的对象。反序列化出来的对象被立即丢弃。此方法在readeObject后调用。常用来反序列化单例类,保证单例的唯一性。

两者只是作用的时间点不同,可以联合使用。

Externalizable:强制自定义序列化

除了实现 Serializable 接口外,还可以通过 Externalizable 接口实现序列化。该接口中有两个方法:

public interface Externalizable extends java.io.Serializable {
     void writeExternal(ObjectOutput out) throws IOException;    //调用writeObject()时,会自动调用此方法来序列化对象  
     void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;    //调用readObject()时,会自动调用此方法来反序列化
}
//implements Externalizable
class User implements Externalizable{
    private int id;
    private String name;
    private String password;
    //......其他成员变量

    //必须要有无参的构造函数
    public User(){

    }

    public User(int id,String name,String password){
        this.id=id;
        this.name=name;
        this.password=password;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getPassword() {
        return password;
    }

    //自定义序列化
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(id);
        out.writeObject(name);
        out.writeObject(password);
    }

    //自定义反序列化。注意读的顺序要和写的顺序一致。
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.id=in.readInt();
        this.name=(String)in.readObject();
        this.password=(String)in.readObject();
    }
}

Externalizable 接口不同于 Serializable 接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供 pulic 的无参构造器,因为在反序列化的时候需要反射创建对象。

两种序列化对比

实现Serializable接口实现Externalizable接口
系统自动存储必要的信息,有没有无参构造函数都行 程序员决定存储哪些信息,不需要有无参构造函数
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 必须实现接口内的两个方法
性能略差 性能略好

虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。

版本控制——serialVersionUID

在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。

一个类实现了 Serializable 接口,如果我们没有显示的指定版本号,那么 JVM 会给这个类指定一个默认的 serialVersionUID,默认样式为:

private static final long serialVersionUID = 1L;

当然,我们也可以显示的指定版本号:

private static final long serialVersionUID = 123456789L;

serialVersionUID 主要是为了解决版本问题,如果一个类的方法或字段有所改动,当我们设置了 serialVersionUID 后,在反序列化是就会报出异常。如果我们没有设置serialVersionUID,由于默认的 serialVersionUID 的值是一样的,所以在反序列化过程中,缺少的字段会默认空值,多余的字段会被丢弃。

使用 serialVersionUID 的情形:

1、如果只是修改了方法,反序列化不容影响,则无需修改版本号;

2、如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;

3、如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

参考资料

java序列化,看这篇就够了

Java 自定义序列化、反序列化

serialVersionUID的作用

以上是关于java基础——序列化的主要内容,如果未能解决你的问题,请参考以下文章

JSP基础

JAVA基础-Serializable序列化学习总结

Smith-Waterman算法及其Java实现

[Java基础]对象(反)序列化流

JSP 基础语法

Java基础:封装