【Java 教程】嗯?你真的会用 Java 序列化机制?

一、序列化、反序列化概念及其使用场景

1、序列化、反序列化的概念

简单的讲,序列化就是将java对象转化成二进制保存到磁盘中去,反序列化就是从磁盘中读取文件流然后转成java对象。

2、使用场景
  • 1、网络通讯传输 java 对象数据。
  • 2、永久保存 java 对象。

二、实现序列化的方式有哪些?

JDK 提供了下面两种方式实现序列化:

  • 1、实现 Serializable 接口。

  • 2、实现 Externalizable。

下面分别实例演示两种实现方式:假设本文所有的序列化对象为 User,其拥有下面属性:

 1/**
 2 * 序列化對象
 3 * 
 4 * @Author jiawei huang
 5 * @Since 2020年1月2日
 6 * @Version 1.0
 7 */
 8public class User {
 9
10    private String userName;
11
12    private String address;
13    // ....setter/getter
14}
1、基于 Serializable 接口

我们在上面 User 对象的基础上实现 Serializable 接口,代码如下:

1public class User implements Serializable {
2    // 序列化ID
3    private static final long serialVersionUID = 1L;
4}

序列化和反序列化代码为:

 1// 序列化方法
 2public static void serialize(User user) {
 3    ObjectOutputStream outputStream = null;
 4    try {
 5        outputStream = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Administrator\\Desktop\\data.txt"));
 6        outputStream.writeObject(user);
 7    } catch (Exception e) {
 8        e.printStackTrace();
 9    } finally {
10        // close stream
11        if (outputStream != null) {
12            try {
13                outputStream.close();
14            } catch (IOException e) {
15                e.printStackTrace();
16            }
17        }
18    }
19}
20
21// 反序列化方法
22public static void deserialize() {
23    ObjectInputStream objectInputStream = null;
24    try {
25        objectInputStream = new ObjectInputStream(
26                new FileInputStream("C:\\Users\\Administrator\\Desktop\\data.txt"));
27        User user = (User) objectInputStream.readObject();
28        System.out.println(user.getAddress());
29        System.out.println(user.getUserName());
30    } catch (Exception e) {
31        // error
32        e.printStackTrace();
33    } finally {
34        // close stream
35        if (objectInputStream != null) {
36            try {
37                objectInputStream.close();
38            } catch (IOException e) {
39                // error
40                e.printStackTrace();
41            }
42        }
43    }
44}

测试代码如下:

1public static void main(String[] args) {
2    User user = new User();
3    user.setAddress("广东深圳");
4    user.setUserName("hjw");
5
6    serialize(user);
7    deserialize();
8}

输出如下:广东深圳hjw

Q1、User 实现 Serializable 接口是必须的吗?

是的,是必须的,否则报下面异常:

1java.io.NotSerializableException: ex.serializable.User
2    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
3    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
4    at ex.serializable.Main.serialize(Main.java:41)
5    at ex.serializable.Main.main(Main.java:33)

因为在 ObjectOutputStream 中执行了如下代码限制:

image

这也说明,jdk 并没有默认支持对象的序列化,为什么默认不支持呢?因为 java 的安全机制限制,我们设想一下,假设对象都默认支持序列化,那么就像上面那个 User 对象,其私有 private 修饰属性也被序列化了,那么不符合 private 的设计语义。

2、基于 Externalizable 接口
  • 1、Externalizable 接口继承自 Serializable 接口。
  • 2、Externalizable 接口允许我们自定义对象属性的序列化。
  • 3、实现 Externalizable 接口必须重写 writeExternal 和 readExternal 方法。

我们重新新建一个 User1 对象如下:

 1public class User1 implements Externalizable {
 2
 3    private String userName;
 4
 5    private String address;
 6
 7    // setter、getter
 8    @Override
 9    public void writeExternal(ObjectOutput out) throws IOException {
10        out.writeObject(userName);
11    }
12
13    @Override
14    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
15        System.out.println(in.readObject());
16    }
17
18}

测试代码如下:

1User1 user1 = new User1();
2user1.setAddress("广东深圳");
3user1.setUserName("hjw");
4user1.writeExternal(
5    new ObjectOutputStream(new FileOutputStream("C:\\Users\\Administrator\\Desktop\\data.txt")));
6user1.readExternal(new ObjectInputStream(new FileInputStream("C:\\Users\\Administrator\\Desktop\\data.txt")));

输出如下:hjw

两种实现方式的区别在于 Externalizable 允许我们自定义序列化规则,Externalizable 接口会初始化一次无参构造器,而 Serializable 不会。

三、transient 关键字和 static 成员变量

通过实现 Externalizable 接口我们可以自定义序列化的属性,同样的道理,关键字 transient 也可以达到相同的效果,但是两者之间还是有一些区别的。

  • 1、transient 修饰的变量,即使使用 private 修饰也会被序列化。
  • 2、如果我们想 private 属性不被序列化,则可以使用 Externalizable。

static 修饰的成员变量属于类全局属性,其值在 JDK1.8 存储于元数据区(1.8以前叫方法区),不属于对象范围,将不会被序列化。

下面验证 static 不会被序列化:修改 User 对象 userName 属性为 static 修饰。

1User user = new User();
2user.setAddress("广东深圳");
3user.setUserName("hjw");
4serialize(user);
5user.setUserName("mike");
6deserialize();

输出如下:广东深圳mike

我们将 hjw 序列化到了磁盘文件,结果反序列化之后得到的值却是 mike。

四、关于 serialVersionUID

java 能否反序列化成功,取决于 serialVersionUID 是否一致,我们可以把它理解成为版本号,一旦版本号不一致,将会报序列化出错。我们可以为对象声明一个自定义 serialVersionUID,也可以使用默认的 1L。

总结就是:

当我们新增一个类实现 Serializable 接口时,建议我们为其新增一个 serialVersionUID,因为假设我们没有声明 serialVersionUID,那么后面假设我们修改了该类的接口(新增字段)时,当我们再次反序列化时,就会报错。因为 java 会拿编译器根据类信息自动生成一个 id1 和反序列化得到的 id2 进行比较,如果类有改动,id2 和 id1 肯定不一致啦。

Q1:既然 static 修饰的不会被序列化,而 java 又是如何通过 serialVersionUID 进行比较的呢?

serialVersionUID 应该是一个特殊字段可以被序列化,应该可以从源码中找到答案,小编没找到,欢迎评论区留言。

五、序列化、反序列化实现深度克隆

深度克隆即把引用类型的成员也给克隆了,基于上面例子,我们新增一个类 Car,并且 User 拥有一个 Car 类型对象。

 1public class Car implements Serializable {
 2    private static final long serialVersionUID = 1L;
 3
 4    private String carName;
 5
 6    private int price;
 7
 8    // ......
 9
10}
 1public class User implements Serializable {
 2
 3    private static final long serialVersionUID = 1L;
 4
 5    private String userName;
 6    //
 7    private Car car;
 8    private String address;
 9    @Override
10    public String toString() {
11        return "User [userName=" + userName + ", carName=[" + car.getCarName() + "],price=[" + car.getPrice() + "]"
12                + ", address=" + address + "]";
13    }
14    // ......
15}

克隆方法:

1public static <T extends Serializable> T cloneObject(T obj) throws IOException {
 2    ObjectOutputStream oos = null;
 3    ByteArrayOutputStream baos = null;
 4    byte[] bytes = null;
 5    try {
 6        // 序列化
 7        baos = new ByteArrayOutputStream();
 8        oos = new ObjectOutputStream(baos);
 9        oos.writeObject(obj);
10        bytes = baos.toByteArray();
11    } catch (Exception e) {
12        e.printStackTrace();
13    } finally {
14        if (baos != null) {
15            baos.close();
16        }
17        if (oos != null) {
18            oos.close();
19        }
20    }
21
22    ByteArrayInputStream bais = null;
23    ObjectInputStream ois = null;
24    try {
25        // 反序列化
26        bais = new ByteArrayInputStream(bytes);
27        ois = new ObjectInputStream(bais);
28        return (T) ois.readObject();
29    } catch (Exception e) {
30        e.printStackTrace();
31    } finally {
32        if (bais != null) {
33            baos.close();
34        }
35        if (oos != null) {
36            ois.close();
37        }
38    }
39    return null;
40}

测试代码及输出如下:

1User user = new User();
2user.setAddress("广东深圳");
3user.setUserName("hjw");
4Car car = new Car();
5car.setCarName("单车");
6car.setPrice(300);
7user.setCar(car);
8User clonedUser = cloneObject(user);
9System.out.println(clonedUser);
1User [userName=hjw, carName=[单车],price=[300], address=广东深圳]

六、总结

  • 1、新增序列化类时,建议新增一个自定义 id,防止后续版本之间不兼容。
  • 2、static、transient 修饰的字段不会被序列化,序列化同样会序列化 private 属性。
  • 3、序列化可以实现深度克隆。
  • 4、实现序列化有两种方式,一种是直接实现 Serializable 接口,另一种是实现 Externalizable 接口,后者允许我们自定义序列化规则。

作者:拥抱心中的梦想
链接:https://juejin.im/post/5e0d86e05188253a907112e1

服务推荐

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值