背景
在Java中,可以通过多种方式来创建对象,并且只要这些对象没有被回收都可以复用这些对象。但是,创建出来的这些对象都储存在JVM的堆(stack)内存中,只有JVM处于运行状态时,这些对象才存在,一旦JVM停止运行,则这些对象就消失了。
如果需要将这些对象持久化储存或传输,并且在需要的时候将对象重新读取出来,Java的序列化可以帮助实现。
概念
序列化概念
把一个对象转换为一个字节序列的过程称为对象的序列化。该字节序列包含该对象的数据、有关对象的类型的信息和储存在对象中数据的类型。并将该字节序列写入文件(可以是字节或者XML格式),从而可以储存和传输。
反序列化概念
把写入文件的字节序列读取出来恢复为对象的过程称为对象的反序列化。也就是说,对象的类型信息、对象的数据和对象的数据类型可以用来在内存中创建新的对象。
序列化特点
整个过程都是Java虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。
序列化使用场景
1.对象网络传输 例如:在微服务系统中或给第三方提供接口调用时,使用rpc进行调用,一般会把对象转化成字节序列,才能在网络上传输;接收方则需要把字节序列再转化为java对象。 2.对象保存至文件中 例如:hibernate中的二级缓存:把从数据库中查询出的对象,序列化转存到硬盘中,下次读取的时候,首先从内存中找是否有该对象,如果没有在去二级缓存(硬盘)中去查找。减少数据库的查询次数,提升性能。 ...... ...... 序列化前提条件
一个类的对象想要序列化成功,需要满足以下两个条件:
- 该类必须实现java.io.Serializable接口
如果对一个没有实现该接口的类进行序列化,则会出现java.io.NotSerializableException异常。
- 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的(使用关键字Transient,JVM会忽略transient变量的原始值并将变量的默认值保存到文件中。因此,transient意味着不要序列化)。
如果试图序列化一个不可序列化的对象会报错,如下所示:
使用
Java IO包提供了ObjectOutputStream类用于实现类的序列化功能;提供了ObjectInputStream类用于实现类的反序列化功能。如下所示:
定义学生类(注意此时还没添加serialVersionUID)
public class Account implements Serializable{ private String username; private String password; public Student() { } public Student(String username, String password) { super(); this.username = username; this.password = password; } @Override public String toString() { return "Account [username=" + username + ", password=" + password + "]"; } }
序列化
public class SerializeTest { public static void main(String[] args) { Account account = new Account("Tom", "123"); try { FileOutputStream fileOutputStream = new FileOutputStream("account.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(student); objectOutputStream.close(); fileOutputStream.close(); } catch (Exception e) { e.printStackTrace(); } } }
反序列化
public class DeserializeTest { public static void main(String[] args) { Account account = null; try { FileInputStream fileInputStream = new FileInputStream("account.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); account = (Account) objectInputStream.readObject(); objectInputStream.close(); fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); return; }catch(ClassNotFoundException c){ System.out.println("Account class not found"); c.printStackTrace(); return; } System.out.println(account); } } 运行结果为: Account [name=Tom, password=123]
transient关键字
transient(短暂的,透明的) 关键字的作用是控制变量的序列化,如果有属性是不可序列化的,则在变量声明前加上该关键字,表示该属性是短暂的,JVM会忽略transient修饰的变量的原始值并将变量类型的默认值(如 int 型的是 0,对象型的是 null)保存到文件中,从而不被写入文件中。
如将上面的password字段声明为transient,如下所示:
private transient String password;
反序列化后结果为:
Account [name=Tom, password = null]
serialVersionUID
在上面的案例中,Account类还没有添加serialVersionUID(版本号)。如果将Account类序列化后,再给Account类中添加一个新的字段,如下所示:
public class Account implements Serializable{ private String username; private transient String password; private double balance; //为Account类添加新的字段 ...... }
此刻再进行序列化,会报java.io.InvalidClassException异常:
java.io.InvalidClassException: day13.Account; local class incompatible: stream classdesc serialVersionUID = 4186211771023085883, local class serialVersionUID = -3783640273171219810 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at day13.DeserializeTest.main(DeserializeTest.java:19)
这是由于Account类修改了,也就是修改过后的class,不兼容了。处于安全机制考虑,程序抛出了错误,并且拒绝载入。从异常信息中可以看出,它是根据 serialVersionUID 值进行判断类是否修改过。
注意事项:图中有两个serialVersionUID,且不一样,这是由于如果没有显示的声明 serialVersionUID 属性,那么java编译器会自动生成一个 serialVersionUID(应该是根据属性和方法进行摘要算出来的,除了方法体内容变动,serialVersionUID 的值不会改变之外,其他变动都会引起默认的serialVersionUID改变,所以有两个不同的serialVersionUID)。
所以,定义需要序列化的类时需要显示声明serialVersionUID,这样serialVersionUID就唯一了。版本号一致就反序列成功,否则会出错。
所以,在上面的Account类中需要添加一个serialVersionUID,这样再添加新的字段后,就不会报错了。如下所示:
public class Account implements Serializable{ private static final long serialVersionUID = 1L; ...... }
参考: