marp: false

Java序列化与反序列化实训


序列化概述

序列化(Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。

反序列化(Deserialization)则是将已序列化的对象状态信息恢复为对象的过程。

在分布式系统中,序列化机制允许对象通过网络在不同的Java虚拟机之间传输。

在Hadoop生态系统中,序列化尤为重要,因为MapReduce、YARN等组件需要在不同节点之间传输大量数据,高效的序列化机制能够显著提升性能。


Java序列化基础

要实现序列化,一个类必须实现java.io.Serializable接口。这个接口是一个标记接口,没有定义任何方法。一旦一个类实现了这个接口,Java虚拟机(JVM)就可以将这个类的对象序列化并通过网络传输。


实训目标

  1. 掌握Java对象序列化机制
  2. 学习使用Socket进行网络通信
  3. 实现客户端与服务器之间的对象传输
  4. 理解transient关键字的作用

示例:Student类

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
35
36
37
38
39
40
import java.io.Serializable;

public class Student implements Serializable {
// 序列化版本ID,确保版本兼容性
private static final long serialVersionUID = 1L;

private String name;
private String sex;
private String address;
private String phoneNumber;
// transient修饰的字段不会被序列化
private transient String emotion;

public Student(String name, String sex, String address, String phoneNumber, String emotion) {
this.name = name;
this.sex = sex;
this.address = address;
this.phoneNumber = phoneNumber;
this.emotion = emotion;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + "'" +
", sex='" + sex + "'" +
", address='" + address + "'" +
", phoneNumber='" + phoneNumber + "'" +
", emotion='" + emotion + "'" + // emotion将为null,因为它是transient的
'}';
}

public void say(){
System.out.println("高兴死了,我来到了这个世界上~~~\n" + this);
}

public void sayAgain(){
System.out.println("我又活过来了~~~\n" + this);
}
}

transient关键字

Java中的transient关键字表示”短暂的”。被transient修饰的成员变量,在对象序列化过程中会被忽略。这意味着:

  1. transient变量不会被序列化到字节流中
  2. 反序列化后,transient变量会被初始化为默认值(对象为null,基本类型为0或false)
  3. 适用于存储临时数据或敏感信息(如密码)

服务器端代码

服务器端负责接收客户端发送的序列化对象,并进行反序列化:

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
35
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ObjectServer {
public static void main(String[] args) {
try {
// 创建服务器Socket,监听8888端口
ServerSocket serverSocket = new ServerSocket(80);
System.out.println("服务器启动,等待客户端连接...");

// 等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("客户端已连接,准备接收对象...");

// 创建对象输入流,用于反序列化
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());

// 读取并反序列化对象
Student student = (Student) ois.readObject();

// 调用对象方法,验证反序列化是否成功
student.sayAgain();

// 关闭资源
ois.close();
socket.close();
serverSocket.close();

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

服务器端IP地址

1
2
3
4
5
6
7
8
9
10
无线局域网适配器 WLAN:

连接特定的 DNS 后缀 . . . . . . . :
IPv6 地址 . . . . . . . . . . . . : 240a:42b8:a00:161e:8f54:737a:808d:7a24
临时 IPv6 地址. . . . . . . . . . : 240a:42b8:a00:161e:1c5e:437a:119:1c0e
本地链接 IPv6 地址. . . . . . . . : fe80::b7e2:1e50:ac88:6f32%3
IPv4 地址 . . . . . . . . . . . . : 192.168.43.172
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : fe80::7c1c:47ff:fea1:ae35%3
192.168.43.1

客户端代码

客户端负责创建对象,序列化后通过Socket发送到服务器:

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
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;

public class ObjectClient {
public static void main(String[] args) {
try {
// 创建Socket,连接到服务器
Socket socket = new Socket("192.168.43.172", 80);
System.out.println("已连接到服务器,准备发送对象...");

// 创建对象
Student zhangsan = new Student("zhangsan", "male", "山东省济南市山东女子学院", "18888888888", "happy");
zhangsan.say();

// 创建对象输出流,用于序列化
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());

// 序列化并发送对象
oos.writeObject(zhangsan);
System.out.println("对象已发送到服务器");

// 关闭资源
oos.close();
socket.close();

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

运行步骤

  1. 首先编译所有Java文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    (base) PS C:\Users\qingy\Desktop>javac -encoding UTF-8 .\Student.java
    (base) PS C:\Users\qingy\Desktop> javac -encoding UTF-8 .\ObjectClient.java
    (base) PS C:\Users\qingy\Desktop> javac -encoding UTF-8 .\ObjectServer.java

    (base) PS C:\Users\qingy\Desktop> java ObjectServer
    服务器启动,等待客户端连接...
    客户端已连接,准备接收对象...
    我又活过来了~~~
    Student{name='zhangsan', sex='male', address='山东省济南市山东女子学院', phoneNumber='18888888888', emotion='null'}
    (base) PS C:\Users\qingy\Desktop>
  2. 启动服务器:

    1
    2
    (base) PS C:\Users\qingy\Desktop> java ObjectServer
    服务器启动,等待客户端连接...
  3. 启动客户端:

    1
    2
    3
    4
    5
    (base) PS C:\Users\qingy\Desktop> java ObjectClient
    已连接到服务器,准备发送对象...
    高兴死了,我来到了这个世界上~~~
    Student{name='zhangsan', sex='male', address='山东省济南市山东女子学院', phoneNumber='18888888888', emotion='happy'}
    对象已发送到服务器
  4. 查看服务器端输出:

    1
    2
    3
    客户端已连接,准备接收对象...
    我又活过来了~~~
    Student{name='zhangsan', sex='male', address='山东省济南市山东女子学院', phoneNumber='18888888888', emotion='null'}

预期输出

客户端输出:

1
2
3
4
已连接到服务器,准备发送对象...
高兴死了,我来到了这个世界上~~~
Student{name='zhangsan', sex='male', address='山东省济南市山东女子学院', phoneNumber='18888888888', emotion='happy'}
对象已发送到服务器

服务器输出:

1
2
3
4
服务器启动,等待客户端连接...
客户端已连接,准备接收对象...
我又活过来了~~~
Student{name='zhangsan', sex='male', address='山东省济南市山东女子学院', phoneNumber='18888888888', emotion='null'}

注意事项

  1. serialVersionUID:为序列化类添加serialVersionUID是一个好习惯,可以确保类的版本兼容性

  2. transient关键字:注意观察服务器端接收到的对象中,emotion字段值为null,这是因为它被transient修饰

  3. 资源关闭:记得关闭所有的输入输出流和Socket连接,最好使用try-with-resources语法

  4. 异常处理:网络编程中需要处理IOException和ClassNotFoundException等异常

  5. 端口占用:如果80端口被占用,可以修改为其他端口号