【Java基础第一弹】Java序列化基础篇
Java类通过实现java.io.Serializable 接口便可启用其序列化功能。实现了序列化的类的实例可以在不同的系统或JVM间传递,并且不会丢失原实例的相关状态及值。
为一个类开启序列化功能只需实现Serializable 接口即可,仅仅作为使用者可以不必了解其内部的更深层次的实现及流程,但是如果想要更好的使用序列化功能就需要与我一起详细的学习和了解它。
1.什么是Java序列化
Java类通过实现java.io.Serializable 接口以启用其序列化功能:
import java.io.Serializable; public class Cat implements Serializable { }
未实现此接口(java.io.Serializable)的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的,也就是说实现了Serializable 接口的类的子类默认也是可序列化的。
序列化接口java.io.Serializable 没有任何方法或字段,仅用于标识可序列化的语义,Serializable 接口可以理解为一种标志,实现了它就被标识为可序列化。
2.序列化的用途
实现序列化接口Serializable 的类,在被实例化后,该类就具备了一定的状态和相应属性值,在相同的虚拟机进程中,我们可以通过参数传递的方式让别的方法或类来使用它,从而获取该实例的 相关信息。但是一旦该JVM进程关闭或该实例不再被使用,该实例的数据内容要么被GC清理掉,要么直接随JVM进程关闭而消失。
如果我想要将该实例对象内容保存或传递给其他虚拟机使用该怎么办呢?
答案当然不是实现 java.io.Serializable 接口,或者说不仅仅是实现java.io.Serializable 接口。
java.io.Serializable 接口只是一个标识,标识着实现它的类可以被序列化,之后再通过其他手段才能将该类的实例信息进行存储和传递。
所以序列化的一个用途就是持久化存储,也就是将对象实例直接存储到相关文件中;另一个用途就是不同系统间进行传递。后者用途更为广泛。
总结:
序列化的用途:传递和存储。
3.可序列化、序列化与反序列化
实现Serializable 接口的类即是可序列化类,所以可序列化可以理解为序列化对象的第一步。一个类可序列化后接下来就需要进行“序列化”与“反序列化”了,这样就组成了一套完整的序列化流程,才能达到序列化对象实例的目的。
无论是采用哪种方式传输序列化内容,序列化与反序列化都可以理解为“写”和“读”操作 ,所以序列化与反序列化是共生关系,它们无法单独存在。就像读和写操作,光把内容写进文件或网络传输中,使用者却没有相应的读取手段则此“写”操作是无意义的。
序列化中所谓的“写”和“读”对应于以下方法:
1 /** 2 * 写入对象内容 3 */ 4 private void writeObject(java.io.ObjectOutputStream out) 5 /** 6 * 读取对象内容 7 */ 8 private void readObject(java.io.ObjectInputStream in)
通过这两个方法可以将对象实例进行“序列化”与“反序列化”操作,除了对象实例的相关操作还有很多其他类型的方法,诸如writeInt/readInt、writeBoolane/readBoolean等。
总结:
可序列化:一个类实现接口Serializable 后即变为可序列化类。
序列化:既是整套序列化流程的总称,又是单独针对某实例进行的“写”操作。
反序列化:读取已经序列化成功的实例内容,将其实例信息还原。
4.可序列化、序列化与反序列化实现
1)一个类想要序列化首先需要实现java.io.Serializable 接口:
1 package com.animals; 2 import java.io.Serializable; 3 /** 4 * 喵星人 5 * @author 286 6 * 7 */ 8 public class Cat implements Serializable { 9 10 private String name; 11 private int age; 12 private String color; 13 14 public String getName() { 15 return name; 16 } 17 public void setName(String name) { 18 this.name = name; 19 } 20 public int getAge() { 21 return age; 22 } 23 public void setAge(int age) { 24 this.age = age; 25 } 26 public String getColor() { 27 return color; 28 } 29 public void setColor(String color) { 30 this.color = color; 31 } 32 }
implements Serializable 即标识着Cat 类已经可以被序列化了。
2)存储序列化后的实例,以文件存储为例(其中os可以修改为其他形式流):
1 Cat cat=new Cat(); 2 cat.setName("喵星人"); 3 cat.setAge(3); 4 cat.setColor("白色"); 5 //定义文件输出流,准备将数据写入文件中 6 OutputStream os=new FileOutputStream("E:\\cat.txt"); 7 ObjectOutputStream oos=new ObjectOutputStream(os); 8 //写入实例数据 9 oos.writeObject(cat); 10 oos.close(); 11 os.close();
3)写入文件成功后,利用记事本打开该文件,发现文件内容根本无法识别,但其中或多或少出现了相关全限定名、属性名等信息,这些信息格式与JVM中的存储方式是一样的:
- sr com.animals.Cat@d追Y袺 I ageL colort Ljava/lang/String;L nameq ~ xp t 鐧借
- 壊t 鍠垫槦浜?
将其转换成UTF-8编码后,会显示诸如:“白色”,“喵星人”等信息,但依旧大部分是乱码。然后我们再利用16进制编辑器打开此文件:
发现原来Cat 实例的相关信息其实是以固定的内容格式存储起来的而已,猜想只需要按这个格式读取的话就能恢复原实例的数据内容了吧。
4)反序列化还原实例内容:
1 InputStream is=new FileInputStream("E:\\cat.txt"); 2 ObjectInputStream ois=new ObjectInputStream(is); 3 //读取序列化实例内容 4 Cat cat=(Cat)ois.readObject(); 5 System.out.println(cat.getName()); 6 System.out.println(cat.getAge()); 7 System.out.println(cat.getColor()); 8 ois.close(); 9 is.close(); 10 //打印结果: 11 喵星人 12 3 13 白色
总结:
可以看出序列化的一系列操作还是非常简单实用的,通过非常简洁的代码就可以实现 实例的共享与传递。
6.序列化版本号
在平时创建可序列化类之后,一般编译器会自动提示类似如下内容:
点击修正后弹出对话框:
其中第一个,第二个选项分别是“添加一个默认的序列化版本ID”、“生成一个序列化版本 ID”,一般情况下我们会默认选择第一种方式,此后会在可序列化类中添加一个serialVersionUID 属性:
- private static final long serialVersionUID = 1L;
serialVersionUID 就是我们要说的“序列化版本号”概念,显然我们也可以不去声明此属 性,依然可以使用序列化的各项操作来写入和获取实例信息,所以从表面现象来看添加serialVersionUID 与否并不会影响我们序列化和反序列化的使用。
其实不然,以下情况时就会凸显出serialVersionUID 的作用:
1)还是按照原来的方式序列化Cat 类实例,将实例信息存储起来。
2)此时我们修改Cat类的属性,将age去掉,换成weight,如下:
1 package com.animals; 2 import java.io.Serializable; 3 /** 4 * 喵星人 5 * @author 286 6 * 7 */ 8 public class Cat implements Serializable { 9 10 private String name; 11 private double weight; 12 private String color; 13 public String getName() { 14 return name; 15 } 16 public void setName(String name) { 17 this.name = name; 18 } 19 public double getWeight() { 20 return weight; 21 } 22 public void setWeight(double weight) { 23 this.weight = weight; 24 } 25 public String getColor() { 26 return color; 27 } 28 public void setColor(String color) { 29 this.color = color; 30 } 31 32 }
3)再利用反序列化获取实例信息:
1 InputStream is = new FileInputStream("E:\\cat.txt"); 2 ObjectInputStream ois = new ObjectInputStream(is); 3 Cat cat = (Cat) ois.readObject(); 4 System.out.println(cat.getName()); 5 System.out.println(cat.getWeight()); 6 System.out.println(cat.getColor()); 7 ois.close(); 8 is.close(); 9 //打印结果: 10 Exception in thread "main" java.io.InvalidClassException: com.animals.Cat; local class incompatible: stream classdesc 11 12 serialVersionUID = 1747507533076615499, local class serialVersionUID = 2870256057611081222 13 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:562) 14 at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1583) 15 at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1496) 16 at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1732) 17 at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1329) 18 at java.io.ObjectInputStream.readObject(ObjectInputStream.java:351) 19 at Test.main(Test.java:17)
4)结果却反序列化失败,抛出了异常,大致意思是两个类的serialVersionUID 值不相等。
序列化运行时使用serialVersionUID 版本号与每个可序列化类相关联,该序列号在反序列 化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException异常。可序列化类可以通过声明名为 "serialVersionUID" 的字段(该字段必须是静态 (static)、最终(final) 的 long 型字段)显式声明其自己的 serialVersion ANY-ACCESS-MODIFIER (任意修饰符) static final long serialVersionUID = 1L;
如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计 算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。不过,强烈建议所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样 在反序列化过程中可能会导致意外的 InvalidClassException。因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列 化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声 明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
总结:
serialVersionUID 的作用类似与我们从网络上下载文件时MD5值的作用,MD5消息摘 要值可以判断所下载的文件是否被篡改过。serialVersionUID 的作用也是如此,用于判断序列化与反序列化的类是否相同或兼容,所谓兼容就 是即使类的内部属性不相同,但只要serialVersionUID 相同,类还是那个类就可以反序列化。更形象的比喻一下就是“即使你整容了,那么你还是你”。
疑问:
如果除了类名不同,所有属性和serialVersionUID 都相同的话,那么Cat类实例被序 列化后我能否直接反序列化成Cat2呢?
答案是否定的,因为在序列化过程中类的全限定名(简单理解为包名+类名)等信息已经被 存储,所以不同的类是无法反序列化兼容的。运行过程中会抛出类似如下异常:
1 Exception in thread "main" java.lang.ClassCastxception: com.animals.Cat cannot be cast to 2 com.animals.Cat2