Java对象序列化详解

Java对象序列化详解所有分布式应用常常需要跨平台,跨网络,因此要求所有传的参数、返回值都必须实现序列化。一、定义  序列化:把Java对象转换为字节序列的过程。    反序列化:把字节序列恢复为Java对象的过程。二、用途  对象的序列化主要有两种用途:    1)把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(持久化对象)    2)在网络上传送对象的字节序列。(网络传输对象)…

大家好,又见面了,我是你们的朋友全栈君。

公众号原文链接

Mysql思维导图分享
RocketMQ思维导图,不点后悔

上面思维导图可去公众号回复:扣扣号,获取联系方式后找我免费获得可编辑版本。 后面会继续分享其他思维导图,包括Redis、JVM、并发编程、RocketMQ、RabbtiMQ、Kafka、spring、Zookeeper、Dubbo等等

什么是序列化和反序列化?

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

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


市面上的几种序列化方式

  Java对象是在JVM中生成的,如果需要远程传输或保存到硬盘上,就需要将Java对象转换成可传输的文件流。
市面上目前有的几种转换方式:

  • 1. 利用Java的序列化功能序列成字节字节流)也就是接下来要讲的。一般是需要加密传输时才用。
  • 2. 将对象包装成JSON字符串字符流
    转Json工具有Jackson、FastJson或者GJson,它们各有优缺点:
    • JackSon:Map、List的转换可能会出现问题。转复杂类型的Bean时,转换的Json格式不是标准的Json格式。适合处理 大文本Json
    • FastJosn:速度最快。将复杂类型的Bean转换成Json可能会有问题:引用类型如果没有引用被出错。适合对性能有要求的场景。
    • GJson:功能最全,可以将复杂的Bean和Json字符串进行互转。性能上面比FastJson有所差距。适合处理小文本Json,和对于数据正确性有要求的场景。
  • 3. protoBuf工具(二进制)
    性能好,效率高,字节数很小,网络传输节省IO。但二进制格式可读性差。

为什么需要序列化和反序列化?

  之所以需要序列化和反序列化,主要是因为Java对象是在JVM中生成的,是内存中的数据,如果需要把对象的字节序列远程传输保存到硬盘上时,你就需要将Java对象转换成二进制流。 这个转换过程就是序列化。

  假如别人传给你一个二进制流数据,当你想要恢复成内存中的对象时,你就需要反序列化

  Java平台允许我们在内存中创建可复用的Java对象,但只有当JVM(Java虚拟机)处于运行时,这些对象才可能存在,也就是这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存指定的对象(持久化对象),并在将来重新读取被保存的对象。     

  网络通信时,无论是何种类型的数据,都会转成字节序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。


Java原生序列化实现

实现了如下两个接口之一的类的对象才能被序列化:

  1) Serializable

  2) Externalizable

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

反序列化:ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

注:使用writeObject() 和readObject()方法的对象必须已经被序列化


关于serialVersionUID

  如果serialVersionUID没有显式生成,系统就会自动生成一个。此时,如果在序列化后我们将该类代码作了改动(包括类名、接口名、方法、属性),系统在反序列化时会重新生成一个新的serialVersionUID然后去和已经序列化的对象进行比较,就会报序列号版本不一致的错误。为了避免这种问题, 一般系统都会要求实现Serialiable接口的类显式的声明一个serialVersionUID。

所以显式定义serialVersionUID有如下两种用途:

  1、 希望类的不同版本对序列化兼容时,需要确保类的不同版本具有相同的serialVersionUID;

  2、 不希望类的不同版本对序列化兼容时,需要确保类的不同版本具有不同的serialVersionUID。


序列化机制算法

1. 所有保存到磁盘中的对象都有一个序列化编号

2. 当程序试图序列化一个对象时,程序先检查该对象是否已经被序列化过。如果从未被序列化过,系统就会将该对象转换成字节序列并输出;如果已经序列化过,将直接输出一个序列化编号。

示例

要被序列化的对象对应的类的代码:

public class Person implements Serializable { 
     
    private String name = null;  
    private Integer age = null;   
    
    public Person(){ 
   
        System.out.println("无参构造");
    }

    public Person(String name, Integer age) { 
     
        this.name = name;  
        this.age = age;  
    }  

    //getter setter方法省略...
    @Override 
    public String toString() { 
     
        return "[" + name + ", " + age+"]";  
    }  

下面MySerilizable 是一个简单的序列化程序,它先将一个Person对象保存到文件person.txt中,然后再从该文件中读出被存储的Person对象,并打印该对象。

public class MySerilizable { 
   
    public static void main(String[] args) throws Exception { 
    
        File file = new File("person.txt");  
        //序列化持久化对象
        ObjectOutputStream out = 
            new ObjectOutputStream(new FileOutputStream(file)); 
        Person person = new Person("Peter", 27);  
        out.writeObject(person);  
        out.close();  

        //反序列化,并得到对象
        ObjectInputStream in = 
            new ObjectInputStream(new FileInputStream(file));  
        // 没有强制转换到Person类型 
        Object newPerson = in.readObject(); 
        in.close();  
        System.out.println(newPerson);  
    }  
}

输出结果:[Peter, 27]

结果没有打印“无参构造”,说明反序列化机制无需通过构造器来初始Java对象。

没有调整构造方法,却能反序列化生成Java对象,这是怎么做到的呢?实际上反序列化时,是通过调用native方法给类的成员变量贬值的。

结论:

  1) 反序列化读取的仅仅是Java对象的数据,而不是Java类,所以在反序列化时必须提供该Java对象所属类的class文件(这里是Person.class),否则会引发ClassNotFoundException异常。

  2)当重新读取被保存的Person对象时,并没有调用Person的任何构造器,说明反序列化机制无须通过构造器来初始化对象。


如何实现选择序列化?

1、transient

  当对某个对象进行序列化时,系统会自动将该对象的所有属性依次进行序列化,如果某个属性引用到别一个对象,则被引用的对象也会被序列化。如果被引用的对象的属性也引用了其他对象,则被引用的对象也会被序列化。这就是递归序列化。

  有时候,我们并不希望出现递归序列化,或是某个存敏感信息(如银行密码)的属性不被序列化,我们就可通过transient关键字修饰该属性来阻止被序列化。

将上面的Person类的age属性用transient修饰:

transient private Integer age = null;

再去执行MySerilizable的结果为:

[Peter, null] // 返序列化时没有值,说明age字段未被序列化

2、writeObject()方法与readObject()方法

  使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性(此时就跟transient一样)。

  如果我们想要上面的Person类里的name属性在序列化后存在文件里不让别人知道具体是什么(加密),我们就可在Person类里加如下代码:

  //自定义序列化
 private void writeObject(ObjectOutputStream out) 
     throws IOException { 
      
    // 将当前类的非静态和非瞬态字段写入此流。 
    //如果不写,如果还有其他字段,则不会被序列化 
    // out.defaultWriteObject(); 
        
    out.writeObject(new StringBuffer(name).reverse());
    //将name简单加密(即反转),这样别人就知道是怎么回事,
    // 当然实际应用不可能这样加密。
    out.writeInt(age);  
 }  



//反序列化
 private void readObject(ObjectInputStream in) 
     throws IOException, ClassNotFoundException { 
     
    // 从此流读取当前类的非静态和非瞬态字段。
    //如果不写,其他字段就不能被反序列化
    //in.defaultReadObject();
    
    //解密:即简单的反转
    name = ((StringBuffer)in.readObject()).reverse().toString();  
    age = in.readInt();  
 }

  详细的自定义序列化与反序列化可参见ObjectOutputStream 和ObjectInputStream 类的JDK文档。

3、Externalizable接口

  Externalizable接口 与Serializable 接口类似,只是Externalizable接口需要强制自定义序列化。

要序列化对象的代码:


public class Teacher implements Externalizable{ 
   
    private String name;
    private Integer age;
    //setter、getter方法省略
    
    public Teacher(){ 
   
        System.out.println("无参构造");
    }

    public Teacher(String name,Integer age){ 
   
        System.out.println("有参构造");
        this.name = name;
        this.age = age;
    }   

    @Override
    public void writeExternal(ObjectOutput out) 
          throws IOException { 
   
         //将name简单加密
        out.writeObject(new StringBuffer(name).reverse());
        //out.writeInt(age); //注掉这句后,age属性将不能被序化
    }

    @Override
    public void readExternal(ObjectInput in) 
          throws IOException, ClassNotFoundException { 
   
        name = ((StringBuffer) in.readObject()).reverse().toString();
        //age = in.readInt(); 
    }
    
    @Override 
    public String toString() { 
     
        return "[" + name + ", " + age+ "]";  
    } 
}

主函数代码改为:

public class MySerilizable { 
   
    public static void main(String[] args) throws Exception { 
     
        File file = new File("person.txt");  

        //序列化持久化对象
        ObjectOutputStream out = 
            new ObjectOutputStream(new FileOutputStream(file)); 
        Teacher person = new Teacher("Peter", 27);  
        out.writeObject(person);  
        out.close();  

        //反序列化,并得到对象
        ObjectInputStream in = 
            new ObjectInputStream(new FileInputStream(file));  
        // 没有强制转换到Person类型 
        Object newPerson = in.readObject(); 
        in.close();  
        System.out.println(newPerson);  
    }  
}

打印结果:

有参构造
无参构造 //与Serializable 不同的是,还调用了无参构造
[Peter, null] //age未被序列化,所以未取到值

单例模式的序列化

  当我们使用Singleton模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能略有不同。对前面使用的Person类进行修改,使其实现Singleton模式,如下所示:

public class Person implements Serializable { 
     
    private static class InstanceHolder { 
    
        private static final Person instatnce = 
            new Person("John", 31, "男");  
    }  

    public static Person getInstance() { 
     
        return InstanceHolder.instatnce;  
    }  

    private String name = null;  
    private Integer age = null;  
    private String gender = null;  

    private Person() { 
     
        System.out.println("必须私有化的无参构造");  
    }  

    private Person(String name, Integer age, String gender) { 
    
        System.out.println("有参构造");  
        this.name = name;  
        this.age = age;  
        this.gender = gender;  
    }  
    ...  
}

  同时要修改MySerilizable 应用,使得能够保存/获取上述单例对象,并进行对象相等性比较,如下代码所示:


public class MySerilizable { 
     
    public static void main(String[] args) throws Exception { 
     
        File file = new File("person.txt");  
        ObjectOutputStream out = 
            new ObjectOutputStream(new FileOutputStream(file)); 
        out.writeObject(Person.getInstance()); // 保存单例对象 
        out.close();  

        ObjectInputStream in = 
            new ObjectInputStream(new FileInputStream(file)); 
        Object newPerson = in.readObject();  
        in.close();  
        System.out.println(newPerson);  
         // 将获取的对象与Person类中的单例对象进行相等性比较 
        System.out.println(Person.getInstance() == newPerson);
    }  
}

打印结果:

有参构造  
[John, 31,]  
false  //说明不是同一个对象

  从上面结果能发现,序列化后反序列化之后,不再是同一个对象了。其实是可以这样解释的:Java有4种创建对象的方式(new、newInstance()、clone()以及这里的readObject()方法),readObject方法就是相当于新建了一个对象,所以上面引用的会是不同的对象。


反序列化漏洞

  黑客会可以在你反序列化的数据里加上恶意代码,当你反序列化时就执行这段恶意代码。

  像这种情况,就需要我们将拿到手的数据进行校验,如签名等。


序列化对象注意事项

  1.对象的类名、属性都会被序列化;而方法、static属性(静态属性)、transient属性(即瞬态属性)都不会被序列化(这也就是第4条注意事项的原因)

  2.虽然加static也能让某个属性不被序列化,但static不是这么用的

  3.要序列化的对象的引用属性也必须是可序列化的,否则该对象不可序列化,除非以transient关键字修饰该属性使其不用序列化。

  4.反序列化地象时必须有序列化对象生成的class文件(很多没有被序列化的数据需要从class文件获取)。
在实际开发过程中,可能你发送后,对方并没有你的实体类对象,那怎么办,这时就需要你这边将对象转成字符串的形式,示例:

MqInfo info = new MqInfo();
String body = JSON.toJSONString(mqInfo)

  5.当通过文件、网络来读取序列化后的对象时,必须按实际的写入顺序读取。

  6.要传输的对象,不要忘记了实现Serializable接口


姐是一位有着十年白嫖经验的渣媛,想白嫖的扣我:2735035681
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/151102.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • RewriteCond和13个mod_rewrite应用举例Apache伪静态

    RewriteCond和13个mod_rewrite应用举例Apache伪静态1.给子域名加www标记RewriteCond%{HTTP_HOST}^([a-z.]+)?example.com$[NC]RewriteCond%{HTTP_HOST}!^www.[NC]RewriteRule.?http://www.xample.com%{REQUEST_URI}[R=301,L]这个规则抓取二级域名的%1变量,如果不是以www开始,

    2022年6月6日
    28
  • ansi编码是什么意思_编码ANSI

    ansi编码是什么意思_编码ANSIANSI就是其他外文编码,且不同国家和地区的ANSI各有不同,即不兼容。举例,在中文简体下,你如果想编码表,保存时

    2022年9月23日
    0
  • android软件开发工具(WiFi破解)

    做为一个多年奋战在Android应用开发一线的程序员来说,程序调试的苦是不言而喻的,在过去的很长一段时间里,我们如果要调试Android应用只能通过USB数据线,一头连着手机,一头联着电脑,不敢让手机离开电脑半步。、         曾经有一段时间,我总是担心天天这样高强度的调试别把手机的USB口给磨坏了。也许有朋友问了,那怎么不用模拟器呢?事实上,不是不想用,而是电脑上开个模似器可能需

    2022年4月13日
    57
  • plsql 连接oracle数据库详细配置「建议收藏」

    plsql 连接oracle数据库详细配置「建议收藏」第一次用这种方式连接oracle数据库,自己百度搞了快两个小时才弄好,百度的资源也不靠谱,看了好多都不完整,搞完了报各种错误,各种连不上数据库,自己整理下资料,希望给其他的同行予以借鉴,不能保证每个人都能操作成功!毕竟有时真的得看人品了,呵呵!第一步:先安装plsql客户端,plsql客户端是必须的,我的是同事给的plsql(英文版客户端)安装很简单(下一步下一步…….)就不做说明!

    2022年9月17日
    0
  • embedding实现_embedded option

    embedding实现_embedded option假设词汇量为100万个时的CBOW模型如下,输入层和输出层存在100万个神经元。下面两个问题导致耗时严重。问题一、输入层的one-hot表示和权重矩阵的乘积。one-hot表示占用内存过多,计算one-hot表示与权重矩阵的乘积,需要花费大量时间。问题二、中间层和权重矩阵的乘积以及Softmax层的计算。需要大量的计算,花费大量时间。解决问题一:计算one-hot表示矩阵和权重矩阵的乘积,其实就是将权重矩阵的某个特定的行取出来。如下图所示。Embedding层

    2022年9月6日
    2
  • java重写和重载的区别总结_java覆盖和重载

    java重写和重载的区别总结_java覆盖和重载重写只存在于子类与父类中,重载存在于一个类中。具体区别如下:一、重写(override)override是重写(覆盖)了一个方法,以实现不同的功能。一般是用于子类在继承父类时,重写(重新实现)父类中的方法。重写(覆盖)的规则:1、重写方法的参数列表必须完全与被重写的方法的相同,否则不能称其为重写而是重载.2、重写方法的访问修饰符一定要大于被重写方法的访问修饰符(public>protecte…

    2022年9月3日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号