当前位置: 首页 > 科技观察

面试官:serialVersionUID在Java中的作用是什么?举个例子说明

时间:2023-03-20 11:37:42 科技观察

serialVersionUID适用于Java的序列化机制。简单的说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。反序列化时,JVM会将传入字节流中的serialVersionUID与对应的本地实体类的serialVersionUID进行比较。如果相同则认为一致,可以反序列化,否则会出现序列化版本不一致的异常InvalidCastException。具体的序列化过程如下:序列化时,系统会将当前类的serialVersionUID写入序列化后的文件中,反序列化时,系统会检测文件中的serialVersionUID,判断是否与当前类一致。serialVersionUID是一致的。如果一致,则说明被序列化的类的版本与当前类的版本相同,反序列化即可成功,否则会失败。有两种方式生成serialVersionUID:默认1L,如:privatestaticfinallongserialVersionUID=1L;根据类名、接口名、成员方法和属性生成一个64位的哈希字段,如:privatestaticfinallongserialVersionUID=xxxxL;当一个类实现了Serializable接口时,如果没有定义serialVersionUID,Eclipse会给出相应的提示。面对这种情况,我们只需要在Eclipse中点击类中的警告图标,Eclipse就会自动提供两种生成方法。如果你不想定义它,你也可以在Eclipse设置中关闭它。设置如下:Window==>Preferences==>Java==>Compiler==>Error/Warnings==>PotentialprogrammingproblemsSetSerializableclasswithoutserialVersionUIDwarning可以改成忽略。当实现java.io.Serializable接口的类没有显式定义serialVersionUID变量时,Java序列化机制会根据编译后的Class自动生成serialVersionUID,用于序列化后的版本比较。这样的话,如果Class文件(类名、方法名等)没有变化(加空格、换行、加注释等),即使你多次编译,serialVersionUID也不会变。如果我们不想通过编译强制划分软件版本,即实现序列化接口的实体可以兼容之前的版本,我们需要显式定义一个long类型的名为serialVersionUID的变量,并且不修改这个变量值的序列化实体两者都可以相互序列化和反序列化。下面用代码来说明serialVersionUID在应用中的几种常见情况。(1)序列化实体类packagecom.sf.code.serial;importjava.io.Serializable;publicclassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=123456789L;publicintid;publicStringname;publicPerson(intid,Stringname){this.id=id;this.name=name;}publicStringtoString(){return"Person:"+id+""+name;}}(2)顺序化功能:packagecom.sf.code.serial;importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.ObjectOutputStream;publicclassSerialTest{publicstaticvoidmain(String[]args)throwsIOException{Personperson=newPerson(1234,"wang");System.out.println("PersonSerial"+person);FileOutputStreamfos=newFileOutputStream("Person.txt");ObjectOutputStreamoos=newObjectOutputStream(fos);oos.writeObject(person);oos.flush();oos.close();}}(3)反序列化功能:packagecom.sf.code.serial;importjava.io.FileInputStream;importjava.io.IOException;importjava.io.ObjectInputStream;publicclassDeserialTest{publicstaticvoidmain(String[]args)throwsIOException,ClassNotFoundException{Personperson;FileInputStreamfis=newFileInputStream("Person.txt");ObjectInputStreamois=newObjectInputStream(fis);person=(Person)ois.readObject();ois.close();System.out.println("PersonDeserial"+person);}}情况一:假设Person类序列化后,从A转移到B,再在B上反序列化,在序列化Person和反序列化Person时,A和B都需要存在一个相同的类。如果两个地方的serialVersionUID不一致,会出现什么错误?【答】可以用上面的代码做个实验验证一下:先执行测试类SerialTest生成一个序列化文件,代表A端的序列化文件,然后修改serialVersion的值,然后执行测试classDeserialTest,表示B端使用不同的serialVersion类反序列化,结果报错:Exceptioninthread"main"java.io.InvalidClassException:com.sf.code.serial.Person;localclassincompatible:streamclassdescserialVersionUID=1234567890,localclassserialVersionUID=123456789atjava.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:621)atjava.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)atjava.io.ObjectInputStream.readClassDesc(ObjectInputStream.java.java:1518)atjava.ioe.ObjectInputreadOrdinaryObject(ObjectInputStream.java:1774)atjava.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)atjava.io.ObjectInputStream.readObject(ObjectInputStream.java:371)atcom.sf.code.serial.DeserialTest.main(DeserialTest.java:13)情况2:假设两个serialVersionUID相同,如果A方增加一个字段,然后序列化,B方保持不变,然后反序列化,会发生什么?packagecom.sf.code.serial;importjava.io.Serializable;公众号lassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=1234567890L;publicintid;publicStringname;publicintage;publicPerson(intid,Stringname){this.id=id;this.name=name;}publicPerson(intid,Stringname,intage){this.id=id;this.name=name;this.age=age;}publicStringtoString(){return"Person:"+id+",name:"+name+",age:"+age;}}publicclassSerialTest{publicstaticvoidmain(String[]args)throwsIOException{Personperson=newPerson(1234,"wang",100);System.out.println("PersonSerial"+person);FileOutputStreamfos=newFileOutputStream("Person.txt");ObjectOutputStreamoos=newObjectOutputStream(fos);oos.writeObject(person);oos.flush();oos.close();}}PersonDeserialPerson:1234,name:wang【答】Addpublicintage;执行SerialTest,生成序列化文件,删除代表A端的publicintage,代表B端的deserialize,最后的结果是:进行了序列化,反序列化正常,但是A端添加的字段丢失了(忽略)B端)。Case3:假设两个serialVersionUID相同,如果B端减少一个字段,A端不变会怎样?/publicStringname;publicintage;publicPerson(intid,Stringname){this.id=id;//this.name=name;}publicStringtoString(){return"Person:"+id//+",name:"+name+",age:"+age;}}PersonDeserialPerson:1234,age:0【答】序列化反序列化正常,B端字段比A端少,A端字段值多丢失(被忽略)B面)。情况四:假设两个serialVersionUID是一致的,如果在B端增加一个字段,A端保持不变会怎样?验证过程如下:首先执行SerialTest,然后在实体类Person中添加一个字段age,如下图,然后ExecutethetestclassDeserialTest.packagecom.sf.code.serial;importjava.io.Serializable;publicclassPersonimplementsSerializable{privatestaticfinallongserialVersionUID=1234567890L;publicintid;publicStringname;publicintage;publicPerson(intid,Stringname){this.id=id;this.name=name;}/*publicPerson(intid,Stringname,intage){this.id=id;this.name=name;this.age=age;}*/publicStringtoString(){return"Person:"+id+",name:"+name+",age:"+age;}}Result:PersonDeserialPerson:1234,name:wang,age:0表示序列化和反序列化正常,B端新添加的int字段赋默认值0.最后通过下面的图片,总结一下上面的几种情况。静态变量序列化场景:参见清单2中的代码。清单2.静态变量序列化问题代码(newTest());out.close();//序列化后修改为10Test.staticVar=10;ObjectInputStreamoin=newObjectInputStream(newFileInputStream("result.obj"));Testt=(Test)oin.readObject();oin.close();//再次读取,通过t.staticVar打印新值System.out.println(t.staticVar);}catch(FileNotFoundExceptione){e.printStackTrace();}catch(IOExceptione){e.printStackTrace();}catch(ClassNotFoundExceptione){e.printStackTrace();}}}清单2中的主要方法,序列化对象后,修改静态变量的值,然后读取序列化后的对象,然后获取值通过读取对象的静态变量并将其打印出来。根据清单2,这个System.out.println(t.staticVar)语句输出10还是5?最后输出10,看不懂的读者,打印出来的staticVar是从读取对象中获取的,应该是保存时的状态。之所以打印10是因为序列化的时候没有保存静态变量,其实更容易理解。序列化保存的是对象的状态,而静态变量属于类的状态,所以序列化不保存静态变量。父类序列化和Transient关键字情况:子类实现了Serializable接口,其父类没有实现Serializable接口,序列化子类对象,反序列化后输出父类定义的一个变量的值,变量的值与序列化时的值不同。解决方法:如果要序列化父类对象,需要让父类也实现Serializable接口。如果父类没有实现,就需要有一个默认的无参构造函数。当父类没有实现Serializable接口时,虚拟机是不会序列化父对象的,Java对象的构造必须先有父对象才有子对象,反序列化也不例外。因此,反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认父对象。因此,当我们取父对象的变量值时,它的值就是调用父类的无参构造函数后的值。如果考虑这种序列化情况,在父类无参构造函数中初始化变量,否则,父类变量值就是默认声明的值,比如int类型默认值为0,string类型默认值类型为空。Transient关键字的作用是控制变量的序列化。在变量声明前加上这个关键字可以防止变量被序列化到文件中。反序列化后,瞬态变量的值被设置为初始值。比如int类型为0,object类型为null。特性用例我们很熟悉使用Transient关键字来防止字段被序列化,那么有没有其他的方法呢?根据父类对象的序列化规则,我们可以将不需要序列化的字段提取出来,放到父类中。子类实现了Serializable接口,父类没有实现。根据父类的序列化规则,父类的字段数据不会被序列化,形成类图如图2所示。图2.案例程序类图从上图可以看出,attr1,attr2、attr3和attr5不会被序列化。放在父类中的好处是,当有另一个Child类时,attr1、attr2、attr3仍然不会被序列化,也不需要重复写transient,代码简洁。staticfinal修饰的serialVersionUID是如何写入到序列化文件中的,参见下面的源码:){//REMIND:synchronizeinsteadfrelyingonvolatile?if(suid==null){suid=AccessController.doPrivileged(newPrivilegedAction(){publicLongrun(){returncomputeDefaultSUID(cl);}});}返回suid。长值();}