一文完全搞懂JAVA序列化

前言

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

序列化有什么意义

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

咱们建立一个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 + "]";
	}
}
复制代码

而后咱们编写发送对象(序列化)的实现:jvm

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();
   		}
   	}
   }
}
复制代码

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

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,看下结果:ide

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的取值方案:ui

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

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序列化使用的总结

经过上面的案例测试和比较,咱们能够获得序列化使用的一些经验总结:

  1. Java 序列化只是针对对象的属性的传递,至于方法和序列化过程无关
  2. 当一个父类实现了序列化,那么子类会自动实现序列化,不须要显示实现序列化接口,反过来,子类实现序列化,而父类没有实现序列化则序列化会失败---即序列化具备传递性
  3. 当一个对象的实例变量引用了其余对象,序列化这个对象的时候会自动把引用的对象也进 行序列化(实现深度克隆)
  4. 当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段
  5. 被申明为 transient 的字段,若是须要序列化,能够添加两个私有方法:writeObject 和 readObject或者实现Externalizable接口