改善Java程序的建议(每日5条)3

探讨Java序列化机制,包括SerialVersionUID的作用、final变量赋值规则、构造函数及方法赋值的影响,以及序列化类的最佳实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录
  • 建议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很正常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值