java 序列化机制

一天一java,一天一进步


一、什么叫序列化?

序列化最简单的理解就是将对象转换为有序字节,方便存储和传输。

Java序列化就是将一个对象转化为一个二进制表示的字节数组,通过保存或则转移这些二进制数组达到持久化的目的。要实现序列化,需要实现java.io.Serializable接口。反序列化是和序列化相反的过程,就是把二进制数组转化为对象的过程。在反序列化的时候,必须有原始类的模板才能将对象还原。从这个过程我们可以猜测到,序列化过程并不像class文件那样保存类的完整的结构信息。序列化接口没有方法或字段,仅用于标识可序列化的语义。Java的"对象序列化"能让你将一个实现了Serializable接口的对象转换成一组byte,这样日后要用这个对象时候,你就能把这些byte数据恢复出来,并据此重新构建那个对象了。

序列化,它是完整的保存了某一状态下的对象信息,是一个整体,而不是零散的!序列化的过程,就是一个“freeze”的过程,它将一个对象freeze住,然后进行存储,等到再次需要的时候,再将这个对象de-freeze就可以立即使用。

进行序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量

在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取 父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都 是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。所以和父类不实现serializable接口,实现的效果一样。

二、为什么序列化?

序列化是一种处理对象流的机制,将对象中的内容进行流化,可以对流化后的对象进行读写操作,也可以将流化后的对象传输于网络之间。

序列化就是对实例对象的状态(State 对象属性而不包括对象方法)进行通用编码(如格式化的字节码)并保存,以保证对象的完整性和可传递性。简而言之:序列化,就是为了在不同时间或不同平台的JVM之间共享实例对象,为了跨进程传递格式化数据。

所以序列化就是为了解决在对对象流进行读写时所引发的问题,通俗点讲也就是上文提到的方便存储和传输。

三、如何序列化?

最简单的,以java为例,我们可以实现Serializable进行接口序列化。(当然,序列化的方式还有很多,包括Java原生以流的方法进行的序列化、Json序列化、FastJson序列化、Protobuff序列化。想要学习其他的方法可以看看这个博客,https://blog.csdn.net/pistolove/article/details/60321123

首先上一个例子,进行说明:

public class foo implements Serializable {

	private static final long serialVersionUID = 1L;
	
	private int width;
	private int length;

	public int getWidth() {
		return width;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public int getLength() {
		return length;
	}

	public void setLength(int length) {
		this.length = length;
	}

}

首先看这个类,实现了Serializable接口,但是你看这个接口的时候,你会发现,哎,这个接口是个空接口。其实,看一下接口的注释说明就知道,当我们让实体类实现Serializable接口时,其实是在告诉JVM此类可被序列化,可被默认的序列化机制序列化。

然后发现后面还跟着一个  private static final long serialVersionUID = 1L,序列化和反序列化就是通过对比这个SerialversionUID来进行的,一旦SerialversionUID不匹配,反序列化就无法成功。在实际的生产环境中,我们可能会建一系列的中间Object来反序列化我们的pojo,为了解决这个问题,我们就需要在实体类中自定义SerialversionUID。

剩下的就是我们的get和set方法了。以上是我们要进行序列化的类,一般是长这个样子。下面是我们一个简单序列化的过程:

public class write {

	public static void main(String[] args) throws IOException {
		foo myFoo = new foo();
		myFoo.setLength(10);
		myFoo.setWidth(9);
		
		FileOutputStream fs = new FileOutputStream("foo.ser");
		ObjectOutputStream os = new ObjectOutputStream(fs);
		os.writeObject(myFoo);
		os.close();
		
		System.out.println("comlete");

	}
}
public class read {

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		FileInputStream fi = new FileInputStream("foo.ser");
		ObjectInputStream inputStream = new ObjectInputStream(fi);

		foo foo = (foo) inputStream.readObject();
		System.out.println(foo.getLength());
		System.out.println(foo.getWidth());
		fi.close();
	}
}

我们使用FileOutputStream写入文件的过程同使用FileInputStream过程相同,都是先用File类打开本地文件,在这里我们直接定义的路径,实例化输入输出流,然后调用流的读写方法读取或写入数据,最后关闭流。然后使用java序列化流 ObjectOutPutStream和ObjectInputStream创建对象,创建的对象称为对象输出流和对象输入流。使用创建的对象输出流调用writeObject方法将一个对象Object写入到一个文件中去,或者调用一个readObject方法读取一个对象到程序中来,读取结束后关闭流。整个过程代码如上图。其中FileOutputStream的流程如下图:

四、序列化后长什么样

序列化后的二进制字节数据如下:

aced 0005 7372 0018 636f 6d2e 7973 6c2e
5365 7269 616c 697a 6162 6c65 5465 7374
ffff ffff ffff ffff 0200 0149 0003 6e75
6d78 7000 0007 e2

上述的内容分为一下几个部分:

第一部分是序列化文件头

  • AC ED :STREAM_MAGIC声明使用了序列化协议
  • 00 05 :STREAM_VERSION序列化协议版本
  • 73 :TC_OBJECT声明这是一个新的对象

第二部分是序列化的类的描述,在这里是SerializableTest

  • 72 :TC_CLASSDESC声明这里开始一个新的class
  • 00 18:class名字的长度是24个字节
  • 636f 6d2e 7973 6c2e 5365 7269 616c 697a 6162 6c65 5465 7374:SerializableTest的完整类名
  • ffff ffff ffff ffff:serialVersionUID,序列化ID,如果没有指定,则会由算法随机生成一个8字节的ID
  • 02 :标记号,声明该类支持序列化
  • 00 01:该类所包含的域的个数为1

第三部分是对象中各个属性的描述

  • 49:域类型,49代表I,也就是int类型
  • 00 03:域名字的长度为3
  • 6e 75 6d:num属性的名称

第四部分为对象的父类信息描述

SerializableTest没有父类,如果有,和第二部分的描述相同

  • 78 :TC_ENDBLOCKDATA,对象块的结束标志
  • 70:TC_NUL:说明没有其他超类的标志

第五部分为对象属性的实际值

如果属性是一个对象,那么这里还将序列化这个对象,规则和第二部分一样

  • 00 0007 e2:数值2018

虽然Java的序列化能够保证对象状态的持久保存,但是遇到一些对象结构复杂的情况还是比较难处理的,下面是对一些复杂情况的总结:

  • 当父类实现了Serializable接口的时候,所有的子类都能序列化
  • 子类实现了Serializable接口,父类没有,父类中的属性不能被序列化(不报错,但是数据会丢失)
  • 如果序列化的属性是对象,对象必须也能序列化,否则会报错
  • 反序列化的时候,如果对象的属性有修改或则删减,修改的部分属性会丢失,但是不会报错
  • 在反序列化的时候serialVersionUID被修改的话,会反序列化失败
  • 在存Java环境下使用Java的序列化机制会支持的很好,但是在多语言环境下需要考虑别的序列化机制,比如xml,json,或则protobuf

五、问题

  1. 希望对象的某些属性不参与序列化应该怎么处理?
  2. 对象序列化之后,如果类的属性发生了增减那么反序列化时会有什么影响呢?
  3. 如果父类没有实现java.io.Serializable接口,子类实现了此接口,那么父类中的属性能被序列化吗?
  4. serialVersionUID属性是做什么用的?必须申明此属性吗?如果不申明此属性会有什么影响?如果此属性的值发生了变化会有什么影响?
  5. 能干预对象的序列化与反序列化过程吗?

     答案:https://www.cnblogs.com/wangg-mail/p/4354709.html

    6.Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得 t1 和 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
Test test = new Test();
test.i = 1;
out.writeObject(test);
out.flush();
test.i = 2;
out.writeObject(test);
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                    "result.obj"));
Test t1 = (Test) oin.readObject();
Test t2 = (Test) oin.readObject();
System.out.println(t1.i);
System.out.println(t2.i);

当两次写入同一个类,但是在中间改变了一个属性值时,输出结果两个输出的都是 第一个的值, 原因就是第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。读者在使用一个文件多次 writeObject 需要特别注意这个问题。

7.对敏感数据加密解密

private void writeObject(ObjectOutputStream out) {
    try {
        PutField putFields = out.putFields();
        System.out.println("原密码:" + password);
        password = "encryption";//模拟加密
        putFields.put("password", password);
        System.out.println("加密后的密码" + password);
        out.writeFields();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
 
private void readObject(ObjectInputStream in) {
    try {
        GetField readFields = in.readFields();
        Object object = readFields.get("password", "");
        System.out.println("要解密的字符串:" + object.toString());
        password = "pass";//模拟解密,需要获得本地的密钥
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
 
}
 
public static void main(String[] args) {
    try {
        ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("result.obj"));
        out.writeObject(new Test());
        out.close();
 
        ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                "result.obj"));
        Test t = (Test) oin.readObject();
        System.out.println("解密后的字符串:" + t.getPassword());
        oin.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}   

 在上面 的 writeObject 方法中,对密码进行了加密,在 readObject 中则对 password 进行解密,只有拥有密钥的客户端,才可以正确的解析出密码,确保了数据的安全。

六、参考文献

          https://www.cnblogs.com/chenmingjun/p/9746310.html

          https://www.cnblogs.com/aishangJava/p/6936927.html

          https://www.cnblogs.com/senlinyang/p/8204752.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yann.bai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值