对象流、序列化详解

一、序列化和反序列化的概念和引出

1.序列化的用处

要想通过对象流实现对象的传输,带传输的对象要先进行序列化处理,才能保证对象能准确地保存和读取。

注:
①序列化是一种对象持久化的手段,普遍应用在网络传输、RMI等场景;
②对象流:对象作为一种复合型数据,不仅包括多种不同类型的属性数据,还有和类相关的信息,所以简单的流处理无法实现对象的传输和永久保存。为此,Java提供了对象流和对象序列化机制,来保证对象作为一个整体进行I/O流传输。

2.概念

对象的序列化是将对象转换成字节序列把字节序列恢复为对象的过程称为对象的反序列化。

3.Serializable接口

一个类如果实现了java.io.Serializable接口,这个类的实例(对象)就是一个序列化的对象。Serializable接口中没有方法,因此实现该接口的类不需要额外实现其他的方法。可序列化类的所有子类型本身都是可序列化的。

4.执行过程

当实现了该接口的对象进行输出时,JVM将按照一定的格式(序列化信息)转换成字节进行传输和存储到目的地。当对象输入流从文件或网络上读取对象时,会先读取对象的序列化信息,并根据这一信息创建对象。

5.serialVersionUID

实现了序列化接口的类中会有一个Long型的静态常量serialVersionUID,这个值用于识别具体的类。如果不设置,JVM会自动分配一个值。为了保证类的准确识别,在定义类时应显示设置该值。
​字​面​意​思​上​是​序​列​化​的​版​本​号​,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。

6、解决问题:什么时候对象需要被序列化?

①把对象保存到文件中(存储到物理介质)
②对象需要在网络上传输时

二、对象输入流与对象输出流

对象流、序列化详解_第1张图片

1.对象输入流类ObjectInputStream

①实现对象的输入操作
  构造方法: public ObjectInputStream(InputStream in)    //创建从指定输入流读取的ObjectInputStream
②类中的方法

Object readObject() // 从ObjectInputStream流中读取对象

③是字节输入流InputStream类的子类

2.对象输入流类ObjectOutputStream

①实现对象的输出操作
构造方法:public ObjectOutputStream(OutputStream out)  //创建写入指定输出流的ObjectInputStream对象
②类中的方法

void writeObject(Object o) // 将指定对象o写入ObjectOutputStream流中

③是字节输出流OutputStream类的子类

三、通过一个例子进行介绍

创建一个可序列化类,将该类的对象写入到文件中。用对象输入流读取并显示对象信息。
import java.io.*;
import java.util.Scanner;

public class ObjectStream {
	public static void main(String[] args) {
		try {
			File file;// File类
			FileInputStream fin;// 文件字节输入流
			FileOutputStream fout;// 文件字节输出流
			ObjectInputStream oin;// 对象输入流
			ObjectOutput oout;// 对象输出流

			Scanner scanner = new Scanner(System.in);
			System.out.println("请输入文件名,例如d:\\foo");
			String filename = scanner.nextLine();

			file = new File(filename);// 创建文件对象
			if (!file.exists()) {// 如果文件不存在
				file.createNewFile();// 创建新文件
			}

			// 序列化
			fout = new FileOutputStream(file);// 实例化文件字节输出流
			oout = new ObjectOutputStream(fout);// 实例化对象输出流
			Person person = new Person("张三", 20);// person此时是一个序列化的对象
			oout.writeObject(person);// 用writeObject将对象写入到文件中,序列化的标志
			System.out.println("序列化成功!");
			oout.close();// 关闭对象输出流
			fout.close();// 关闭文件输出流
			System.out.println("对象写入完毕!");

			// 反序列化
			System.out.println("文件" + filename + "的内容是:");
			Person object;
			fin = new FileInputStream(file);// 创建文件输入流
			oin = new ObjectInputStream(fin);// 创建对象输入流
			try {
				object = (Person) oin.readObject();// 强制转换,读取对象中的信息,反序列化的标志
				System.out.println("反序列化成功!");
				System.out.println("读取的对象信息:");
				System.out.println("用户名:" + object.getName());
				System.out.println("年龄:" + object.getAge());

			} catch (ClassNotFoundException e) {
				System.out.println("读取对象失败!");
			}
			oin.close();// 关闭对象输入流
			fin.close();// 关闭文件输入流

		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

class Person implements Serializable {// 对象序列化
	private static final long serialVersionUID = 1234567890L;// 设置Long型的静态常量serialVersionUID,用于识别具体的类

	String name;
	int age;

	public Person(String name, int age) {// 含参构造
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

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

	public int getAge() {
		return age;
	}

	public void setAge() {
		this.age = age;
	}

}

对象流、序列化详解_第2张图片

四、补充说明:

①writeObject & readObject

有时候需要将一个类的很多对象的信息写入文件,方便二次读取,但是如果一个对象所包含的属性太多,将这些属性信息依次写入文件所需要的代码比较繁杂,为了省事可以使用writeObject方法直接写入对象。读取信息时,只需要使用readObject方法直接读出来。 需要注意的是,使用writeObject所写入的类必须要实现Serializable接口!

查找API文档:

public final void writeObject(Object obj) throws IOException // 将指定的对象写入ObjectOutputStream
obj -----> 要写入的对象

②定义规则:

a、当对象被序列化时,JVM只序列化其字段值,方法和构造器不参与序列化;
b、如果对象的某个字段引用了另一个对象,则被引用对象的字段也将被序列化;
c、有些对象类(如FileInputStream)是不可序列化的,因为字段值与操作系统相关的即时信息;
d、访问控制修饰符对序列化无影响;
e、静态变量不参与序列化,因为它不属于对象。

③另一种解释

对象的序列化:将对象的属性值以字节的形式存储到文件中,是个将对象的状态信息转换为可存储或传输的形式的过程。
对象的反序列化:读取文件的字节内容(原对象的属性值)生成新的对象就叫反序列化。

【注意:Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的serialVersionUID是否一致。】

④如果是要序列化对象是一组,可以先把对象存在对象数组里,然后对该对象数组进行序列化。

    Dog dog = new Dog("haha",3,"雄",0);
    Dog dog2 = new Dog("lala",2,"雄",1);
    Dog dog3 = new Dog("wawa",4,"雌",2);
    Dog[] dogs = {dog,dog2,dog3};//将对象存在数组中
    -----> oos.writeObject(dogs); //对一组进行序列化
    
    //当反序列化时,对象数组生成新的对象数组
   Dog[] dog = (Dog[])ois.readObject();  //读取的是对象数组的序列化,所有需要强转型为Dog
                                          类型的数组,接收也是Dog类型的数组

⑤实际应用

a、在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有10万用户并发访问,就可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些Session先序列化到硬盘中,等要用的时候再把保存在硬盘中的对象还原到内存中。
b、当两个进程在进行远程通信时,彼此可以发送各种类型数据,无论是何种类型的数据,都会以二进制序列的形式在网络上传送,发送方需要把这个Java对象转换为字符序列才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

⑥序列化与反序列化步骤

a、对象序列化:

1)创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流(FileOutputStream);
2)通过对象输出流的writeObject()方法写对象。

b、对象反序列化

1)创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流(FileInputStream);
2)通过对象输入流的readObject()方法读取对象。

⑦serialVersionUID的取值

a、serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码做了修改,再重新编译的话,新生成的类文件的serialVersionUID的取值有可能发生变化;
b、类的serialVersionUID完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同;
c、为了提高serialVersionUID的独立性和确定性,应在一个可序列化的类中显示定义serialVersionUID,赋予明确的值;
d、阿里巴巴要求程序员谨慎修改serialVersionUID 字段的值在这里插入图片描述
serialVersionUID是用来验证版本一致性的。所以在做兼容性升级的时候,不要改变类中serialVersionUID的值。

serialVersionUID 既然是验证版本一致性的,在做版本升级的时候(非兼容性升级),记得要修改这个字段的值,这样可以避免序列化混乱。

总结:如果一个类实现了Serializable接口,一定要记得定义serialVersionUID,否则会发生异常。之所以会发生异常,是因为反序列化过程中做了校验,并且如果没有明确定义的话,会根据类名及属性等自动生成一个。

⑧显示定义serialVersionUID的用途

a、在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
b、在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

你可能感兴趣的:(I/O流(Java))