此文为阅读《Java程序的151个建议》时的笔记记录。
1.不要在常量和变量中出现易混淆的字母。
2.莫让常量蜕变成变量。务必让常量的值在运行期保持不变。
3.三元操作符的类型必须一致。
三元操作符是if-else的简化写法,在项目中使用它的地方很多,也非常好用,但是好用又简单的东西并不表示就可以随便用,如下面的例子:
public class Client {
public static void main(String[] args){
int i= 80;
String s = String.valueOf(i < 100 ? 90 : 100);
String s1 = String.valueOf(i < 100 ? 90 : 100.0);
System.out.println("两者是否相等 : " + s.equals(s1));
}
}
看完代码,会认为虽然s2的第二个操作数是100.0,但是三元操所符的条件都为真了,就肯定只返回第一个值了,与第二个值没有关系,肯定运行结果为true。然鹅实际的运行结果为false,因为90(int) 和 100.0(float) 类型不一致,可三元操作符必须要返回一个数据,而且类型要确定,不可能条件为真时返回 int,条件为假时返回 float,编译器是不允许如此的,所以它就会进行自动类型转换了,int 转换为浮点数 90.0,自然s1与s2就不相等了。这里还设计到三元操作符的类型转换规则:
- 若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来进行转换,int 类型转换为long类型,long类型转换为float类型等
- 若两个操作数中有一个是数字S,另外一个是表达式,且类型标示为T,那么,若数字S在T范围内,则转换为T 类型;若超出了T 类型的范围,则T 转换为S类型
- 若两个操作数都是直接量数字,则返回值类型为范围较大者。
4.避免带有变长参数的方法重载。
5.别让 null 值和空值威胁到变长方法。
public class Client {
public void methodA(String str,Integer is){...}
public void methodA(String str,String str){...}
public static void main(String[] args){
Client client = new Client();
client.methodA("China",0);
client.methodA("China","People");
client.methodA("China");
client.methodA("China",null);
}
}
上面这个例子在client.methodA("China")和 client.methodA("China",null)语句都会编译报错,原因就是方法模糊不清,编译器不知道调用哪一种方法。
6.覆盖变长方法也循规蹈矩。
覆写必须满足以下条件:
- 重写方法不能缩小方位权限
- 参数列表必须与被重写方法相同
- 返回类型必须与被重写方法的相同或者是其子类
- 重写方法不能抛出新的一场,或者超出父类范围的异常,但是可以抛出更少、更有限的异常,或者不抛出异常。
7.警惕自增陷阱。
public class Client {
public static void main(String[] args){
int count = 0;
for(int i=0;i<10;i++){
count = count++;
}
System.out.println("count="+count);
}
}
这个程序的输出的count等于几?是count自加10次吗?答案等于10?可以非常肯定的告诉你,答案错误!运行结果是count=0。
count++是一个表达式,是有返回值的,它的返回值就是count自加前的值,Java对自加是这样处理的:首先把count的值(注意是值不是引用)拷贝到一个临时变量区,然后对count变量加1,最后返回临时变量区的值。陈小古第一次循环时的详细处理步骤如下:
- JVM把 count 的值(0)拷贝到临时变量区。
- count 值加1,这时候count的值是1。
- 返回临时变量区的值,注意这个值是0,没修改过。
- 返回值赋给 count,此时 count 值被重置成 0。
"count=count++"这条语句可以按照如下代码来理解:
public static int mockAdd(int count){
//先保存初始值
int temp = count;
//做自增操作
count = count + 1;
//返回原始值
return temp;
}
于是第一次循环后的 count 值还是0,其他9此的循环也一样,所以最终为0。
8.养成良好习惯,显式声明UID。
我们编写一个实现了Serializable接口(序列化标志接口)的类,Eclipse马上就会给一个黄色警告:需要增加一个Serial Version ID。为什么要增加?它是怎么知道的呢?接下来我们就来解释该问题。
类实现Serializable接口的目的是为了可持久化,比如网络传输或者本地存储,为系统的分布和异构部署提供先决支持条件。若没有序列化,现在我们熟悉的远程调用、对象数据库都不可能存在,我们来看一个简单的序列化实例类:
pubilc class Person implements Serializable{
private String name;
/* name属性的getter/setter方法省略 */
}
这是一个简单的JavaBean,实现了 Serializable 接口,可以在网络上传输,也可以本地存储然后读取。这里我们以Java消息服务(Java Message Service)方式传递该对象(即通过网络传输一个对象),定义在消息队列中的数据类型为ObjectMessage,首先顶一个消息的生产者(Producer),代码如下:
public class Producer{
public static void main(String[] args) throws Exception {
Person p = new Person();
p.setName("你的名字");
//序列化,保存到磁盘上
SerializationUtils.writeObject(p);
}
}
这里引入了一个工具类 SerializationUtils,其作用是对一个类进行序列化和反序列化,并且存储到硬盘上(模拟网络传输),其代码如下:
public class SerializationUtils {
private static String FILE_NAME = "c:/obj.bin";
//序列化
public static vod writeObject(Serializable s) {
try{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream (FILE_NAME));
oos.writeObject(s);
oos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static Object readObject(){
Object obj = null;
//反序列化
try {
ObjectInput input = new ObjectInputStream(new FileInputStream(FILE_NAME));
obj = input.readObeject();
input.close();
} catch {
e.printStackTrace();
}
}
}
通过对象序列化过程,把一个对象从内存块转化为可传输的数据流,然后通过网络发送到消息消费者(Consumer)那里,并进行反序列化,生成实例对象,代码如下:
public class Consumer {
public static void main (String[] args) throws Exception {
//反序列化
Person p = (Person) SerializationUtils.readObject();
System.out.println("name=" + p.getName());
}
}
这是一个反序列化过程,也就是对象数据流转换为一个实例对象的过程,其运行后的输出结果为:你的名字。这就是序列化和反序列化的Demo。但在此处隐藏一个问题:如果消息的生产者和消息的消费者所参考的类(Person类)有差异,会出现何种神奇事件?比如:消息生产者中的Person类增加了一个年龄属性,而消费者没有增加该属性。为什么没有增加?!因为这是个分布式部署的应用,你甚至都不知道这个应用部署在何处,特别是通过广播(broadcast)方式发送消息的情况,漏掉一两个订阅者也是很正常的。
在这种序列化和反序列化的类不一致的情况下,反序列化时会报一个InvalidClassException异常,原因时序列化和反序列化所对应的类版本发生了变化,JVM不能把数据流转换为实例对象。接着刨根问底:JVM到底时根据什么来判断一个类的版本的呢?
是通过SerialVersionUID,也叫做流标识符,即类的版本定义的,它可以显式声明也可以隐式声明。显式声明格式如下:
private static final long serialVersionUID = xxxxxL;
而隐式声明则是我不声明,你编译器在编译的时候帮我生成。生成的依据是通过包名、类名、继承关系、非私有的方法和属性,以及参数,返回值等诸多因子计算得出的,极度复杂,基本上计算出来的这个值是唯一的。
JVM在反序列化时,会比较数据流中的serialVersionUID 与类的 serialVersionUID 是否相同,如果相同,则认为类没有发生改变,可以把数据流load为实例对象;如果不同,就会抛出异常。这是一个非常好的校验机制,可以完美的保证一个对象即使在磁盘或者网络中“滚过”一次,仍能做到“出淤泥而不染”,完美的实现类的一致性。
但是,有时候我们需要一点特殊场景,例如:我的类改变不大,JVM是否可以把我们以前的对象反序列化过来?就是依靠显式声明serialVersionUID,向JVM撒谎说“我的类版本没有变更”,如此,我们编写的类就实现了向上兼容。我们修改一下上面的Person类,代码如下:
public class Person implemnets Serializabel {
private static final long serialVersionUID = 55799L;
/*其他保持不变*/
}
刚开始生产者和消费者持有的Person类版本一致,都是V1.0,某天生产者的Person类版本变更了,增加了一个“年龄”属性,升级为V2.0,而由于种种原因(比如程序员疏忽、升级时间窗口不同等)消费端的Person还保持为V1.0的版本,此时虽然生产者和消费者对应的类版本不同,但是显式声明的serialVersionUID相同,反序列化也是可以运行的,所带来的业务问题就是消费端不能读取到新增的业务属性(age属性)而已。
通过此例,我们的反序列化实现了版本向上兼容的功能,使用V1.0版本的应用访问了一个V2.0版本的对象,这无疑提高了代码的健壮性。我们在编写序列化代码时,随手加上serialVersionUID字段,也不会给我们带来太多的工作量,但它却可以在关键时候发挥异乎寻常的作用。