编写高质量代码(从入坑到出坑)

第一坑:三元操作符的类型务必一致

    三元操作符是if-else的简化写法,在项目中使用它的地方很多,也非常好用,但是好用
又简单的东西并不表示就可以随便用,我们来看看下面这段代码:
public class Client i
    public static void main(String[] args){
                int i=80;
              String s = String.valueOf(i<100?90;100);
              String s1 = String.valueOf(i<l00?90;100.0);
            System.out.println(“两者足否相等;".s.equals(s1));
    }

    分析一下这段程序:i是80,那它当然小干100,两者的返回值肯定都是90,再转成
String类型,其值也绝对相等.毋庸置疑的。恩,分析得有点道理,但是变量s中三元操作
符的第二个操作数是100,而s1的第二个操作数是100.0,难道没有影响吗?不可能有影响
吧,三元操作符的条件都为真了,只返回第一个值嘛,与第二个值有一毛钱的关系吗?貌似
有道理。
    果真如此吗?我们通过结果来验证一下,运行结果是:“两者是否相等: false",什么?
不相等,Why?

    问题就出在了100和100.0这两个数字上,在变量s中,三元操作符中的第一个操作数
(90)和第二个操作数(100)都是int类型,类型相同,返回的结果也就是int类型的90,
而变量s1的情况就有点不同了,第一个操作数是90 ( int类型),第二个操作数却是100.0,
而这是个浮点数,也就是说两个操作数的类型不一致,可三元操作符必须要返回一个数据,

而且类型要确定,不可能条件为真时返回int类型,条件为假时返回float类型,编译器是不
允许如此的,所以它就会进行类型转换了,int型转换为浮点数90.0,也就是说三元操作符的
返回值是浮点数90.0,那这当然与整型的90不相等了。这里可能有读者疑感了:为什么是
整型转为浮点,而不是浮点转为整型呢?这就涉及三元操作符类型的转换规则:
    1:若两个操作数不可转换,则不做转换,返回值为Object类型。
    2:若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换.
      int类型转换为long类型,long类型转换为float类型等。
    3:若两个操作数中有一个是数字S,另外一个是表达式,且其类型标示为T,那么.若
      数字S在T的范围内,则转换为T类型;若S超出了T类型的范围,则T转换为S
      类型(可以参考“建议22",会对该问题进行展开描述).
    4:若两个操作数都是直接量数字(Literal) ,则返回值类型为范围较大者.
    知道是什么原因了,相应的解决办法也就有了:保证三元操作符中的两个操作数类型一
致,即可减少可能错误的发生。

第二坑:警惕自增的陷阱

记得大学刚开始学C语言时,老师就说:自增有两种形式,分别是i++和++i, i++表

示的是先赋值后加1, ++i是先加1后赋值,这样理解了很多年也没出现向题,直到遇到如下
代码,我才怀疑我的理解是不是错了:

public class Client{
      public static void main(String[] args)
int count =0;
for(int i=0;1<10;1++){
            count=count++;
}
Syetem.out.println("count-"+count);
}
}

    这个程序输出的count等于几?是count自加10次吗?答案等于10?可以非常肯定地
告诉你,答案错误!运行结果是count等于0.为什么呢?
    count++是一个表达式,是有返回值的,它的返回值就是count自加前的值,Java对自
加是这样处理的:首先把count的值(注意是值,不是引用)拷贝到一个临时变量区,然后
对count变量加1,最后返回临时变量区的值。程序第一次循环时的详细处理步骤如下:
    步骤1  JVM把count值(其值是0)拷贝到临时变量区。
    步骤2  count值加1,这时候count的值是1。
    步骤3 返回临时变量区的值.注意这个值是0,没修改过。
    步骤4 返回值赋值给count,此时count值被重置成0。

count=count++”这条语句可以按照如下代码来理解:

public static int mockAdd(int count){
      //充保存初始值
      int temp -count;
      //做自增操作
      count=count+1;
      //返田初始值
      return temp;
}

    于是第一次循环后count的值还是0,其他9次的循环也是一样的,最终你会发现count
的值始终没有改变,仍然保持着最初的状态。
    此例中代码作者的本意是希望count自增,所以想当然地认为斌值给自身就成了,不
曾想掉到Java自增的陷阱中了.解决方法很简单,只要把“count=count++"修改为
"count++”即可。该问题在不同的语言环境有不同的实现:C++中“count=count++”与
"count++”是等效的,而在PHP中则保持着与Java相同的处理方式.每种语言对自增的实
现方式各不同,读者有兴趣可以多找几种语言测试一下,思考一下原理。
    下次如果看到某人T恤上印着“i=i++”,千万不要鄙视他,记住,能够以不同的语言解
释清楚这句话的人绝对不简单,应该表现出“如滔滔江水”般的敬仰,心理默念清“高人,绝世高人“。

第三坑:避免用序列化类在构造函数中为不变量赋值

    我们知道带有final标识的属性是不变量,也就是说只能赋值一次,不能重复赋值,但
是在序列化类中就有点复杂了,比如有这样一个类:

public class Person implements Sericlizable{

    private static final long serialVersionUID = 71282334L;

   //不变量

   public final String name = ""混世魔王";

}

    这个Person类(此时v1.0版本)被序列化,然后存储在磁盘上,在反序列化时name
属性会重新计算其值(这与static变量不同,static变量压根就没有保存到数据流中),比如
name属性修改成了“德天使”(版本升级为V2.0),那么反序列化对象的name值就是“德
天使”。保持新旧对象的final变量相同,有利于代码业务逻辑统一,这是序列化的基本规则
之一,也就是说,如果final属性是一个直接量,在反序列化时就会重新计算。对这基本规则
不多说,我们要说的是final变量另外一种赋值方式:通过构造函数赋值。代码如下:

这也是我们常用的一种赋值方式,可以把这个Person类定义为版本V1.0,然后进行序
列化,看看有什么问题没有,序列化的代码如下所示:

    Person的实例对象保存到了磁盘上.它是一个贫血对象(承载业务属性定义,但不
包含其行为定义),我们做一个简单的模拟,修改一下name值代表变更,要注意的是

serialVersionUID保持不变,修改之后的代码如下:

    此时Person类的版本是V2.0,但serialVersionUlD没有改变,仍然可以反序列化,其代
码如下:

    现在问题来了:打印的结果是什么?是混世魔王还是德天使?
    答案即将揭晓,答案是:混世魔王。
    final类型的变量不是会重新计算吗?答案应该是“德天使”才对啊.为什么会是“混世
魔王”?这是因为这里触及了反序列化的另一个规则:反序列化时构造函数不会执行。
    反序列化的执行过程是这样的:JVM从数据流中获取一个Object对象,然后根据数据
流中的类文件描述信息(在序列化时,保存到磁盘的对象文件中包含了类描述信息,注意
是类描述信息,不是类)查看,发现是final变量,需要重新计算,于是引用Person类中的
name值,而此时JVM又发现name竟然没有赋值,不能引用,于是它很“聪明”地不再初
始化,保持原值状态,所以结果就是“混世魔王"了。
    读者不要以为这样的情况很少发生,如果使用Java开发过桌面应用,特别是参与过
对性能要求较高的项目(比如交易类项目),那么很容易遇到这样的问题。比如一个C/S
结构的在线外汇交易系统,要求提供24小时的联机服务,如果在升级的类中有一个final
变最是构造函数赋值的,而且新旧版本还发生了变化,则在应用请求热切的过程中(非
常短暂,可能只有30秒),很可能就会出现反序列化生成的final变量值与新产生的实例
值不相同的情况,于是业务异常就产生了,情况严重的话甚至会影响交易数据,那可是
天大的事故了。

注意在序列化类中,不使用构造函数为final变量赋值。

摘录自:《编写高质量代码:改善Java程序的152个建议》

猜你喜欢

转载自blog.csdn.net/weixin_42102798/article/details/82427144