目录
- 建议11:显示声明UID
- 建议12:避免用序列化类的构造函数中为常量赋值
- 建议13:避免为final变量复杂赋值
- 建议14:break的必记
- 建议15:避免Instanceof非预期结果
建议11:显示声明UID
我们编写了一个实现Serializable接口(序列化标志接口)的类,Eclipse就会给你一个黄色警告:需要增加一个SerialVersion ID。为什么要增加就在以下说明。
类实现Serializable 接口的目的是为了可持久化,比如网络传输或本地存储,为系统的分布和异构部署提供先决支持条件。若没有序列化,我们的远程调用、对象数据库就都不能存在。
public class Test implements Serializable{
private String name;
public String getName(){
return this.name;
}
public void setName(String name){
this.name = name;
}
}
这是一个简单的JavaBean,实现了Serializable接口,可以在网络上传输,也可以本地存储然后读取。这里我们展示以Java消息服务方式传递该对象。首先定义一个消息的生产者
public class Producer{
public static void main(String[] args) throws Exception{
Test test = new Test();
test.setName("魔鬼");
// 序列化,保存到磁盘上
Utils.wirteObject(test);
}
}
class Utils{
private static String FILE_NAME = "d:/a.bin";
public static void writeObject(Serializable s){
try{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME));
oos.writeObject(s);
}catch(Exception e){
e.printStackTrace();
}finally{
oos.close();
}
}
public static Obejct readObject(){
Object obj = null;
//反序列化
try{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_NAME));
obj = ois.readObject();
}catch(Exception e){
e.printStackTrace();
}finally{
ois.close();
}
}
}
通过对象序列化过程,把一个对象从内存块转化为可传输的数据流,然后通过网络发送到消息消费者,并进行反序列化,生成实例对象
public class Customer{
public static void main(String[] args) throws Exception{
// 反序列化
Test test = (Test)Utils.readObject();
System.out.println(test.getName());
}
}
这是一个对象数据流转换为一个实例对象的过程 结果是 魔鬼
但是此处隐藏一个问题:如果消息的生产者和消息的消费者所参考的类Test有差异,会出现什么事情?譬如说消息生产者中增加了一个年龄属性,而消费者中没有增加该属性。为什么没增加,因为这可能是一个分布式部署的应用。这样的话反序列化会报一个InvalidClassException异常,原因是序列化所对应类版本发生了变化,JVM不能把数据流转换为实例对象。
这就要说JVM是根据什么来判断一个类的版本呐?
- SerialVersionUID也叫做流标识符即类的版本定义的,可以显式声明也可以隐式声明
显式声明
private static final lonf serialVersionUID = XXXXXL;
隐式生命就是我不声明,编译器在编译的时候帮我生成。生成的依据是通过包名、类名、继承关系、非私有的方法和属性,以及参数、返回值等诸多因子计算得出的,极度复杂,基本上计算出来的值是唯一的。
serialVersionUID的作用就是。JVM在反序列化时,会比较数据流中的与类中的是否一致,如果一致则表示没有发生改变。可以实例对象,否则抛异常。
有时候我们的类改变不大,就要求JVM是否可以把以前的对象反序列化回来,就依靠显式声明serialVersionUID,向JVM通知我的版本没有变,实现向上兼容。
也就是在Test类中加入
private static final long serialVersionUID = 2333L;
【注意】显式声明可以避免对象不一致,但是尽量不要用这种方式。
建议12:避免用序列化类的构造函数中为常量赋值
带有final标识的属性是不变量,也就是说只能赋值一次,不能重复赋值,但是在序列化类中有点复杂。
public class Test implements Serializable{
private static final long serialVersionUID = 71282334L;
public final String name="魔鬼";
}
这个Test类被絮硫化存储在磁盘,反序列化时name会被重新计算值(与static变量不同,static变量根本就没保存到数据流中),比如name属性修改成"天使",那么反序列化对象的name值就是"天使".保持新旧对象final变量相同,有利于代码业务逻辑统一,保证序列化基本规则之一。这种方式不多说
另一种赋值方式:通过构造方法
public class Test implements Serializable{
private static final long serialVersionUID = 71282334L;
public final String name;
public Test(){
this.name = "魔鬼";
}
}
这也是一种我们常用的赋值方式,我们试试序列化并做一个简单的模拟,修改name值代表变更,这里注意serialVersionUID不变 第一次输入"魔鬼",第二次改为"天使"
那么打印结果是什么,是"魔鬼"。因为这里触及了反序列化的另一个规则:反序列化时构造函数不会执行。
反序列化过程
- JVM从数据流中获取一个Object对象,然后根据数据流中的类文件描述信息(在序列化时,保存到磁盘的对象文件包含了类描述信息,不是类)查看发现是final变量,需要重新计算,于是引发Test类的name值,此时JVM发现name没有赋值,不能引用,于是他就没有初始化,而是保持原值。所以结果就是"魔鬼"
【注意】:在序列化类中,不要用构造函数为final变量赋值
建议13:避免为final变量复杂赋值使
为final变量赋值的方式还有一种是通过方法赋值
public class Test implements Serializable{
private static final long serialVersionUID = 71282334L;
public final String name = initName();
public String initName(){
return "魔鬼";
}
}
name属性是通过initName方法的返回值赋值的,这在复杂类中经常用到,这比使用构造函数赋值更简洁,易修改,那么如此用法在序列化时是否会有问题?
以下是修改后的Test类代码
public class Test implements Serializable{
private static final long serialVersionUID = 71282334L;
public final String name = initName();
public String initName(){
return "天使";
}
}
仅仅修改了initName返回值,也就是说通过new生成的Test对象的final变量值都是"天使",把之前存储在磁盘的士力架再下来,打印下name值会是什么,结果会是"魔王"
上个建议说final变量会被重新赋值,这个值指的是简单对象(8个基本类型,数组,字符串(特指不通过new创建的情况下),final变量的赋值与基本类型相同),但是不能方法赋值
其中的原理是
- 类描述信息
保存了包路径、继承关系、访问权限、变量描述、变量访问权限、方法签名、返回值、以及变量的关联类信息。这些是为了保证反序列化的正常运行 - 非瞬态(transient关键字)和非瞬态(static关键字)的实例变量值
值如果是基本类型的就简单保存下来,如果是复杂对象,连该对象和关联类信息一起保存,并且持续递归下去。
【注意】反序列化时final变量在以下情况下不会被重新赋值: - 通过构造函数final变量赋值
- 通过方法返回值为final变量赋值
- final修饰的属性不是基本类型。
建议14:break的必记
我们经常写一些转换类由器是将1转换为"壹"等操作那就要写工具类了
public class Test{
public static String toChineseNumberCase(int n){
String chinsesNumber = "";
switch(n){
case 1: chineseNumber ="一";
case 2: chineseNumber ="二";
case 3: chineseNumber ="三";
case 4: chineseNumber ="四";
case 5: chineseNumber ="五";
case 6: chineseNumber ="六";
case 7: chineseNumber ="七";
case 8: chineseNumber ="八";
case 9: chineseNumber ="九";
}
return chineseNumber;
}
}
我们运行看看输入2=九??
回头再看程序程序从case2 后面的语句开始执行一直找break关键字可惜我们没有写,一直到switch结束。
这种问题十分容易出现,对于这种问题个人建议就是将编辑器IDE的警告级别switch case调到Errors级别没写就报错。
建议15:避免Instanceof非预期结果
instanceof是一个简单的二元操作符,是用来判断一个对象是否是一个类实例的,其操作类似于>=、==。
public class Test{
public static void main(String[] args){
boolean b1 = "String" instanceof Object;
boolean b2 = new String() instanceof String;
boolean b3 = new Object() instanceof String;
boolean b4 = 'A' instanceof Character;
boolean b5 = null instanceof String;
boolean b6 = new Date() instanceof String;
boolean b7 = new GenericClass<String>().isDateInstance("");
}
}
class GenericClass<T>{
public boolean isDateInstance(T t){
return t instanceOf Date;
}
}
boolean b1 = “String” instanceof Object;
返回值是true 因为字符串继承了Object 所以是true
boolean b2 = new String() instanceof String;
返回值是true 一个类的对象当然是它的实例
boolean b3 = new Object() instanceof String;
返回值是false Object是父类,其对象当然不是String的实例
boolean b4 = ‘A’ instanceof Character;
返回值false’A’是char类型,也就是一个基本类型,不是一个对象,instanceof只能用于对象判断,不能用于基本类型判断
boolean b5 = null instanceof String;
返回值是false,这是instanceof特有规则,左操作数是null就直接返回false
boolean b6 = new Date() instanceof String;
编译通不过,因为 Date类和 String没有继承或实现关系
boolean b7 = new GenericClass().isDateInstance("");
编译通过了返回值是false,因为Java泛型是为编码服务的,在编译成字节码是,T已经是Object类型了,传递的实数是String类型,也就是说T表面类型是Object,实际类型是String,这就话就等价于 Object instanceof Date 所以返回false很正常