序列化
概念
序列化就是将对象存储到特定存储介质中的过程,也就是将对象的状态转换为可保持或可传输格式的过程
本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。
核心:
- 保存对象状态
- 对象的状态可存储
最终目的:
对象可以跨平台存储和进行网络传输
进行跨平台存储和网络传输的方式就是IO,其支持的数据格式就是字节数组。
java
中
Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该 对象的数据 、 对象的类型和 对象中存储的属性 等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。 对象的数据 、 对象的类型 和 对象中存储的数据 信息,都可以用来在内存中创建对象。
简单说序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]
数组。
为什么要把Java对象序列化呢?
因为序列化后可以把byte[]
保存到文件中,或者把byte[]
通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。有序列化,就有反序列化,即把一个二进制内容(也就是byte[]
数组)变回Java对象。有了反序列化,保存到文件中的byte[]
数组又可以“变回”Java对象,或者从网络上读取byte[]
并把它“变回”Java对象。
序列化的方式
序列化只是一种拆装组装对象的规则,那么这种规则肯定也可能有多种多样,比如现在常见的序列化方式有:
JDK
(不支持跨语言)、JSON
、XML
、Hessian
、Kryo
(不支持跨语言)、Thrift
、Protostuff
、FST
(不支持跨语言)
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
既可以写入基本类型,如int
,boolean
,也可以写入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);
}
}