面试|什么是序列化?怎么实现?有哪些方式?
1 为什么要序列化(背景)以及什么是序列化?
对于Java初学者来说,序列化这个概念很难接触到,因为这个阶段还没有接触到系统和框架,没有系统的交互和消息的传递,Java对象以及类的基本信息在JVM内存中随着JVM停止而消失,JVM下次启动又会重新加载字节码。但是假如系统下次启动后,某对象A需要依赖系统本次对象A的值的时候,就需要考虑对象A“持久化”的问题。相信大家看到“持久化”都会想到数据库或者缓存,咱们这里注重面向对象的思维来“持久化”对象,所以数据库和缓存的方式不适合这种情况。基于对象能够在程序不运行的情况下仍能存在并保存其信息的需求,对象的序列化功能孕育而生。
对象的序列化是指通过某种方法把对象以字节序列的形式保存起来。反之通过字节序列得到原对象就是反序列化。
这里要想到几个问题:
- 通过什么方法进行序列化?
- 对象序列化后的字节序列是什么样子?
- 字节序列保存在哪里?通过什么形式保存?
下面围绕这些问题一一深入分析。
2 通过什么方式进行序列化?
简单来说,只要对象实现了Serializable接口,该对象就可以进行序列化。Serializable接口只是一个标记接口,不包括任何属性和方法。或许这样说比较抽象,下面写个简单的例子说明序列化的过程。
被序列化类:
public class Person implements Serializable { private static final long serialVersionUID = 8580374262428896565L; private String name; private int age; private String height; public Person(String name, int age, String height) { this.name = name; this.age = age; this.height = height; } // 省略setter getter }
客户端类序列化person对象:
public class Client { public static void main(String[] args) throws IOException, ClassNotFoundException { // 序列化 FileOutputStream fos = new FileOutputStream("e://out.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); Person person = new Person("starry", 25, "177"); oos.writeObject(person); oos.flush(); oos.close(); //反序列化 FileInputStream fis = new FileInputStream("e://out.txt"); ObjectInputStream ois = new ObjectInputStream(fis); Person personOut = (Person) ois.readObject(); System.out.println(personOut.getName()+ " " + personOut.getAge() + " " + personOut.getHeight()); } }
我们这里手动把person对象序列化到本地文件“e://out.txt”(存储的文件名和后缀名可随意取)内,具体过程是调用ObjectOutputStream对象的writeObject();反序列化则是调用ObjectInputStream对象的readObject()方法。
注意:Person类中的serialVersionUID是实现Serializable接口的类都要生成的一个静态常量,有两种生成方式:一是直接定义为1L,一是随机生成;其作用是为了保证反序列化时找到正确的版本。
3 对象序列化后的字节序列是什么样子?
跟踪ObjectOutputStream对象的writeObject()方法,不难发现最后以byte形式存储对象,这种存储就是字节序列存储,也是序列化名字的由来。我们知道.class文件是字节码文件,所以序列化后的文件与.class文件类似,那么字节序列的存储形式也就很明显辽。下面以十六进制方式查看序列化后的具体内容:
//字节byte读取 FileInputStream fis1 = new FileInputStream("e://out.class"); //将文件数据读取到字节数组byte中,数组大小由fis1的可读大小决定 byte[] bytes = new byte[fis1.available()]; while( fis1.read(bytes) != -1){} //确定十六进制的书写方式 String HEX = "0123456789ABCDEF"; //将字节转化为十六进制 for(byte b:bytes){ //取字节的高四位,与0x0f与运算,得到该十六进制数据对应的索引(0~15) System.out.print(HEX.charAt((b >> 4) & 0x0f)); //字节的低四位 System.out.print(HEX.charAt(b & 0x0f)); System.out.print(" "); //AC ED 00 05 73 72 ...... } fis1.close();
输出结果:
AC ED 00 05 73 72 00 1F 63 6F 6D 2E 73 74 61 72 72 79 2E 73 65 72 69 61 6C 69 7A 61 62 6C 65 31 2E 50 65 72 73 6F 6E 77 13 9C EE 4F FA 8D 35 02 00 03 49 00 03 61 67 65 4C 00 06 68 65 69 67 68 74 74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 4C 00 04 6E 61 6D 65 71 00 7E 00 01 78 70 00 00 00 19 74 00 03 31 37 37 74 00 06 73 74 61 72 72 79
这是person对象序列化后,用十六进制表示的形式。对于它具体表示的内容等到学习字节码相关知识时再回过头来看。但是,根据理论来讲,只要是属于对象的属性和方法都会序列化。除了普通的成员变量和实例方法外,静态成员变量和静态方法呢?
我们可以在Person类里面增加一个静态属性slave,然后自己敲敲看看什么结果。注意静态属性属于类的,而不是属于对象。
4 字节序列保存在哪里?通过什么形式保存?
这个问题问的有点多余,因为对象的序列化不是为了保存在本地(而是保存对象某时刻的状态),而是用于系统之间的通信。系统间都是通过接口传递信息,信息除了是常见的自定义bean对象和集合对象(常见的Map)外,还有XML格式的报文。所以当上游系统发送序列化的bean或者XML格式的报文后,下游系统可以同步或者异步的接收信息并进行处理。(关于XML格式报文的序列化本文不讨论,可以自行百度)
5 序列化和反序列化过程?
先看下面的实例:(建议实操)
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class TypeA { public static void main(String[] args) throws Exception { // 序列化 System.out.println("..............序列化开始.............................."); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("e://A.txt")); A a = new A(2); oos.writeObject(a); oos.flush(); oos.close(); System.out.println("..............序列化结束.............................."); //反序列化 System.out.println("..............反序列化开始.............................."); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e://A.txt")); A a_ = (A) ois.readObject(); ois.close(); System.out.println(a_.getI()); System.out.println("..............反序列化结束.............................."); } } class A implements Serializable { private int i; public int getI() { return i; } public A() { System.out.println("A : 不带参数的构造方法"); } public A(int i) { this.i = i; System.out.println("A: 带参数的构造方法"); } }
运行结果:
..............序列化开始.............................. A: 带参数的构造方法 ..............序列化结束.............................. ..............反序列化开始.............................. 2 ..............反序列化结束..............................
表明反序列化过程不会调用类的构造方法,而是直接根据字节码生成对象。但是有一种情况例外,接着往下看。
6 定制序列化内容
序列化给我们传递对象和报文带来了方便,如果没有序列化就没有系统间的交互,所以序列化非常重要。有些场景不需要我们把所有的属性都传递出去,所以需要针对实际情况定制化对象的序列化内容。针对定制化,Java提供了一接口Externalizable。
public class ExternalizableTest { public static void main(String[] args) throws Exception { B b = new B(2, "helloWorld"); FileOutputStream fos = new FileOutputStream("E://B.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(b); oos.close(); FileInputStream fis = new FileInputStream("E://B.txt"); ObjectInputStream ois = new ObjectInputStream(fis); B b_ = (B)ois.readObject(); System.out.println(b_.toString()); ois.close(); } } class B implements Externalizable { private int i; private String str; public B() { System.out.println("B : 不带参数的构造方法"); } public B(int i, String str) { this.i = i; this.str = str; System.out.println("B : 带参数的构造方法"); } @Override public String toString() { return str + " " +i; } @Override public void writeExternal(ObjectOutput out) throws IOException { System.out.println("B.writeExternal"); out.writeObject(str); out.writeInt(i); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("B.readExternal"); str = (String)in.readObject(); i = in.readInt(); } }
运行结果:
B : 带参数的构造方法 B.writeExternal B : 不带参数的构造方法 B.readExternal helloWorld 2
首先说明下Externalizable接口继承自Serialiazable接口,且有两个抽象方法,即实例中的writeExternal(arg)和readExternal(arg)。然后,这个运行结果透露出很多信息:
- b对象序列化时,会自动调用writeExternal(arg)方法,此方法用于定制序列化的内容,即b对象的什么属性需要序列化,什么属性不需要序列化;针对不需要序列化的属性就没必要调用out.writeXX()方法;另外,还可以在该方法中自定义属性的内容。
- b对象反序列化时,会自动调用类B不带参数的构造函数,意思是会重新生成一个新的B类对象(新对象的任何属性都没有值);这点与实现Serializable接口的类完全不同。
- b对象反序列化时,会自动调用readExternal(arg)方法,此方法用于给新生成的B类对象赋值;这里你可以在两个方法里把属性i不序列化(注释掉),你会发现结果反序列化结果中,i的值为0,即使你开始构造b对象时是赋值的。
所以,实现Externalizable接口的类,完全可以根据场景定制序列化内容。如果你嫌这种方法比较麻烦,需要在方法里单独定制化内容,那么你需要了解下transient关键字。
7 transient关键字
transient是Java关键字之一,可以用来修饰属性,可以防止属性被序列化。
public class ExternalizableTest { public static void main(String[] args) throws Exception { C c = new C("xiaoLi", "123456"); FileOutputStream fos = new FileOutputStream("E://C.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(c); oos.close(); FileInputStream fis = new FileInputStream("E://C.txt"); ObjectInputStream ois = new ObjectInputStream(fis); C c_ = (C)ois.readObject(); System.out.println(c_.toString()); ois.close(); } } class C implements Serializable{ private String name; private transient String passWord; public C (String name, String passWord) { this.name = name; this.passWord = passWord; } public String toString() { return name + " " + passWord; } }
注意:由于Externalizable对象在默认情况下不保存它们的任何属性(不调用任何out.wirteXX()方法),所以transient关键字只能和Serializable对象一起使用。
8 序列化里面需要注意的几个点
a.子类能够继承父类的序列化功能
public class Point { public static void main(String[] args) throws Exception { Son son = new Son("xiaoLi", 20); FileOutputStream fos = new FileOutputStream("E://Son.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(son); oos.close(); FileInputStream fis = new FileInputStream("E://Son.txt"); ObjectInputStream ois = new ObjectInputStream(fis); Son son_ = (Son)ois.readObject(); System.out.println(son_.toString()); ois.close(); } } class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String toString() { return this.getClass().getName()+ ": " + name + ", " + age; } } class Son extends Person { public Son(String name, int age) { super(name, age); } }
b.引用类型的属性会随着对象序列化而序列化
乍看这句话,容易让人产生一种错误的理解:序列化一个对象a时,对象a的引用属性b不用实现序列化接口也能随着对象a的序列化而序列化;实际不然,对象b的类也需要实现序列化接口才能随着对象a的序列化而序列化。(可以自己写个实例验证下)
9 附加题
本来序列化内容到这里应该结束了,但是Java提供的功能太强大了,下面这部分内容感兴趣的可以了解下,因为实际工作中上面的知识已经够了。
先看一个实例:
public class SelfSerializable { public static void main(String[] args) throws Exception { D d = new D("name", "password"); FileOutputStream fos = new FileOutputStream("E://D.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(d); oos.close(); FileInputStream fis = new FileInputStream("E://D.txt"); ObjectInputStream ois = new ObjectInputStream(fis); D d_ = (D)ois.readObject(); System.out.println(d_.toString()); ois.close(); } } class D implements Serializable { private String i; private transient String j; public D(String i, String j) { this.i = "non transient: " + i; this.j = "transient: " + j; } public String toString() { return i + ", " + j; } private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); oos.writeObject(j); } private void readObject(ObjectInputStream ois) throws Exception { ois.defaultReadObject(); System.out.println("readObject before: " + j); j = (String)ois.readObject(); System.out.println("readObject after: " + j); } }
运行结果:
readObject before: null readObject after: transient: password non transient: name, transient: password
根据上面讲的,被transient关键字修饰的属性j应该不会被序列化,但是这里还是正常序列化辽。为什么呢?
关键在于类D的两个方法:
private void wirteObject(ObjectOutStream oos){} private void readObject(ObjectInputStream ois){}
如果你打debug断点进到自定义的writeObject(oos)方法,你会发现它执行的流程是这样的:
在执行main()方法的oos.writeObject(arg)方法时,会通过反射调用类D的writeObject(arg)方法,序列化流程都会按照类D的writeObject(arg)逻辑执行。咱们这里是先执行默认的序列化方法,然后把属性j进行序列化,在读取的时候,其实属性j的值已经有值了。defaultReadObject()方法是为了读取属性i的值。
所以,这种序列化方法有什么用呢?
前面讲到,针对不需要序列化的属性可以通过Externalizable接口和transient关键字解决,此时的属性是没有进行序列化的。如果我网络上传输的是密码,那么对于安全性要求很严格,不能不传但又要保证安全性,那么就需要对特定的属性字段加密或者特殊化处理,那么这个时候就可以使用这里的方法改造那个特殊字段。故,此方法适用于针对某字段特殊化处理的情况。
毕!
- 设计模式用过哪些,应用场景是什么;单例模式有几种实现方式,代码怎么写?
- 什么是单例模式?单例模式有哪些方式实现?写个例子。
- java,什么是序列化,怎么实现序列化
- spring boot项目怎么创建jsp页面?咋访问jsp?要配置什么?有哪些配置方式?
- NingShanFeng_2019面试之Mapper 接口绑定有几种实现方式,分别是怎么实现 的?
- java,什么是序列化,怎么实现序列化
- 什么是Java序列化?为什么序列化?序列化有哪些方式?
- 什么是js深拷贝和浅拷贝?有哪些实现方式?
- 继承有几种方式,分别是什么,想要实现继承可以使用哪些方法
- 什么是Java序列化?为什么序列化?序列化有哪些方式?
- GOF23设计模式-单例模式-5种实现方式比较和防止反射与反序列化漏洞
- 什么是对象序列化?如何实现?什么情况下使用?
- spring AOP是什么?拿它做什么? 在面试中,你怎么答?
- 【Java面试题】45 什么是java序列化,如何实现java序列化?或者请解释Serializable接口的作用。
- 什么是java序列化,如何实现java序列化?
- 什么是java序列化,如何实现java序列化?
- 怎么实现用户匿名访问web,但数据库要用Windows集成验证方式(数据库和web服务器分别在两台机器上)
- java 实现线程同步的方式有哪些
- 什么是java序列化,如何实现java序列化?
- ASP.net的身份验证方式有哪些?分别是什么原理?