序列化

概念

序列化就是将对象存储到特定存储介质中的过程,也就是将对象的状态转换为可保持或可传输格式的过程

本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。

核心:

  1. 保存对象状态
  2. 对象的状态可存储

最终目的:

对象可以跨平台存储和进行网络传输

进行跨平台存储和网络传输的方式就是IO,其支持的数据格式就是字节数组

java

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该 对象的数据 、 对象的类型和 对象中存储的属性 等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。 对象的数据 、 对象的类型 和 对象中存储的数据 信息,都可以用来在内存中创建对象。

简单说序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。

为什么要把Java对象序列化呢?

因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。

序列化的方式

序列化只是一种拆装组装对象的规则,那么这种规则肯定也可能有多种多样,比如现在常见的序列化方式有:

JDK(不支持跨语言)、JSONXMLHessianKryo(不支持跨语言)、ThriftProtostuffFST(不支持跨语言)

Java 是如何实现序列化的?

Serializable

序列化需要类实现java.io.Serializable接口,也就是说不实现此接口的类将不能序列化或反序列化。Serializable类的所有子类都是可序列化的。序列化接口没有方法或字段,仅用于标识可串行化的语义。我们把这样的空接口称为“标记接口”(Marker Interface)实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

public interface Serializable {
}

ObjectOutputStream

java.io.ObjectOutputStream类,将Java对象的原始数据类型写出到流,实现对象的持久存储。

public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants

构造方法

ObjectOutputStream提供了一个public的构造方法:

//创建一个指定OutputStream的ObjectOutputStream
public ObjectOutputStream(OutputStream out);

序列化操作

序列化为byte[]

package com.itlaobing.demo;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Arrays;

public class ObjectOutputStreamTest {

	public static void main(String[] args) throws IOException {
		try(ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos)){
			//写内容
			 // 写入int:
			oos.writeInt(12345);
            // 写入String:
			oos.writeUTF("测试");
            // 写入Object:
			oos.writeObject(Double.valueOf(123.456));
			System.out.println(Arrays.toString(bos.toByteArray()));
            System.out.println(bos.toString());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}

ObjectOutputStream既可以写入基本类型,如intboolean,也可以写入String(以UTF-8编码),还可以写入实现了Serializable接口的Object

因为写入Object时需要大量的类型信息,所以写入的内容很大

对象序列化

创建一个Student类,并实现Serializable接口

package com.itlaobing.demo.seri;

import java.io.Serializable;

public class Student implements Serializable{
	
	private String name;
	
	private double score;
	
	private String gender;

	public Student(String name, double score, String gender) {
		super();
		this.name = name;
		this.score = score;
		this.gender = gender;
	}

//省略getter/setter方法
}

例子:

package com.itlaobing.demo.seri;

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

public class ObjectOutputStreamTest2 {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		
		try(FileOutputStream fos = new FileOutputStream("d:\\temp\\student.txt");ObjectOutputStream oos = new ObjectOutputStream(fos);){
			Student stu = new Student("尼古拉斯", 99.99, "男");
			oos.writeObject(stu);
		}
	}

}

思考:如果我Student类中的某个属性不想被序列化怎么办呢?

transient关键字,transient瞬态,修饰的成员,不会被序列化。

Student类修改为:

package com.itlaobing.demo.seri;

import java.io.Serializable;

public class Student implements Serializable{
	
	private String name;
	
	private double score;
	
	private transient String gender;

	public Student(String name, double score, String gender) {
		super();
		this.name = name;
		this.score = score;
		this.gender = gender;
	}

	//省略getter/setter

	@Override
	public String toString() {
		return "Student [name=" + name + ", score=" + score + ", gender=" + gender + "]";
	}
}

序列化:

package com.itlaobing.demo.seri;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class ObjectOutputStreamTest3 {

	public static void main(String[] args) throws IOException {
		Student stu = new Student("尼古拉斯", 99.99, "男");
		System.out.println("序列化前: " + stu);
		try(FileOutputStream fos = new FileOutputStream("d:\\temp\\student2.txt");ObjectOutputStream oos = new ObjectOutputStream(fos);){
			oos.writeObject(stu);
		}
	}

}

反序列化操作

ObjectInputStream

java.io.ObjectOutputStream相反,java.io.ObjectInputStream负责从一个字节流读取Java对象。将之前使用ObjectOutputStream序列化的原始数据恢复为对象.

public class ObjectInputStream
    extends InputStream implements ObjectInput, ObjectStreamConstants

构造方法

ObjectInputStream有一个public修饰的构造方法

//创建一个指定InputStream的ObjectInputStream
public ObjectInputStream(InputStream in);

反序列化

如果能找到一个对象的class文件,我们可以进行反序列化操作,调用 ObjectInputStream 读取对象的方法。

D:\\temp\\student.txt反序列化成对象。

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ObjectInputStreamTest {

	public static void main(String[] args) throws ClassNotFoundException, IOException {
		try(FileInputStream in = new FileInputStream("D:\\temp\\student2.txt");
				ObjectInputStream ois = new ObjectInputStream(in)){
			Object obj = ois.readObject();
			System.out.println(obj.getClass());//class com.itlaobing.demo.Student
			if(obj instanceof Student) {
				Student stu = (Student) obj;
				System.out.println(stu);
			}
		}		
	}
}

反序列化过程中,将字节序列转换成了对象,这个对象的创建没有经过构造方法。这个对象中的域(属性)的值是固定的,如果是transient声明的域(属性)值是其类型的默认值。

对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个ClassNotFoundException 异常。

另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个 InvalidClassException异常。

为了避免这种class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的serialVersionUID静态常量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID的值,这样就能自动阻止不匹配的class版本:

// 加入序列版本号
private static final long serialVersionUID = 1L;

这个时候序列化完成后,修改类后,可以反序列化。

要特别注意反序列化的几个重要特点:

反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。

Java序列化常见问题

问题一:static 属性不能被序列化

原因:序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。

问题二:Transient 属性不会被序列化

问题三:序列化版本号serialVersionUID

所有实现序列化的对象都必须要有个版本号,这个版本号可以由我们自己定义,当我们没定义的时候JDK工具会按照我们对象的属性生成一个对应的版本号。

安全性

因为Java的序列化机制可以导致一个实例能直接从byte[]数组创建,而不经过构造方法,因此,它存在一定的安全隐患。一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。

实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。

测试:

public static void main(String[] args) {
        // What Where How Why
        try(ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos);
                FileOutputStream fileOutputStream = new FileOutputStream("D:\\小练习\\讲义\\java-base\\src\\com\\kfm\\base\\io\\o\\test.txt")) {
            
            Student student = new Student("张三", 88.8, "男");
            oos.writeObject(student);
            //baos.toByteArray() 序列化字节数组
            fileOutputStream.write(baos.toByteArray());

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }