序列化

序列化

  • 将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。

下面是序列化和反序列化常见应用场景:

  • 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
  • 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
  • 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

Java序列化操作

一个对象要想序列化,必须满足两个条件:

  • 该类必须实现java.io.Serializable 接口,Serializable是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException。

  • 该类的所有属性必须是可序列化的

  • 不可被序列化的情况

    • static修饰的字段:静态优先于非静态加载到内存中(静态优先于对象进入到内存中),被static修饰的成员变量不能被序列化
    • transient修饰的字段:如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient关键字修饰。
  • transient 只能修饰变量,不能修饰类和方法。

  • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0

serialVersionUID 有什么作用?

序列化号 serialVersionUID 属于版本控制的作用。反序列化时,会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的 serialVersionUID

为什么不推荐使用 JDK 自带的序列化?

我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:

  • 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
  • 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
  • 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。相关阅读:应用安全:JAVA 反序列化漏洞之殇 - CryinJava 反序列化安全漏洞怎么回事? - Monica
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Person implements Serializable{
private static final long serialVersionUID = 1L;
private String name;
//private static int age; //静态字段不能被序列化
//private transient int age; //瞬态修饰成员,不能被序列化
private int age;


public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}

ObjectOutputStream

  • ObjectOutputStream 用于将对象序列化为字节流。

  • 主要方法:

    • writeObject(Object obj):将对象写入输出流,实现对象的序列化。
  • 示例:

    1
    2
    3
    4
    5
    6
    7
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.dat"))) {
    MyClass myObject = new MyClass(); // MyClass 需要实现 Serializable 接口
    oos.writeObject(myObject);
    } catch (IOException e) {
    e.printStackTrace();
    }

ObjectInputStream

  • ObjectInputStream 用于从字节流中反序列化对象。

  • 主要方法:

    • readObject():从输入流中读取对象,实现对象的反序列化。
  • 示例:

    1
    2
    3
    4
    5
    6
    7
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.dat"))) {
    MyClass myObject = (MyClass) ois.readObject();
    // 对象已经被反序列化,可以使用 myObject 进行操作
    } catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
    }

注意:

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

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

  • 发生这个异常的原因如下:

    • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
    • 该类包含未知数据类型
    • 该类没有可访问的无参数构造方法

Properties属性集

java.util.Properties extends Hashtable<k,v> implements Map<k,v>

  • Properties类表示了一个持久的属性集
  • Properties 可保存在流中或从流中加载
  • Properties集合是一个唯一和IO流相结合的集合
  • 可以使用Properties集合中的方法store,把集合中的临时数据,持久化写入到硬盘中存储
  • 可以使用Properties集合中的方法load,把硬盘中保存的文件(键值对)读取到集合中使用
  • 属性列表中每个键及其对应值都是一个字符串,Properties集合是一个双列集合,key和value默认都是字符串

Properties集合有一些操作字符串的特有方法

Object setProperty(String key, String value):调用 Hashtable 的方法 put。

String getProperty(String key):通过key找到value值,此方法相当于Map集合中的get(key)方法

Set stringPropertyNames():返回此属性列表中的键集,其中该键及其对应值是字符串,此方法相当于Map集合中的keySet方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void show01() {
//创建Properties集合对象
Properties prop = new Properties();
//使用setProperty往集合中添加数据
prop.setProperty("赵丽颖","168");
prop.setProperty("迪丽热巴","165");
prop.setProperty("古力娜扎","160");
//prop.put(1,true);

//使用stringPropertyNames把Properties集合中的键取出,存储到一个Set集合中
Set<String> set = prop.stringPropertyNames();

//遍历Set集合,取出Properties集合的每一个键
for (String key : set) {
//使用getProperty方法通过key获取value
String value = prop.getProperty(key);
System.out.println(key+"="+value);
}
}

store方法

可以使用Properties集合中的方法store,把集合中的临时数据,持久化写入到硬盘中存储

void store(OutputStream out, String comments)

void store(Writer writer, String comments)

OutputStream out:字节输出流,不能写入中文

Writer writer:字符输出流,可以写中文

String comments:注释,用来解释说明保存的文件是做什么用的,不能使用中文,会产生乱码,默认是Unicode编码

,一般使用””空字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void show02() throws IOException {
//1.创建Properties集合对象,添加数据
Properties prop = new Properties();
prop.setProperty("赵丽颖","168");
prop.setProperty("迪丽热巴","165");
prop.setProperty("古力娜扎","160");

//2.创建字节输出流/字符输出流对象,构造方法中绑定要输出的目的地
FileWriter fw = new FileWriter("prop.txt");

//3.使用Properties集合中的方法store,把集合中的临时数据,持久化写入到硬盘中存储
prop.store(fw,"save data");

//4.释放资源
fw.close();

//使用字节流
prop.store(new FileOutputStream("prop2.txt"),"");
}

load方法

可以使用Properties集合中的方法load,把硬盘中保存的文件(键值对),读取到集合中使用

void load(InputStream inStream)

void load(Reader reader)

InputStream inStream:字节输入流,不能读取含有中文的键值对

Reader reader:字符输入流,能读取含有中文的键值对

注意:

  • 存储键值对的文件中,键与值默认的连接符号可以使用=,空格(其他符号)
  • 存储键值对的文件中,可以使用#进行注释,被注释的键值对不会再被读取
  • 存储键值对的文件中,键与值默认都是字符串,不用再加引号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void show03() throws IOException {
//1.创建Properties集合对象
Properties prop = new Properties();

//2.使用Properties集合对象中的方法load读取保存键值对的文件
prop.load(new FileReader("prop.txt"));
//prop.load(new FileInputStream("09_IOAndProperties\\prop.txt"));

//3.遍历Properties集合
Set<String> set = prop.stringPropertyNames();
for (String key : set) {
String value = prop.getProperty(key);
System.out.println(key+"="+value);
}
}

序列化
http://example.com/2023/06/26/Java/JavaIO/6. 序列化/
作者
PALE13
发布于
2023年6月26日
许可协议