面试|什么是序列化?怎么实现?有哪些方式?

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)方法,你会发现它执行的流程是这样的:
面试|什么是序列化?怎么实现?有哪些方式?_第1张图片
在执行main()方法的oos.writeObject(arg)方法时,会通过反射调用类D的writeObject(arg)方法,序列化流程都会按照类D的writeObject(arg)逻辑执行。咱们这里是先执行默认的序列化方法,然后把属性j进行序列化,在读取的时候,其实属性j的值已经有值了。defaultReadObject()方法是为了读取属性i的值。

所以,这种序列化方法有什么用呢?
前面讲到,针对不需要序列化的属性可以通过Externalizable接口和transient关键字解决,此时的属性是没有进行序列化的。如果我网络上传输的是密码,那么对于安全性要求很严格,不能不传但又要保证安全性,那么就需要对特定的属性字段加密或者特殊化处理,那么这个时候就可以使用这里的方法改造那个特殊字段。故,此方法适用于针对某字段特殊化处理的情况

毕!

你可能感兴趣的:(Java基础面试题,Java基础知识)