享学课堂作者:逐梦々少年

转载请声明出处!

现在开发过程中经常遇到多个进程多个服务间需要交互,或者不同语言的服务之间需要交互,这个时候,我们一般选择使用固定的协议,将数据传输过去,但是在很多语言,比如java等jvm语言中,传输的数据是特有的类对象,而类对象仅仅在当前jvm是有效的,传递给别的jvm或者传递给别的语言的时候,是无法直接识别类对象的,那么,我们需要多个服务之间交互或者不同语言交互,该怎么办?这个时候我们就需要通过固定的协议,传输固定的数据格式,而这个数据传输的协议称之为序列化,而定义了传输数据行为的框架组件也称之为序列化组件(框架)

序列化有什么意义

首先我们先看看,java中的序列化,在java语言中实例对象想要序列化传输,需要实现Serializable 接口,只有当前接口修饰定义的类对象才可以按照指定的方式传输对象。而传输的过程中,需要使用java.io.ObjectOutputStream 和java.io.ObjectInputStream 来实现对象的序列化和数据写入,接着我们看一个最基础的序列化:

我们创建一个java实体类:

public class User {

private Integer id;

private String name;

private Byte sex;

private Integer age;

public Integer getId() {

return id;

}

public void setId(Integer id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Byte getSex() {

return sex;

}

public void setSex(Byte sex) {

this.sex = sex;

}

public Integer getAge() {

return age;

}

public void setAge(Integer age) {

this.age = age;

}

@Override

public String toString() {

return "User [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";

}

}

然后我们编写发送对象(序列化)的实现:

public class OutPutMain

{

public static void main( String[] args ) throws UnknownHostException, IOException

{

Socket socket = new Socket("localhost",8080);

try(ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())){

User user = new User().setAge(10).setId(10).setName("张三").setSex((byte)0);

outputStream.writeObject(user);

outputStream.flush();

System.out.println("对象已经发送:--->"+user);

}catch (Exception e) {

e.getStackTrace();

System.err.println("对象发送失败:--->");

}finally{

if(!socket.isClosed()){

socket.close();

}

}

}

}

然后定义读取实体(反序列化)的代码:

public class InputMain {

public static void main(String[] args) throws IOException {

ServerSocket serverSocket = new ServerSocket(8080);

Socket socket = serverSocket.accept();

try(ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())){

User user = (User) inputStream.readObject();

System.out.println(user);

}catch (Exception e) {

e.getStackTrace();

}finally {

if(!serverSocket.isClosed()){

serverSocket.close();

}

}

}

}

接着我们先运行InputMain,再运行OutPutMain,看下结果:

java.io.NotSerializableException: demo.ser.User

at java.io.ObjectOutputStream.writeObject0(Unknown Source)

at java.io.ObjectOutputStream.writeObject(Unknown Source)

at demo.ser.OutPutMain.main(OutPutMain.java:15)

很明显报错了,告诉我们user类不能序列化,原因很明显,我们的User类没有实现Serializable接口,现在我们修改如下:

public class User implements Serializable{

然后我们再按照顺序执行一次后,就能看到打印的结果了:

对象已经发送:--->User [id=10, name=张三, sex=0, age=10]

serialVersionUID的认知

上面我们学习了一个最基础的序列化传递的方法,但是我们仔细观察代码,发现编译器在class申明那里报了一个黄色的波浪线,这个是为什么呢?原来jdk推荐我们实现序列化接口后,让我们再去生成一个固定的序列化id--serialVerionUID,而这个id的作用是用来作为传输/读取双端进程的版本是否一致的,防止我们因为版本不一致导致的序列化失败,那么serialVerionUID取值应该如何取值?又或者serialVerionUID不一致的时候,是不是序列化会失败呢?接下来我们来看看serialVerionUID的取值方案:

可以看到编译器推荐我们有两种方式,一种是生成默认的versionID,这个值为1L,还有一种方式是根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段,只要我们类名、方法名、变量有修改,或者有空格、注释、换行等操作,计算出来的哈希字段都会不同,当然这里需要注意,每次我们有以上的操作的时候尽量都要重新生成一次serialVerionUID(编译器并不会给你自动修改)。接下来我们来看下一个问题,如果我们修改了serialVerionUID,而另一个的serialVerionUID还是原来的,我们能否序列化,是否会有影响呢?我们把上述的案例修改下:

OutPutMain对应的User类的serialVerionUID修改为2L:

public class User implements Serializable{

private static final long serialVersionUID = 2L;

........

而InputMain对应的User还是使用的默认值:

public class User implements Serializable{

private static final long serialVersionUID = 1L;

........

再次运行一下,果不其然,抛出了InvalidClassException,告诉我们序列化id不一样,导致传输失败:

java.io.InvalidClassException: demo.ser.User; local class incompatible: stream classdesc serialVersionUID = 2, local class serialVersionUID = 1

at java.io.ObjectStreamClass.initNonProxy(Unknown Source)

at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)

at java.io.ObjectInputStream.readClassDesc(Unknown Source)

at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)

at java.io.ObjectInputStream.readObject0(Unknown Source)

at java.io.ObjectInputStream.readObject(Unknown Source)

at demo.ser.InputMain.main(InputMain.java:13)

serialVersionUID两种方式的区别及选择

那么又有个问题出现了,既然这个serialVersionUID如此重要,那么编译器推荐我们两种方法,我们到底该如何选择,这两种区别又在哪?上面我们也知道两种序列化UID一个是固定的1L默认值,一个是按照类方法属性等计算出来的hash,只要有代码的修改,重新计算出来的结果就会改变,所以两个id一个是固定的,除非手动修改,另外一个可以认为每次修改完都会变化(其实是需要我们重新生成),根据这个特性,我们可以分别用在不同的场景下,比如,我们的一些dto与业务并无太大关系,很长时间甚至整个项目周期中,都是固定不会进行改变或者很少改变的dto,这里的dto建议使用默认值方式,同样也防止因为误操作等方式导致uid改变造成序列化失败(比如不小心修改了顺序等,如果是第二种方式,重新生成的话,就会改变),也可以在基础库或者基础jar中定义的dto使用固定UID方式,保证dto的稳定,而在业务线开发过程中,我习惯动态生成UID,尤其是频繁修改的dto中,更是需要如此,防止在开发阶段一些未知的序列化问题或者未知问题没有被检测出来,而serialVersionUID的作用就是在序列化的时候,判断两个dto是否一致,也是jdk实现的接口规则,防止序列化不一致导致问题,除此之外并无其他区别

Transient关键字

到现在我们已经知道了序列化的大概使用方式,但是这个时候我们遇到一个需求,一个dto在使用的时候需要有这个字段完成业务流程,但是序列化的时候我们不需要这个字段,该如何呢?这个时候就需要Transient关键字了,这个是java针对序列化出的关键字,修饰在指定字段上,可以在序列化的时候,排除当前关键字修饰的字段,仅序列化其他字段,当我们反序列化的时候,可以看到基础类型为默认值,引用类型则为null,代码如下:

修改outPutMain工程的User类:

public class User implements Serializable{

private static final long serialVersionUID = 2L;

//不序列化id字段

private transient Integer id;

private String name;

private Byte sex;

private Integer age;

......

再次进行序列化后可以看到序列化的结果如下:

对象已经发送:--->User [id=10, name=张三, sex=0, age=10]

但是反序列化的结果如下:

User [id=null, name=张三, sex=0, age=10]

可以看到,当前的id字段果然没有任何结果,但是这个时候我们不禁怀疑,如果这个dto刚好没有id字段,其他完全一样,并且故意把serialVersionUID也设置为一样的,我们序列化会有问题吗?接着我们把IntputMain工程的User类的id字段移除,再来看下运行结果:

序列化的结果和上面一样:

对象已经发送:--->User [id=10, name=张三, sex=0, age=10]

但是反序列化的结果居然没有出现序列化异常,而且成功的完成了反序列化操作:

User [name=张三, sex=0, age=10]

怎么会这样呢?原来transient关键字会把所有属性都序列化到IO(内存、硬盘)等,但是有了当前关键字修饰的属性并不会包含在序列化中,所以当序列化完成后,已经丢失了transient修饰的属性信息,而在反序列化的时候,是按照序列化的结果来反向给属性赋值,所以我们反序列化的属性存在多余的或者仅和序列化结果一致,缺少几个属性也是可以的,所以我们通过以上的案例我们可以总结以下三点:

1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问

2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口

3)java的序列化机制是向上兼容的,也就是说,可以包含或者超过序列化的属性,但是当反序列化的时候缺少属性,序列化就会失败

而序列化的时候还需要注意一点,序列化不是万能的,除了transient关键字外,如果某个属性存在static关键字修饰,那么无论是否有transient修饰,都不能参与序列化

可能有人会比较疑惑,如果我们给id属性使用static修饰,并且初始化的时候设置了值,但是序列化完成后我们依然收到了之前设置的值,这不是和上面的描述矛盾吗?其实不然,我们都知道static在jvm加载的过程中会有唯一一份初始化的结果,而我们拿到的所谓序列化的值,是因为jvm初始化的值,而不是序列化带来的值,接着我们修改上面的案例来检测下:

将两个工程中得User类修改如下:

public class User implements Serializable{

private static final long serialVersionUID = 1L;

private Integer id;

public static String name;

private Byte sex;

private Integer age;

........

然后修改反序列化(InputMain)工程的main代码:

public static void main(String[] args) throws IOException {

ServerSocket serverSocket = new ServerSocket(8080);

Socket socket = serverSocket.accept();

try(ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())){

//在反序列化之前设置一个值

User.name = "李四";

//进行反序列化

User user = (User) inputStream.readObject();

System.out.println(user);

}catch (Exception e) {

e.printStackTrace();

}finally {

if(!serverSocket.isClosed()){

serverSocket.close();

}

}

}

可以看到这里我们给值修改为李四,如果结论正确,那么结果应该为李四而不是初始化传递的张三,现在我们看下序列化的对象:

对象已经发送:--->User [id=10, name=张三, sex=0, age=10]

再来看反序列化的结果:

User [id=10, name=李四, sex=0, age=10]

果然是按照静态加载的结果来的,而不是序列化,从而确定结论是正确的

Externalizable 自定义序列化

如果这个时候有人会说,transient关键字不够灵活啊,如果我需要动态的指定哪些可以序列化哪些不能序列化,该怎么办?这个时候我们不妨考虑Externalizable 接口,这个接口是Serializable接口的子接口,使用当前接口的时候必须存在无参构造,接口定义如下:

public interface Externalizable extends Serializable {

public void writeExternal(ObjectOutput out) throws IOException ;

public void readExternal(ObjectInput in) throws IOException,ClassNot FoundException ;

}

可以看到我们实现当我们实现当前接口的时候,必须要重写writeExternal和readExternal两个方法,而当前的两个方法作用则是自定义序列化和反序列化的操作,接着我们通过自定义的序列化实现id不被序列化的操作:

public class NUser implements Externalizable {

private Integer id;

private String name;

private Byte sex;

private Integer age;

public Integer getId() {

return id;

}

public void setId(Integer id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Byte getSex() {

return sex;

}

public void setSex(Byte sex) {

this.sex = sex;

}

public Integer getAge() {

return age;

}

public void setAge(Integer age) {

this.age = age;

}

//反序列化的时候调用--自定义反序列化

@Override

public void readExternal(ObjectInput input) throws IOException, ClassNotFoundException {

//按照序列化的顺序获取反序列化的字段

this.name = input.readObject().toString();

this.sex = input.readByte();

this.age = input.readInt();

}

//序列化的时候调用--自定义序列化

@Override

public void writeExternal(ObjectOutput output) throws IOException {

output.writeObject(this.name);

output.writeByte(this.sex);

output.writeInt(this.age);

}

@Override

public String toString() {

return "NUser [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";

}

}

从上面可以看出来,实现了Externalizable接口以后,编译器不能自动实现serialVersionUID,需要我们给OutPutMain和InputMain工程手动添加如下代码:

private static final long serialVersionUID = 1L;

因为当前完全属于自定义的序列化,系统不再提供默认的方式和自动计算的hash方式,而是完全由我们决定是否创建serialVersionUID以及对应的版本,接着将OutPutMain工程下的代码修改:

//User user = new User().setAge(10).setId(10).setName("张三").setSex((byte)0);

NUser user = new NUser().setAge(10).setId(10).setName("张三").setSex((byte)0);

InputMain工程的代码修改为:

//User user = (User) inputStream.readObject();

NUser user = (NUser) inputStream.readObject();

接着序列化的结果如下:

对象已经发送:--->NUser [id=10, name=张三, sex=0, age=10]

反序列化的结果为:

NUser [id=null, name=张三, sex=0, age=10]

可以看到完全按照我们的序列化方式来操作了,这样就可以实现灵活的序列化/反序列化代码了

writeObject 和 readObject

通过Externalizable接口我们可以实现自定义的序列化和反序列化,但是我们可以看到这两个操作需要依赖readExternal和writeExternal方法实现,而这两个方法内部是依赖了ObjectInput和ObjectOutput实现的自定义,这个时候我们不禁疑问,难道序列化机制和IO流有关系?ObjectInput接口我们知道,内部定义了很多read相关的方法,最常见的实现类为ObjectInputStream,而ObjectOutput内部定义了很多write相关的方法,常见的实现类为ObjectInputStream,那么我们可以大胆猜测是因为writeObject和readObject方法实现的,现在我们修改两个工程中的NUser类如下:

public class NUser implements Serializable{

private static final long serialVersionUID = 1L;

private Integer id;

private String name;

private Byte sex;

private Integer age;

public Integer getId() {

return id;

}

public NUser setId(Integer id) {

this.id = id;

return this;

}

public String getName() {

return name;

}

public NUser setName(String name) {

this.name = name;

return this;

}

public Byte getSex() {

return sex;

}

public NUser setSex(Byte sex) {

this.sex = sex;

return this;

}

public Integer getAge() {

return age;

}

public NUser setAge(Integer age) {

this.age = age;

return this;

}

private void writeObject(ObjectOutputStream output) throws IOException{

output.writeObject(this.name);

output.writeByte(this.sex);

output.writeInt(this.age);

}

private void readObject(ObjectInputStream input) throws IOException,ClassNotFoundException{

//按照序列化的顺序获取反序列化的字段

this.name = input.readObject().toString();

this.sex = input.readByte();

this.age = input.readInt();

}

@Override

public String toString() {

return "NUser [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";

}

}

可以看到我们和之前重写Externalizable的两个方法一样的写法,再次运行序列化后,结果如下:

对象已经发送:--->NUser [id=10, name=张三, sex=0, age=10]

NUser [id=null, name=张三, sex=0, age=10]

是不是和之前的结果一样?所以可以看到我们的猜测是正确的,并且我们在查看源码后可以看到:

我们的readObject/writeObjet方法 是通过反射来调用的,所以最终都是会调用了readObject/writeObject方法来实现

Java序列化使用的总结

通过上面的案例测试和比较,我们可以得到序列化使用的一些经验总结:Java 序列化只是针对对象的属性的传递,至于方法和序列化过程无关

当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口,反过来,子类实现序列化,而父类没有实现序列化则序列化会失败---即序列化具有传递性

当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进

行序列化(实现深度克隆)

当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段

被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法:writeObject 和

readObject或者实现Externalizable接口既然来了,点个关注再走呗~

java string 序列化_详解JAVA序列化相关推荐

  1. java 引用传递_详解java的值传递、地址传递、引用传递

    详解java的值传递.地址传递.引用传递 一直来觉得对值传递和地址传递了解的很清楚,刚才在开源中国上看到一篇帖子介绍了java中的值传递和地址传递,看完后感受颇深.下边总结下以便更容易理解. 按照以前 ...

  2. java内部格式_详解java内部类的访问格式和规则

    详解java内部类的访问格式和规则 1.内部类的定义 定义一个类来描述事物,但是这个事物其中可能还有事物,这时候在类中再定义类来描述. 2.内部类访问规则 ①内部类可以直接访问外部类中的成员,包括私有 ...

  3. java comparator相等_详解Java中Comparable和Comparator接口的区别

    详解Java中Comparable和Comparator接口的区别 发布于 2020-7-20| 复制链接 摘记: 详解Java中Comparable和Comparator接口的区别本文要来详细分析一 ...

  4. python java混合编程_详解java调用python的几种用法(看这篇就够了)

    java调用python的几种用法如下: 在java类中直接执行python语句 在java类中直接调用本地python脚本 使用Runtime.getRuntime()执行python脚本文件(推荐 ...

  5. java sleep唤醒_详解Java中的线程让步yield()与线程休眠sleep()方法

    Java中的线程让步会让线程让出优先级,而休眠则会让线程进入阻塞状态等待被唤醒,这里我们对比线程等待的wait()方法,来详解Java中的线程让步yield()与线程休眠sleep()方法 线程让步: ...

  6. java runnable 异常_详解Java中多线程异常捕获Runnable的实现

    详解Java中多线程异常捕获Runnable的实现 1.背景: Java 多线程异常不向主线程抛,自己处理,外部捕获不了异常.所以要实现主线程对子线程异常的捕获. 2.工具: 实现Runnable接口 ...

  7. java 代码块_详解java中的四种代码块

    在java中用{}括起来的称为代码块,代码块可分为以下四种: 一.简介 1.普通代码块: 类中方法的方法体 2.构造代码块: 构造块会在创建对象时被调用,每次创建时都会被调用,优先于类构造函数执行. ...

  8. java static 函数_详解java中的static关键字

    Java中的static关键字可以用于修饰变量.方法.代码块和类,还可以与import关键字联合使用,使用的方式不同赋予了static关键字不同的作用,且在开发中使用广泛,这里做一下深入了解. 静态资 ...

  9. Java implement意思_详解JAVA中implement和extends的区别

    详解JAVA中implement和extends的区别 发布于 2020-4-14| 复制链接 摘记: 详解JAVA中implement和extends的区别extends是继承父类,只要那个类不是声 ...

最新文章

  1. 【Ant Design Pro 五】箱套路由在菜单栏显示返回上一页
  2. 我为什么暂时放弃了React Native
  3. 强行分类提取特征自编码网络例3
  4. arXiv灌水机:机器自动生成论文标题、摘要信息,还有40+奇妙AI应用
  5. 蓝桥杯-拿糖果(java)
  6. Java黑皮书课后题第4章:*4.6(图上的随机点)编写一个程序,产生一个圆心位于(0,0)原点半径为40的圆上面的三个随机点,显示由这三个随机点组成的三角形的三个角的度数
  7. 从Java面试官的角度,如何快速判断程序员的能力
  8. WebAPI(part5)--排他操作
  9. Debian GNU/Linux 的发展简史
  10. redis storm mysql_storm-redis 详解
  11. littlevgl 透明按钮_张家港3-10吨叉车日租价格透明2020
  12. phpcms发布文章:overflow不显示问题(解决“代码横向溢出”)- 含代码、案例、截图
  13. java中事物的注解_JAVA中对事物的理解
  14. HTML基础知识回顾整理
  15. 小白教程 微信小程序 官方示例Demo下载及运行
  16. ftp 命令访问 ftp服务器
  17. 泰灏咨询的使命及愿景
  18. axure怎么制作聊天页面
  19. python关闭指定浏览器页面_如何用Python关闭打开的网页
  20. 云服务器怎么配置cpu与内存搭配

热门文章

  1. 期待的东软一面,好易通二面
  2. 计算机网络之Socket编程(UDP)
  3. mariadb galera 集群部署
  4. mysql 列转行 unpivot_Unpivot 列转行
  5. Linux登陆苹果M1芯片
  6. GitKraken Pro安装
  7. OSI七层模型和TCP/IP协议四层模型
  8. 【存储】什么是 WAL|数据库性能
  9. OpenStack组件--Cinder
  10. 女生喜欢上一个人时的生理反应是怎样的?