从浅入深理解序列化和反序列化

文章目录

      • 什么是java序列化
      • 什么情况需要使用 Java 序列化
      • 为什么要序列化
      • 序列化和反序列化过程如下
      • RPC 框架为什么需要序列化
      • 序列化用途
        • 序列化机制可以让对象地保存到硬盘上,减轻内存压力的同时,也起了持久化的作用
        • 序列化机制让Java对象可以在网络传输
      • 实现对象序列化需要做哪些工作
      • Java序列化常用API
        • Serializable 接口
        • Externalizable 接口
        • java.io.ObjectOutputStream类
        • java.io.ObjectInputStream
      • 如何实现对象的序列化
        • Serializable 实现序列化
        • 实现Externalizable接口
          • writeObject 和 readObject 自定义序列化策略,代码示例
      • 什么是 transient?
      • 什么是serialVersionUID
        • serialVersionUID的作用
        • serialVersionUID的生成有哪三种方式
        • 序列化类增加属性时,最好不要修改serialVersionUID,避免反序列化失败
        • 如果某个序列化类的成员变量是对象类型,则该对象类型的类必须实现序列化
        • 子类实现了Serializable,父类没有实现Serializable接口的话,父类不会被序列化。
        • 下面是摘自 jdk api 文档里面关于接口 Serializable 的描述
        • 关于 serialVersionUID 的描述
      • JSON序列化和JDK序列化区别
      • 常见的序列化
        • JDK原生序列化(前面已讲过如何使用)
          • JDK的序列化过程
        • JSON序列化
        • Hessian
        • Protobuf
        • Protostuff
      • 序列化底层
        • Serializable底层
      • writeObject(Object)
      • 序列化注意事项
      • 为什么static静态变量和transient 修饰的字段是不会被序列化的
      • 序列化的底层是怎么实现的?
      • 序列化时,如何让某些成员不要序列化?
      • 在 Java 中,Serializable 和 Externalizable 有什么区别
      • 是否可以自定义序列化过程, 或者是否可以覆盖 Java 中的默认序列化过程?
      • 在 Java 序列化期间,哪些变量未序列化?
      • 动态代理是什么?有哪些应用?
      • 怎么实现动态代理?
      • 如何选择序列化方式
      • RPC使用过程中注意哪些问题
      • 对象序列化网络传输案例
        • 首先,要传输的Student类(实现Serializable接口):
        • Socket服务器类核心方法:
        • Socket客户端类:
      • Java使用序列化实现深克隆
        • 深拷贝的两种实现方式
        • 使用Serializable方式实现深拷贝
        • 使用clone方式(浅拷贝)
          • 调用clone方法的前提
          • clone方法的存在问题
        • 使用clone方式实现“深拷贝”
          • 覆写考试类`Exam.java`的`clone()`方法
          • 解析
          • 改写老师类`Teacher.java`
        • 使用泛型实现序列化深拷贝方法
      • 作者留言
      • 参考文献

什么是java序列化

序列化:把对象转换为字节序列的过程

反序列:把字节序列恢复为对象的过程

对象序列化机制(object serialization)是java语言内建的一种对象持久化方式,通过对象序列化,可以将对象的状态信息保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式转换成对象,对象的序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。

简单说:对象序列化成的字节序列会包含对象的类型信息、对象的数据等,说白了就是包含了描述这个对象的所有信息,能根据这些信息“复刻”出一个和原来一模一样的对象。

在代码运行的时候,我们可以看到很多的对象,可以是一个,也可以是一类对象的集合,很多的对象数据,这些数据中,有些信息我们想让他持久的保存起来,那么这个序列化,就是把内存里面的这些对象给变成一连串的字节描述的过程。

常见的就是变成文件

什么情况需要使用 Java 序列化

想把的内存中的对象状态保存到一个文件中或者数据库中时候;

想用套接字在网络上传送对象的时候;

想通过RMI(远程方法调用)传输对象的时候。

为什么要序列化

Java对象是运行在JVM的堆内存中的,如果JVM停止后,它的生命也就戛然而止。

从浅入深理解序列化和反序列化_第1张图片

如果想在JVM停止后,把这些对象保存到磁盘或者通过网络传输到另一远程机器,怎么办呢?磁盘这些硬件可不认识Java对象,它们只认识二进制这些机器语言,所以我们就要把这些对象转化为字节数组,这个过程就是序列化啦~

序列化和反序列化过程如下

从浅入深理解序列化和反序列化_第2张图片

因为在网络中传输的数据只能是二进制。

序列化就是将对象转换成二进制,反序列化就是讲二进制转化为对象的过程。

RPC 框架为什么需要序列化

因为网络传输的数据必须是二进制数据,所以在 RPC 调用中,对入参对象与返回值对象进行序列化与反序列化是一个必须的过程。

RPC 的通信流程

从浅入深理解序列化和反序列化_第3张图片

序列化用途

序列化使得对象可以脱离程序运行而独立存在,它主要有两种用途

序列化机制可以让对象地保存到硬盘上,减轻内存压力的同时,也起了持久化的作用

比如 Web服务器中的Session对象,当有 10+万用户并发访问的,就有可能出现10万个Session对象,内存可能消化不良,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

序列化机制让Java对象可以在网络传输

我们在使用Dubbo远程调用服务框架时,需要把传输的Java对象实现Serializable接口,即让Java对象序列化,因为这样才能让对象在网络上传输。

实现对象序列化需要做哪些工作

JAVA提供了API实现了对象的序列化和反序列化的功能,使用这些API时需要遵守如下约定:

被序列化的对象类型需要实现序列化接口,此接口是标志接口,没有声明任何的抽象方法,JAVA编译器识别这个接口,自动的为这个类添加序列化和反序列化方法。

为了保持序列化过程的稳定,建议在类中添加序列化版本号。

不想让字段放在硬盘上就加transient

Java序列化常用API

java.io.ObjectOutputStream
java.io.ObjectInputStream
java.io.Serializable
java.io.Externalizable

Serializable 接口

Serializable接口是一个标记接口,没有方法或字段。一旦实现了此接口,就标志该类的对象就是可序列化的。

public interface Serializable {
}

Externalizable 接口

Externalizable继承了Serializable接口,还定义了两个抽象方法:writeExternal()和readExternal(),如果开发人员使用Externalizable来实现序列化和反序列化,需要重写writeExternal()和readExternal()方法

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

java.io.ObjectOutputStream类

表示对象输出流,它的writeObject(Object obj)方法可以对指定obj对象参数进行序列化,再把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream

表示对象输入流,它的readObject()方法,从输入流中读取到字节序列,反序列化成为一个对象,最后将其返回。

如何实现对象的序列化

  1. 对于要序列化对象的类要去实现Serializable接口或者Externalizable接口
  2. JDK提供的ObjectOutputStream类的writeObject方法,实现序列化
  3. JDK提供的ObjectInputStream类的readObject方法,实现反序列化

Serializable 实现序列化

public class TestBean implements Serializable {
    private Integer id;
    private String name;
    private Date date;
    //省去getter和setter方法和toString
}

序列化写入文本,执行后可以在test.txt文件中看到序列化内容

public static void main(String[] args) {
    TestBean testBean = new TestBean();
    testBean.setDate(new Date());
    testBean.setId(1);
    testBean.setName("zll1");
    //使用ObjectOutputStream序列化testBean对象并将其序列化成的字节序列写入test.txt文件
    try (FileOutputStream fileOutputStream = new FileOutputStream("D:\\test.txt");
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);) {
        objectOutputStream.writeObject(testBean);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

反序列化:

public static void main(String[] args) {
    try (FileInputStream fileInputStream = new FileInputStream("D:\\test.txt");
         ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream)) {
        TestBean testBean = (TestBean) objectInputStream.readObject();
        System.out.println(testBean);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

输出结果

TestBean{id=1, name='zll1', date=Fri Nov 27 14:52:48 CST 2020}

注意

  1. 一个对象要进行序列化,如果该对象成员变量是引用类型的,那这个引用类型也一定要是可序列化的,否则会报错
  2. 同一个对象多次序列化成字节序列,这多个字节序列反序列化成的对象还是一个(使用==判断为true)(因为所有序列化保存的对象都会生成一个序列化编号,当再次序列化时回去检查此对象是否已经序列化了,如果是,那序列化只会输出上个序列化的编号)
  3. 如果序列化一个可变对象,序列化之后,修改对象属性值,再次序列化,只会保存上次序列化的编号(这是个坑注意下)
  4. 对于不想序列化的字段可以再字段类型之前加上transient关键字修饰(反序列化时会被赋予默认值)

实现Externalizable接口

实现Externalizable接口必须重写连个方法

  • writeExternal(ObjectOutput out)
  • readExternal(ObjectInput in)
writeObject 和 readObject 自定义序列化策略,代码示例
public class TextBean implements Externalizable {

    private Integer id;
    private String name;
    private Date date;
   
    //可以自定义决定那些需要序列化
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(id);
        out.writeObject(name);
        out.writeObject(date);
    }
    //可以自定义决定那些需要反序列化
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.id = in.readInt();
        this.name = (String) in.readObject();
        this.date = (Date) in.readObject();
    }
    //省去getter和setter方法和toString
}

序列化:

public static void main(String[] args) {
    TextBean textBean = new TextBean();
    textBean.setDate(new Date());
    textBean.setId(1);
    textBean.setName("zll1");

    try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("D:\\externalizable.txt"))) {
        outputStream.writeObject(textBean);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

反序列化:

public static void main(String[] args) {
    try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\externalizable.txt"))) {
        TextBean textBean = (TextBean) objectInputStream.readObject();
        System.out.println(textBean);
        //输出结果:TextBean{id=1, name='zll1', date=Fri Nov 27 16:49:17 CST 2020}
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

实现了 Serializable 接口是自动序列化的,实现 Externalizable 则需要手动序列化,通过 writeExternal 和 readExternal 方法手动进行

注意

  1. 序列化对象要提供无参构造
  2. 如果序列化时一个字段没有序列化,那反序列化是要注意别给为序列化的字段反序列化了

实现序列化代码还可参考该博客说明,关于 serialVersionUID序列化中有为什么要加一串序列ID,下方博客有写,为了解决序列化和反序列化的算法一致性,一般1L就可以。注意 静态static的属性,他不序列化

(47条消息) 什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决。_老周聊架构的博客-CSDN博客

什么是 transient?

被 transient 修饰的变量不能被序列化。

1)transient修饰的变量不能被序列化;

2)transient只作用于实现 Serializable 接口;

3)transient只能用来修饰普通成员变量字段;

4)不管有没有 transient 修饰,静态变量都不能被序列化;

什么是serialVersionUID

serialVersionUID 表面意思就是序列化版本号ID,其实每一个实现Serializable接口的类,都有一个表示序列化版本标识符的静态变量,或者默认等于1L,或者等于对象的哈希码。

serialVersionUID的作用

先讲述下序列化的过程:在进行序列化时,(JAVA的话,是JVM)会把当前类的serialVersionUID写入到字节序列中(也会写入序列化的文件中),在反序列化时会将字节流中的serialVersionUID同本地对象中的serialVersionUID进行对比,一直的话进行反序列化,不一致则失败报错(报InvalidCastException异常)

故,JAVA序列化的机制是通过判断类的serialVersionUID来验证版本是否一致的

阿里开发手册,强制要求序列化类新增属性时,不能修改serialVersionUID字段

img

serialVersionUID的生成有哪三种方式

private static final long serialVersionUID= XXXL :

  1. 显式声明:默认的1L
  2. 显式声明:根据包名、类名、继承关系、非私有的方法和属性以及参数、返回值等诸多因素计算出的64位的hash值
  3. 隐式声明:未显式的声明serialVersionUID时java序列化机制会根据Class自动生成一个serialVersionUID(最好不要这样,因为如果Class发生变化,自动生成的serialVersionUID可能会随之发生变化,导致匹配不上)

序列化类增加属性时,最好不要修改serialVersionUID,避免反序列化失败

IDEA中新建Class可以在类名上按alt+enter:

从浅入深理解序列化和反序列化_第4张图片

如果不显示上图提示,可以按照下面步骤设置:

从浅入深理解序列化和反序列化_第5张图片

注意

不同的serialVersionUID的值,会影响到反序列化,也就是数据的读取,你写1L,注意L大些。计算机是不区分大小写的,但是,作为观众的我们,是要区分1和L的l,所以说,这个值,闲的没事不要乱动,不然一个版本升级,旧数据就不兼容了,你还不知道问题在哪

如果某个序列化类的成员变量是对象类型,则该对象类型的类必须实现序列化

当属性是对象的时候,没实现序列化接口,会产生异常Exception in thread “main” java.io.NotSerializableException: com....

代码示例

给Student类添加一个Teacher类型的成员变量,其中Teacher是没有实现序列化接口的

public class Student implements Serializable {
    private Integer age;
    private String name;
    private Teacher teacher;
    ...
}

//Teacher 没有实现
public class Teacher  {
	......
}

序列化运行,就报NotSerializableException异常啦

Exception in thread "main" java.io.NotSerializableException: com.example.demo.Teacher
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at com.example.demo.Test.main(Test.java:16)

其实这个可以在上小节的底层源码分析找到答案,一个对象序列化过程,会循环调用它的Object类型字段,递归调用序列化的,也就是说,序列化Student类的时候,会对Teacher类进行序列化,但是对Teacher没有实现序列化接口,因此抛出NotSerializableException异常。所以如果某个实例化类的成员变量是对象类型,则该对象类型的类必须实现序列化

从浅入深理解序列化和反序列化_第6张图片

子类实现了Serializable,父类没有实现Serializable接口的话,父类不会被序列化。

子类Student实现了Serializable接口,父类User没有实现Serializable接口

//父类实现了Serializable接口
public class Student  extends User implements Serializable {
    private Integer age;
    private String name;
}

//父类没有实现Serializable接口
public class User {
    String userId;
}

Student student = new Student();
student.setAge(25);
student.setName("jayWei");
student.setUserId("1");

ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\text.out"));
objectOutputStream.writeObject(student);
objectOutputStream.flush();
objectOutputStream.close();

//反序列化结果
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
Student student1 = (Student) objectInputStream.readObject();
System.out.println(student1.getUserId());
//output
/**  * null */

从反序列化结果,可以发现,父类属性值丢失了。因此子类实现了Serializable接口,父类没有实现Serializable接口的话,父类不会被序列化。

下面是摘自 jdk api 文档里面关于接口 Serializable 的描述

类通过实现 java.io.Serializable 接口以启用其序列化功能。
未实现此接口的类将无法使其任何状态序列化或反序列化。
可序列化类的所有子类型本身都是可序列化的。因为实现接口也是间接的等同于继承。
序列化接口没有方法或字段,仅用于标识可序列化的语义。

关于 serialVersionUID 的描述

序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。

如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。

可序列化类可以通过声明名为 “serialVersionUID” 的字段(该字段必须是静态 (static)、最终 (final) 的 long 型字段)显式声明其自己的 serialVersionUID:

如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java™ 对象序列化规范”中所述。

为什么要显示声明serialVersionUID 值

计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。

因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值

还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 – serialVersionUID 字段作为继承成员没有用处。

数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。

JSON序列化和JDK序列化区别

对于对象转化成json字符串和json字符串转化成对象,也是属于序列化和反序列化的范畴,相对于JDK提供的序列化机制,各有各的优缺点:

  • JDK序列化/反序列化:原生方法不依赖其他类库、但是不能跨平台使用、字节数较大
  • json序列化/反序列化:json字符串可读性高、可跨平台使用无语言限制、扩展性好、但是需要第三方类库、字节数较大

想了解json的使用可以看这里(https://mp.weixin.qq.com/s/S4R21FXSUPzpwBUv3uE6Xg)

常见的序列化

JDK原生序列化(前面已讲过如何使用)

序列化的实现是由ObjectOutputStream完成,反序列化由ObjectInputStream完成。

JDK的序列化过程

从浅入深理解序列化和反序列化_第7张图片

序列化过程就是在读取对象数据的时候,不断的加入一些特殊分隔符,这些特殊分隔符在反序列化中使用。

头部数据用来声明序列化协议、版本,用于高版本向后兼容
对象数据主要包括类名、签名、属性名、属性类型及属性值、开头结尾数据
存在对象引用、继承的情况下,递归遍历写对象逻辑
任何一种序列化框架核心思想就是设计一个序列化协议,将对象的类型、属性类型、属性值按照固定的格式写到二进制字节流中来完成序列化,再按照固定格式一一读取对象的类型、属性类型、属性值,通过这些信息重建对象,完成反序列化

JSON序列化

JSON是典型的key-value形式,没有数据类型,是一种文本型序列化框架
基于HTTP的RPC通信框架会采用JSON格式,存在以下问题

JSON序列化的额外空间开销比较大,对于大数据量服务这意味着巨大的内存和磁盘开销,所以选择JSON的时候,数据量要小。
JSON没有类型,像Java这种强类型语言,需要反射统一解决,性能不太好

Hessian

Hession是动态类型、二进制、紧凑的、可跨语言移植的一种序列化框架。

Hessian协议要比JDK、JSON更加紧凑,性能要比JDK、JSON序列化高效很多,而且生成的字节数也更小。

Student student = new Student();
student.setNo(101);
student.setName("HESSIAN");
//把student对象转化为byte数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(bos);
output.writeObject(student);
output.flushBuffer();
byte[] data = bos.toByteArray();
bos.close();

 //把刚才序列化出来的byte数组转化为student对象
 ByteArrayInputStream bis = new ByteArrayInputStream(data);
 Hessian2Input input = new Hessian2Input(bis);
 Student deStudent = (Student) input.readObject();
 input.close();

 System.out.println(deStudent);

存在问题:主要集中在对Java一些常见类型不支持

  • linked系列:LinkedHashMap、LinkedHashSet不支持,可以通过CollectionDesric
  • Locale类:通过扩展ContextSerializerFactory类修复
  • Byte/Short反序列化为Integer

Protobuf

Google内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。

Protobuf 使用的时候需要定义 IDLProtobuf使用的时候需要定义IDL文件,使用不用语言的IDL编译器,生成序列化工具类。

序列化体积比JSON、Hessian要小
IDL能清晰的描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器;
序列化、反序列化速度很快,不需要反射获取类型
消息格式升级和兼容性不错,可以做到向后兼容。

// IDl 文件格式
synax = "proto3";
option java_package = "com.test";
option java_outer_classname = "StudentProtobuf";
message StudentMsg {
	int32no=1;
 	//姓名
 	string name = 2;
}



StudentProtobuf.StudentMsg.Builder builder = StudentProtobuf.StudentMsg.newBuilder();
builder.setNo(103);
builder.setName("protobuf");

//把student对象转化为byte数组
StudentProtobuf.StudentMsg msg = builder.build(); 
byte[] data = msg.toByteArray();

//把刚才序列化出来的byte数组转化为student对象
StudentProtobuf.StudentMsg deStudent = StudentProtobuf.StudentMsg.parseFrom(dat
System.out.println(deStudent);

Protobuf 非常高效,但是对于具有反射和动态能力的语言来说,这样用起来很费劲,这一点就不如 Hessian,比如用 Java 的话,这个预编译过程不是必须的,可以考虑使用 Protostuff。

Protostuff

Protostuff 不需要依赖 IDL 文件,可以直接对 Java 领域对象进行反 / 序列化操作,在效率上跟 Protobuf 差不多,生成的二进制格式和 Protobuf 是完全相同的,可以说是一个 Java 版本的 Protobuf 序列化框架。但是

不支持null
不支持淡村的Map、List集合对象,需要包在对象里。

序列化底层

Serializable底层

Serializable接口,只是一个空的接口,没有方法或字段,为什么这么神奇,实现了它就可以让对象序列化了?

public interface Serializable {
}

为了验证Serializable的作用,把实现序列化的对象,去掉实现Serializable接口,看序列化过程怎样吧~

public class Student /**implements Serializable*/ {
    private Integer age;
    private String name;
    public Integer getAge() {
        return age;
    }    public void setAge(Integer age) {
        this.age = age;
    }    public String getName() {
        return name;
    }    public void setName(String name) {
        this.name = name;
    }
}

序列化过程中抛出异常啦,堆栈信息如下:

Exception in thread "main" java.io.NotSerializableException: com.example.demo.Student
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at com.example.demo.Test.main(Test.java:13)

顺着堆栈信息看一下,原来有重大发现,如下~

从浅入深理解序列化和反序列化_第8张图片

**原来底层是这样:**ObjectOutputStream 在序列化的时候,会判断被序列化的Object是哪一种类型,String?array?enum?还是 Serializable,如果都不是的话,抛出 NotSerializableException异常。所以呀,Serializable真的只是一个标志,一个序列化标志~

writeObject(Object)

序列化的方法就是writeObject,基于以上的demo,我们来分析一波它的核心方法调用链吧~(建议大家也去debug看一下这个方法,感兴趣的话)

从浅入深理解序列化和反序列化_第9张图片

writeObject直接调用的就是writeObject0()方法,

public final void writeObject(Object obj) throws IOException {
	.....
    writeObject0(obj, false);
    .....
}

writeObject0 主要实现是对象的不同类型,调用不同的方法写入序列化数据,这里面如果对象实现了Serializable接口,就调用writeOrdinaryObject()方法~

private void writeObject0(Object obj, boolean unshared)
        throws IOException    {
    //String类型
    if (obj instanceof String) {
        //数组类型
        writeString((String) obj, unshared);
    } else if (cl.isArray()) {
        //枚举类型
        writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) {
        //Serializable实现序列化接口
        writeEnum((Enum<?>) obj, desc, unshared);
    } else if (obj instanceof Serializable) {
        writeOrdinaryObject(obj, desc, unshared);    } else{
        //其他情况会抛异常~
        if (extendedDebugInfo) {
            throw new NotSerializableException(
                cl.getName() + "\n" + debugInfoStack.toString());
        } else {
            throw new NotSerializableException(cl.getName());
        }    
    }
    .....
}       

writeOrdinaryObject()会先调用writeClassDesc(desc),写入该类的生成信息,然后调用writeSerialData方法,写入序列化数据

private void writeOrdinaryObject(Object obj,
                                 ObjectStreamClass desc,
                                 boolean unshared) throws IOException {
    .....
    // 调用ObjectStreamClass的写入方法
	writeClassDesc(desc, false);
    // 判断是否实现了Externalizable接口
    if (desc.isExternalizable() && !desc.isProxy()) {
        writeExternalData((Externalizable) obj);
    } else {
        //写入序列化数据
        writeSerialData(obj, desc);
    }
    .....
}

writeSerialData()实现的就是写入被序列化对象的字段数据

private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException {
    for (int i = 0; i < slots.length; i++) {
        if (slotDesc.hasWriteObjectMethod()) {
            //如果被序列化的对象自定义实现了writeObject()方法,则执行这个代码块
            slotDesc.invokeWriteObject(obj, this);
        } else {
            // 调用默认的方法写入实例数据
            defaultWriteFields(obj, slotDesc);
        }
    }
}

defaultWriteFields()方法,获取类的基本数据类型数据,直接写入底层字节容器;

获取类的obj类型数据,循环递归调用writeObject0()方法,写入数据~

private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException {
    // 获取类的基本数据类型数据,保存到primVals字节数组
    desc.getPrimFieldValues(obj, primVals);
    //primVals的基本类型数据写到底层字节容器
    bout.write(primVals, 0, primDataSize, false);
    // 获取对应类的所有字段对象
    ObjectStreamField[] fields = desc.getFields(false);
    Object[] objVals = new Object[desc.getNumObjFields()];
    int numPrimFields = fields.length - objVals.length;
    // 获取类的obj类型数据,保存到objVals字节数组
    desc.getObjFieldValues(obj, objVals);
    //对所有Object类型的字段,循环
    for (int i = 0; i < objVals.length; i++) {
        //递归调用writeObject0()方法,写入对应的数据
        writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());
        ......
    }
}

序列化注意事项

  • static静态变量和transient 修饰的字段是不会被序列化的
  • serialVersionUID问题
  • 如果某个序列化类的成员变量是对象类型,则该对象类型的类必须实现序列化
  • 子类实现了序列化,父类没有实现序列化,父类中的字段丢失问题

为什么static静态变量和transient 修饰的字段是不会被序列化的

static静态变量和transient 修饰的字段是不会被序列化的,我们来看例子分析一波~ Student类加了一个类变量gender和一个transient修饰的字段specialty

public class Student implements Serializable {
    private Integer age;
    private String name;
    public static String gender = "男";
    transient  String specialty = "计算机专业";
    public String getSpecialty() {
        return specialty;
    }
    public void setSpecialty(String specialty) {
        this.specialty = specialty;
    }
    
    @Override
    public String toString() {
        return "Student{" +"age=" + age + ", name='" + name + '\'' + ", gender='" + gender + '\'' + ", specialty='" + specialty + '\'' + '}';
    }
    ......
}

打印学生对象,序列化到文件,接着修改静态变量的值,再反序列化,输出反序列化后的对象~

从浅入深理解序列化和反序列化_第10张图片

运行结果:

序列化前Student{age=25, name='jayWei', gender='男', specialty='计算机专业'}
序列化后Student{age=25, name='jayWei', gender='女', specialty='null'}

对比结果可以发现:

  • 1)序列化前的静态变量性别明明是‘男’,序列化后再在程序中修改,反序列化后却变成‘女’了,what?显然这个静态属性并没有进行序列化。其实,静态(static)成员变量是属于类级别的,而序列化是针对对象的~所以不能序列化哦
  • 2)经过序列化和反序列化过程后,specialty字段变量值由’计算机专业’变为空了,为什么呢?其实是因为transient关键字,它可以阻止修饰的字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如int型的值会被设置为 0,对象型初始值会被设置为null。

即:序列化并不保存静态变量,Transient 关键字阻止该变量被序列化到文件中

序列化的底层是怎么实现的?

上文中已描述底层实现,回答Serializable关键字作用,序列化标志啦,源码中,它的作用啦还有,可以回答writeObject几个核心方法,如直接写入基本类型,获取obj类型数据,循环递归写入

序列化时,如何让某些成员不要序列化?

可以用transient关键字修饰,它可以阻止修饰的字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如int型的值会被设置为 0,对象型初始值会被设置为null。

在 Java 中,Serializable 和 Externalizable 有什么区别

Externalizable继承了Serializable,给我们提供 writeExternal() 和 readExternal() 方法, 让我们可以控制 Java的序列化机制, 不依赖于Java的默认序列化。正确实现 Externalizable 接口可以显著提高应用程序的性能。

是否可以自定义序列化过程, 或者是否可以覆盖 Java 中的默认序列化过程?

可以的。我们都知道,对于序列化一个对象需调用 ObjectOutputStream.writeObject(saveThisObject), 并用 ObjectInputStream.readObject() 读取对象, 但 Java 虚拟机为你提供的还有一件事, 是定义这两个方法。如果在类中定义这两种方法, 则 JVM 将调用这两种方法, 而不是应用默认序列化机制。同时,可以声明这些方法为私有方法,以避免被继承、重写或重载。

在 Java 序列化期间,哪些变量未序列化?

static静态变量和transient 修饰的字段是不会被序列化的。

静态(static)成员变量是属于类级别的,而序列化是针对对象的。

transient关键字修字段饰,可以阻止该字段被序列化到文件中。

动态代理是什么?有哪些应用?

动态代理是运行时动态生成代理类。

动态代理的应用有 Spring AOP数据查询、测试框架的后端 mock、rpc,Java注解对象获取???

怎么实现动态代理?

JDK 原生动态代理和 cglib 动态代理。

JDK 原生动态代理是基于接口实现的,而 cglib 是基于继承当前类的子类实现的。

11、静态编译和动态编译

**静态编译:**在编译时确定类型,绑定对象

**动态编译:**运行时确定类型,绑定对象

如何选择序列化方式

  • RPC序列化框架的性能和效率
  • 空间开销:序列化之后的二进制数据的体积大小
  • 序列化协议的通用性和兼容性:多个语言,多个版本
  • 序列化协议的安全性:JDK 原生序列化存在安全漏洞

从浅入深理解序列化和反序列化_第11张图片

首选的还是 Hessian 与 Protobuf,因为他们在性能、时间开销、空间开销、通用性、 兼容性和安全性上,都满足了我们的要求。

Hessian 在使用上更加方便,在对象的兼 容性上更好;
Protobuf 则更加高效,通用性上更有优势。

RPC使用过程中注意哪些问题

对象要尽量简单,没有太多的依赖关系
入参和返回值体积不要太大
尽量使用简单的、常用的原生对象。
对象不要有复杂的继承关系。

对象序列化网络传输案例

来自于博文:(47条消息) JAVA 通过网络传输对象(对象序列化)简单示例_wjwisme的博客-CSDN博客

首先,要传输的Student类(实现Serializable接口):

public class Student implements Serializable{
    private static final long serialVersionUID = 8683452581334592189L;
    private String name;
    private int age;
    private int score;
    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;
	}
	public int getScore() {
		return score;
	}
	public void setScore(int score) {
		this.score = score;
	}
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return "name:" + name + " age:" + age + " score:" + score;
	}
}

Socket服务器类核心方法:

public static void openObjectServer(){
	ServerSocket ss = null;
	try {
		ss = new ServerSocket(1111);
		while(true){
            final Socket socket = ss.accept();
            new Runnable(){
                public void run() {
                    try { 
                        InputStream is = socket.getInputStream();
                        OutputStream os = socket.getOutputStream();
                        os.write("欢迎连接 服务器 一号!".getBytes());
				
                        ObjectInputStream ois = new ObjectInputStream(is);
                        Object object = ois.readObject();
                        
                        //打印对象
                        System.out.println(object);	
                        
                        //关闭socket
                        socket.close();
                    
                    }catch(Exception e){
                        e.printStackTrace();
                    
                    }finally{
                        if(socket != null )
						try {
							socket.close();
						} catch (IOException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
                    }
                }
            }.run();
		}
	
    } catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	
    }finally{
		System.out.println("服务器关闭连接!");
		try {
			if(ss != null)
			ss.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

说明:该服务器方法可以接受多个客户端连接,一般网络服务器编程核心思想应该都是这样,开启后,支持多线程处理多个连接请求,互不影响。

Socket客户端类:

public class ClientSocketClass {
    public static void main(String[] args){
        Socket socket = null;
        try{
			socket = new Socket(InetAddress.getByName("127.0.0.1"),1111);
			OutputStream os = socket.getOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(os);
	
			Student student = new Student();
			student.setAge(20);
			student.setName("wjw");
			student.setScore(100);
	
			oos.writeObject(student);
		}catch(Exception e){
			e.printStackTrace();
		}finally{
            try {
				if(socket != null)
				socket.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

Java使用序列化实现深克隆

深拷贝的两种实现方式

  1. 实现Cloneable接口,重写Object类中clone()方法,实现层层克隆的方法。(clone()方法要求目标类及其成员变量类都需要实现java.lang.Cloneable接口,并且覆写java.lang.Objectclone()方法。
  2. 通过序列化(Serializable)的方法,将对象写到流里,然后再从流中读取出来。虽然这种方法效率很低,但是这种方法才是真正意义上的深度克隆。(序列化方法通过静态方法实现,其目标类及其成员变量类都需要实现java.lang.Serializable接口

性能对比:序列化克隆和Cloneable,Cloneable更快

Java实现深克隆的两种方式 - luankun0214 - 博客园 (cnblogs.com)

由于序列化的方式实现深度克隆性能较差,还是推荐使用Cloneable接口的方式重写clone方法,但是注意该方法需要注意克隆对象的对象(多层)。

使用Serializable方式实现深拷贝

需要被克隆的对象实现Serializable接口,克隆的过程相对比较通用

被克隆对象:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SerialCar implements Serializable {
	private static final long serialVersionUID = -7308342867043888945L;
	private String carType;
	private String carName;
 
    @Override
    public String toString() {
        return "SerialCar{" +
                "carType='" + carType + '\'' +
                ", carName='" + carName + '\'' +
                '}';
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SerialUser implements Serializable {
	private static final long serialVersionUID = -2167957013938386204L;
	private String username;
	private SerialCar serialCar;
    
    @Override
    public String toString() {
        return "SerialUser{" +
                "username='" + username + '\'' +
                ", serialCar=" + serialCar +
                '}';
    }
}
import java.io.*;

public class TestDeepClone {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerialUser serialUser = new SerialUser("李四", new SerialCar("奔驰", "250"));
        //在内存中开辟一块缓冲区,将对象序列化成流
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
    	ObjectOutputStream oos = new ObjectOutputStream(bos);
    	oos.writeObject(serialUser);
        
		//找到这一块缓冲区,将字节流反序列化成另一个对象
    	ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    	ObjectInputStream ois = new ObjectInputStream(bis);
    	SerialUser cloneUser = (SerialUser)ois.readObject();

		SerialCar cloneCar = cloneUser.getSerialCar();
		cloneCar.setCarType("奥迪");
		cloneCar.setCarName("A8");
 
	    System.out.println(serialUser);
	    System.out.println(cloneUser);
	}
}
  1. 类需要实现java.lang.Serializable接口。否则代码在运行时报错。

解释:
对象类Exam需要实现java.lang.Serializable接口,否则会在代码执行到os.writeObject(exam)时抛出NotSerializableException异常。

  1. 父类中的成员变量子类也需要实现java.lang.Serializable接口。否则在运行时报错。

解释:
当父类中包含了成员变量子类时,如果只有父类实现java.lang.Serializable接口,但是子类没有实现java.lang.Serializable接口,那么代码执行到os.writeObject(exam)时还是会**抛出NotSerializableException异常。

使用clone方式(浅拷贝)

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Exam implements Cloneable {

    private int examId;
    private String examName;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

测试类

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Exam exam = new Exam(1, "语文考试");
        Exam cloneExam = (Exam) exam.clone();
        System.out.println(cloneExam != exam);
        System.out.println(cloneExam.equals(exam));
    }
}
控制台输出:
true
false

我们确实拷贝出了另一个对象。equals没有覆写,所以调用的是java.lang.Object中的以下方法:

public boolean equals(Object obj) {
	return (this == obj);
}
调用clone方法的前提
  1. Exam需要继承java.lang.Cloneable接口。否则代码在运行时报错。

解释:
调用exam.clone()的对象类Exam需要继承Cloneable接口,否则会在代码运行时抛出CloneNotSupportedException异常

  1. Exam需要覆写父类的clone()方法。否则代码在编译时报错。

解释:
因为clone()java.lang.Object中是protected访问控制。如果不覆写,exam.clone()这句代码无法编译通过

clone方法的存在问题

阅读java.lang.Object中的clone()方法上的英文注释时有这样一段话:

this method creates a new instance of the class of this object and initializes all its fields with exactly the contents of the corresponding fields of this object, as if by assignment; the contents of the fields are not themselves cloned. Thus, this method performs a “shallow copy” of this object, not a “deep copy” operation.

翻译为:
该方法创建该对象类的新实例,并使用该对象相应字段的内容完全初始化其所有字段,就像通过赋值一样; 字段的内容本身不会被克隆。 因此,此方法执行此对象的“浅复制”,而不是“深复制”操作。

代码示例

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Teacher {
    private String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Exam implements Cloneable {

    private int examId;
    private String examName;
    private Teacher teacher;

    @Override
    public String toString() {
        return "Exam{" +
                "examId=" + examId +
                ", examName='" + examName + '\'' +
                ", teacher=" + teacher +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

测试类

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Exam exam = new Exam(1, "语文考试");
        Teacher teacher = new Teacher("马老师");
        exam.setTeacher(teacher);
        Exam cloneExam = (Exam) exam.clone();
        System.out.println(cloneExam != exam);
        System.out.println(cloneExam.equals(exam));

        cloneExam.getTeacher().setName("Lily");
        System.out.println(exam.toString());
        System.out.println(cloneExam.toString());
    }
}
public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Exam exam = new Exam(1, "语文考试");
        Exam cloneExam = (Exam) exam.clone();
        System.out.println(cloneExam != exam);
        System.out.println(cloneExam.equals(exam));
    }
}
控制台输出:
true
false
Exam{examId=1, examName='语文考试', teacher=Teacher{name='Lily'}}
Exam{examId=1, examName='语文考试', teacher=Teacher{name='Lily'}}

原本只想将克隆出来的考试的监考老师改为 Lily ,但是把原考试对象的监考老师也修改了

使用clone方式实现“深拷贝”

覆写考试类Exam.javaclone()方法
@Override
protected Object clone() throws CloneNotSupportedException {
	Exam exam = (Exam) super.clone();
	if (teacher != null) {
		Teacher teacher = (Teacher) this.teacher.clone();
		exam.setTeacher(teacher);
	}
	return exam;
}
解析

用上述方法,取代return super.clone()的默认实现。同时因为这里调用了teacher.clone(),所以类Teacher也要实现Cloneable接口,覆写clone()方法。

改写老师类Teacher.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Teacher implements Cloneable{

    private String name;

    @Override
    public String toString() {
        return "Teacher{" +
                "name='" + name + '\'' +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

控制台输出:
true
false
Exam{examId=1, examName=‘语文考试’, teacher=Teacher{name=‘马老师’}}
Exam{examId=1, examName=‘语文考试’, teacher=Teacher{name=‘Lily’}}

使用泛型实现序列化深拷贝方法

public class Util {
    private Util() {}
    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T deepCopy(T obj) {
        T cloneObj = null;
        try {
            //写入字节流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();

            //分配内存,写入原始对象,生成新对象
            ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            
            //返回生成的新对象
            cloneObj = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

使用该方法可以在代码编译期检查出没有实现java.lang.Serializable接口的对象。

作者留言

平时也经常用到序列化,使用无非就是 implements Serializable ,又加了个UID而已.一直没有去深入理解其含义,为了把JAVA基础打牢,对这些知识点都做了深度学习,看了很多篇博文,存档在这只是为了个人学习笔记使用,可能还有些文献没有标进来,实在是太多了,记不住看过哪篇,还请见谅!!!

参考文献

(47条消息) 对象在网络中如何传输之序列化_序列化和传输方式有关吗_程序员面试那点事儿的博客-CSDN博客

(47条消息) 静态变量能被序列化吗?_静态变量可以序列化吗_码上得天下的博客-CSDN博客

一文搞懂序列化与反序列化 - 知乎 (zhihu.com)

全方位解析Java的序列化 - 知乎 (zhihu.com)

一步步分析Java深拷贝的两种方式-clone和序列化 - 极客子羽 - 博客园 (cnblogs.com)

你可能感兴趣的:(jvm,java,开发语言)