IO流的序列化和反序列化

IO流的序列化和反序列化 - 小简博客 (ideaopen.cn)

何为序列化和反序列化

序列化:指把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。这个过程称为序列化。通俗来说就是将数据结构或对象转换成二进制串的过程

反序列化:把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。也就是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

好像有些说的不够清晰,我们缩短一下。

序列化:把对象转换为字节序列的过程称为对象的序列化。(常见的就是存文件)

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

再通俗一定。

  • 序列化:将对象写入到IO流中
  • 反序列化:从IO流中恢复对象

为何会诞生它们

我们想要学好一个东西,那就肯定必须要去了解它的作用和原因。

为了节省时间,我在博客园大佬博客里面找到了一个清晰明了的解释。

JavaBeanSerializeable

程序创建的每个JavaBean类都实现Serializeable接口。

不知道你们看到这句话的时候,有没有感到疑惑?嘿嘿,那就对了,这个你们可能没去了解过。

那我们现在就来讲一讲这两个玩意,不过,我这篇文章是讲解序列化的,所以我肯定不会去自己写一份说明,这样太费时间了,于是我将会截图知乎上的讲解或其他网上博主的说明。

但是,说的不明白的文章我肯定不会选择的。

我们看看知乎大佬的说明!

这就是我们所说的JavaBean的由来了。它或许你们可以完美理解成是一个概念,而非具体的东西。

而我们想要一个类可以实现JavaBean这个概念,我们需要有如下条件。

1、所有属性为private

2、提供默认构造方法

3、提供getter和setter

4、实现serializable接口

这里我们就发现了第二个疑问,serializable,这是啥?

这个玩意,是个接口,这个接口有什么用呢?

Serializable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。

也就是说,他就是个标记一样,它没什么内容需要你实现,你继承了这个接口,就给了一个标记,有这个标记的类就可序列化。

不过同时也必须满足上面的四个条件才可以!!!

我们写一段试试。

示范

这就是一个满足序列化的类,这个类我定义了两个字段,最后一个重写是返回了一个String值,他就和普通类作用一样,只不过需要满足一些条件。

package IoDemo.Demo;

import java.io.Serializable;

public class IoObj implements Serializable {
    private String str;
    private int num;

    //默认构造
    public IoObj() {
    }
    public IoObj(String str, int num) {
        this.str = str;
        this.num = num;
    }

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }
    @Override
    public String toString() {
        return "IoObj [str=" + str + ", num=" + num + "]";
    }
    
}

如何序列化

好了,这里有一个支持序列化的类了,我们现在就来试试如何将这个类的对象序列化。

  • 步骤一:创建一个ObjectOutputStream输出流;
  • 步骤二:调用ObjectOutputStream对象的writeObject输出可序列化对象。

Object是对象的意思,我们这里可以的序列化与反序列化又可以叫对象流。

我们先看代码:

package IoDemo.Demo;
import java.io.*;
public class IoDemoTest {
    public static void main(String[] args) {
        IoObj ioObj = new IoObj("序列化测试",1);
        // 创建Object流对象
        ObjectOutputStream oos = null;
        try {
            // 创建文件对象
            File file = new File("D:\\test.txt");
            // 创建文件输出流对象
            FileOutputStream fos = new FileOutputStream(file);
            // 创建Object流对象
            oos = new ObjectOutputStream(fos);
            // 写入对象
            oos.writeObject(ioObj);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

这里我用了几步呢?

新建对象

IoObj ioObj = new IoObj("序列化测试",1);

写入文件的位置

我们想要将对象写入到D:\\test.txt,于是我们需要用File对象保存地址。

然后我们还需要将我们序列化的内容写入到文件,所以我们还得创建文件的输出流。

// 创建文件对象
File file = new File("D:\\test.txt");
// 创建文件输出流对象
FileOutputStream fos = new FileOutputStream(file);

序列化(创建对象流对象)

// 创建Object流对象
oos = new ObjectOutputStream(fos);

这里我们将上面的输出流对象给了对象流对象。

然后对象流对象有一个方法,是writeObject()方法,用于写入对象。

// 写入对象
oos.writeObject(ioObj);

这样,我们就将IoObj对象写入到了文件,我们看一下。

注意: 当序列化一个对象到文件时, 按照 Java 的标准约定是给文件一个 .ser 扩展名。

至于,你可能疑惑,这写入的内容是什么?看不懂。那看看下面的解释就明白了。

反序列化

package IoDemo.Demo;
import java.io.*;
public class IoDemoTest {
    public static void main(String[] args) {
        IoObj ioObj = new IoObj("序列化测试",1);
        // 创建Object流对象
        ObjectOutputStream oos = null;
        try {
            // 创建文件对象
            File file = new File("D:\\test.txt");
            //读取文件
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
            // 读取对象
            IoObj ioObj1 = (IoObj) ois.readObject();
            System.out.println(ioObj1.toString());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

反序列化同理可知了,我就不多说了。

ObjectInputStream是对象的写入流。

// 读取对象
IoObj ioObj1 = (IoObj) ois.readObject();

这里是将读取的值赋值给对象,readObject()方法就是用于读取对象流文件内容。

至于(IoObj),你可以理解为强转。

扩展

这儿有一位博客园大佬,我也借鉴了它文章,大家可以看看。

大佬的博客

文章有许多扩展知识点,我就直接为了方便阅读,一起搬过来,版权链接上面给了。

反序列化并不会调用构造方法。反序列的对象是由JVM自己生成的对象,不通过构造方法生成。

成员是引用的序列化

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

如存在这种成员:private Person person

同一对象序列化多次的机制

同一对象序列化多次,会将这个对象序列化多次吗?答案是否定的。Java序列化同一对象,并不会将此对象序列化多次得到多个对象。

Java序列化算法
  1. 所有保存到磁盘的对象都有一个序列化编码号
  2. 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
  3. 如果此对象已经序列化过,则直接输出编号即可。

Java序列化算法潜在的问题

由于Java序列化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。

可选的自定义序列化

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

public class Person implements Serializable {
   //不需要序列化名字与年龄
   private transient String name;
   private transient int age;
   private int height;
   private transient boolean singlehood;
   public Person(String name, int age) {
       this.name = name;
       this.age = age;
   }
   //省略get,set方法
}

public class TransientTest {
   public static void main(String[] args) throws Exception {
       try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
            ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {
           Person person = new Person("9龙", 23);
           person.setHeight(185);
           System.out.println(person);
           oos.writeObject(person);
           Person p1 = (Person)ios.readObject();
           System.out.println(p1);
       }
   }
}
//输出结果
//Person{name='9龙', age=23', singlehood=true', height=185cm}
//Person{name='null', age=0', singlehood=false', height=185cm}

从输出我们看到,使用transient修饰的属性,Java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。

两种序列化对比
实现Serializable接口实现Externalizable接口
系统自动存储必要的信息 程序员决定存储哪些信息
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 必须实现接口内的两个方法
性能略差 性能略好

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

总结
  1. 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
  2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  3. 如果想让某个变量不被序列化,使用transient修饰。
  4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
  5. 反序列化时必须有序列化对象的class文件。
  6. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
  7. 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
  8. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
  9. 建议所有可序列化的类加上serialVersionUID版本号,方便项目升级。
posted @ 2022-04-12 19:47  JanYork(小简)  阅读(64)  评论(0编辑  收藏  举报