编写高质量代码:Java程序的建议(一)

此文为阅读《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,最后返回临时变量区的值。陈小古第一次循环时的详细处理步骤如下:

  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此的循环也一样,所以最终为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字段,也不会给我们带来太多的工作量,但它却可以在关键时候发挥异乎寻常的作用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目:使用 JavaScript 编写的杀死幽灵游戏(附源代码) 杀死鬼魂游戏是使用 Vanilla JavaScript、CSS 和 HTML 画布开发的简单项目。这款游戏很有趣。玩家必须触摸/杀死游荡的鬼魂才能得分。您必须将鼠标悬停在鬼魂上 - 尽量得分。鬼魂在眨眼间不断从一个地方移动到另一个地方。您必须在 1 分钟内尽可能多地杀死鬼魂。 游戏制作 这个游戏项目只是用 HTML 画布、CSS 和 JavaScript 编写的。说到这个游戏的特点,用户必须触摸/杀死游荡的幽灵才能得分。游戏会根据你杀死的幽灵数量来记录你的总分。你必须将鼠标悬停在幽灵上——尽量得分。你必须在 1 分钟内尽可能多地杀死幽灵。游戏还会显示最排名分数,如果你成功击败它,该分数会在游戏结束屏幕上更新。 该游戏包含大量的 javascript 以确保游戏正常运行。 如何运行该项目? 要运行此游戏,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox。要玩游戏,首先,单击 index.html 文件在浏览器中打开游戏。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
javascript 中的 Paint War Game 是使用 HTML、CSS 和 JavaScript 开发的。谈到游戏玩法,这款游戏的主要目标是建造比敌人更多的油漆砖。您所要做的就是使用 WASD 键输入玩家的动作。您可以使用 VS Code 来运行该项目。 关于项目 每次您的玩家走过一块瓷砖时,它都会被涂成您的团队颜色。您必须在同一块瓷砖上走 4 次才能获得更多游戏点数。瓷砖会被您的团队挡住,并且不能再被偷走。如果您走过另一支球队的瓷砖,它会像您第一次走过时一样被涂上颜色。如果您创建一个封闭的被阻挡瓷砖图形,图形内所有未被阻挡的瓷砖都将固定为您的团队颜色。这个游戏充满乐趣,创造和重新即兴发挥会更有趣。 要运行此项目,我们建议您使用现代浏览器,例如 Google Chrome、  Mozilla Firefox。该游戏可能还支持 Explorer/Microsoft Edge。 演示: javascript 中的 Paint War Game 是使用 HTML、CSS 和 JavaScript 开发的。谈到游戏玩法,这款游戏的主要目标是建造比敌人更多的油漆砖。您所要做的就是使用 WASD 键输入玩家的动作。您可以使用 VS Code 来运行该项目。 关于项目 每次您的玩家走过一块瓷砖时,它都会被涂成您的团队颜色。您必须在同一块瓷砖上走 4 次才能获得更多游戏点数。瓷砖会被您的团队挡住,并且不能再被偷走。如果您走过另一支球队的瓷砖,它会像您第一次走过时一样被涂上颜色。如果您创建一个封闭的被阻挡瓷砖图形,图形内所有未被阻挡的瓷砖都将固定为您的团队颜色。这个游戏充满乐趣,创造和重新即兴发挥会更有趣。 要运行此项目,我们建议您使用现代浏览器,例如 Google Chrome、  Mozilla Firefox。该游戏可能还支持 Explorer/Microsoft Edge。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值