编写高质量代码:改善Java程序的151个建议 | 第一章 Java开发中通用的方法和准则

编写高质量代码:改善Java程序的151个建议

第一章 Java开发中通用的方法和准则

建议1:不要在常量和变量中出现易混淆的字母
  • i、l、1混用;o、0混用等

    public static void main(String[] args) {
            long i = 1l;
            System.out.println("i的两倍是: " + (i + i));
        }
    

    此情此景就有可能会出现。所以,为了让您的程序更容易理解,字母“I”(还包括大写字母“O")尽量不要和数字混用,以免使阅读者的理解与程序意图产生偏差。如果字母和数字必须混合使用,字母“1”务必大写,字母“O”则增加注释。

建议2:莫让常量蜕变成变量
  • 代码在运行过程中不要去改变常量值

    public class Suggest2 {
    
        public static void main(String[] args) {
            System.out.println("常量会变噢: " + Const.RAND_CONST);
        }
    }
    
    public interface Const {
    
        public static final int RAND_CONST = new Random().nextInt();
    
    }
    

    这种常量的定义方式是极不可取的,常量就是常量,在编译期就必须确定其值,不应该在运行期更改,否则程序的可读性会非常差,甚至连作者自己都不能确定在运行期发生了何种神奇的事情。

建议3:三元操作符的类型务必一致
  • 不一致会导致自动类型转换,类型提升int->float->double等。需要保证三元操作符的两个操作数类型一致, 即可减少可能错误的发生。

    public class Suggest3 {
    
        public static void main(String[] args) {
            int i = 80;
            String s1 = String.valueOf(i < 100 ? 90 : 100);
            String s2 = String.valueOf(i < 100 ? 90 : 100.0);
            System.out.println("两者是否相等: " + s1.equals(s2));
        }
    }
    

    三元操作符是 if - else 的简化写法, 在项目中使用它的地方很多, 但是好用又简单的东西并不表示就可以随便用。

    三元操作符类型的转换规则:

    • 若两个操作数不可转换,则不做转换,返回值为Object类型。
    • 若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换,int类型转换为long类型,long 类型转换为float类型等。
    • 若两个操作数中有一个是数字S,另外一个是表达式,且其类型标示为T,那么,若数字S在T的范围内,则转换为T类型﹔若S超出了T类型的范围,则T转换为S类型(可以参考“建议22”,会对该问题进行展开描述)。
    • 若两个操作数不可转换,则不做转换,返回值为Object类型。
      若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换,int类型转换为long类型,long 类型转换为float类型等。
    • 若两个操作数中有一个是数字S,另外一个是表达式,且其类型标示为T,那么,若数字S在T的范围内,则转换为T类型﹔若S超出了T类型的范围,则T转换为S类型(可以参考“建议22”,会对该问题进行展开描述)。
      若两个操作数都是直接量数字(Literal) 9,则返回值类型为范围较大者。
      保证三元操作符中的两个操作数类型一致,即可减少可能错误的发生。
建议4:避免带有变长参数的方法重载
  • 变长参数的方法重载之后可能会包含原方法

    public class Suggest4 {
    
        // 简单折扣计算
        public void calPrice(int price, int discount) {
            float knockdownPrice = price * discount / 100.0F;
            System.out.println("简单折扣后的价格是: " + formatCurrency(knockdownPrice));
        }
        // 复杂多折扣计算
        public void calPrice(int price, int... discounts) {
            float knockdownPrice = price;
            for (int discount : discounts) {
                knockdownPrice = price * discount / 100;
            }
            System.out.println("复杂折扣后的价格是: " + formatCurrency(knockdownPrice));
        }
    
        private String formatCurrency(float price) {
            return NumberFormat.getCurrencyInstance().format(price / 100);
        }
    
        public static void main(String[] args) {
            Suggest4 suggest4 = new Suggest4();
            suggest4.calPrice(49900, 75);
        }
    }
    
    

    Java 5引入变长参数( varags)就是为了更好垃提高方法的复用性,让方法的调用者可以“随心所欲”地传递实参数量,当然变长参数也是要遵循一定规则的,

    比如变长参数必须是方法中的最后一个参数: 一个方法不能定义多个型长参数等,这些基本规则需要牢记,但是即使记住了这些规则,仍然有可能出现错误。

    因为Java在编译时,首先会根据实参的数量和类型(这里是2个实参,都为int类型,注意没有转成int数组〉来进行处理,也就是查找到calPrice(int price,int discount)方法,而且确认它是否符合方法签名条件。

    现在的问题是编译器为什么会首先根据2个int类型的实参而不是1个int类型、1个int数组类型的实参来查找方法呢﹖这是个好问题,也非常好回答:

    因为int是一个原生数据类型,而数组本身是一个对象,编译器想要“偷懒”,于是它会从最简单的开始“猜想”,只要符合编译条件的即可通过,于是就出现了此问题。

建议5:别让null值和空值威胁到变长方法
  • 两个都包含变长参数的重载方法,当变长参数部分空值,或者为null值时,重载方法不清楚会调用哪一个方法

    public class Suggest5 {
    
        public void methodA(String str, Integer... is){
            
        }
    
        public void methodA(String str, String... strs){
    
        }
        
        public static void main(String[] args) {
                    Suggest5 suggest5 = new Suggest5();
            suggest5.methodA("Rocky编程日记",0);
            suggest5.methodA("Rocky编程日记","Rocky");
            // suggest5.methodA("Rocky编程日记");
            // suggest5.methodA("Rocky编程日记",null);
        }
    }
    

    有两处编译通不过:

    client.Method A(“中国”)和client.Method A(中国,null),估计你已经猜到了,两处的提示是相同的:方法模糊不清,编译器不知道调用哪一个方法,但这两处代码反映的代码味道可是不同的。

    第一个不符合懒人原则外,第二个是调用者隐藏了实参类型,这是非常危险的,不仅仅调用者需要“猜测”该调用哪个方法,而且被调用者也可能产生内部逻辑混乱的情况。对于本例来说应该做如下修改:

    public static void main(String[] args) {
            Suggest5 suggest5 = new Suggest5();
            suggest5.methodA("Rocky编程日记",0);
            suggest5.methodA("Rocky编程日记","Rocky");
            // suggest5.methodA("Rocky编程日记");
            // suggest5.methodA("Rocky编程日记",null);
            String[] s = null;
            suggest5.methodA("Rocky编程日记", s);
        }
    
建议6:覆写变长方法也循规蹈矩
  • 变长参数与数组,覆写的方法参数与父类相同,不仅仅是类型、数量,还包括显示形式

    public class Suggest6 {
    
        public static void main(String[] args) {
            // 向上转型
            Base base = new Sub();
            base.fun(100, 50);
            // 不转型
            Sub sub = new Sub();
    //        sub.fun(100, 50);
        }
        // 基类
        static class Base {
            void fun(int price, int... discounts) {
                System.out.println("Base.....fun");
            }
        }
        // 子类, 覆写父类方法
        static class Sub extends Base {
    
            @Override
            void fun(int price, int[] discounts) {
                System.out.println("Sub....fun");
            }
        }
    }
    

    注意看注释的内容是有问题的。

    事实上,base对象是把子类对象Sub做了向上转型,形参列表是由父类决定的,由于是变长参数,在编译时,“base.fun(100,50)”中的“50”这个实参会被编译器“猜测”而编译成“{50}”数组,再由子类Sub执行。

    我们再来看看直接调用子类的情况,这时编译器并不会把“50”做类型转换,因为数组本身也是一个对象,编译器还没有聪明到要在两个没有继承关系的类之间做转换,要知道Java是要求严格的类型匹配的,类型不匹配编译器自然就会拒绝执行,并给予错误提示。

建议7:警惕自增的陷阱
  • count=count++;操作时JVM首先将count原值拷贝到临时变量区,再执行count加1,之后再将临时变量区的值赋给count,所以count一直为0或者某个初始值。C++中count=count++;与count++等效,而PHP与Java类似

    public class Suggest7 {
        public static void main(String[] args) {
            int count = 0;
            for (int i = 0; i < 10; i++) {
                count = count++;
            }
            System.out.println("count== " + count);
        }
    }
    
建议8:不要让旧语法困扰你
  • Java中抛弃了C语言中的 goto 语法,但是还保留了该关键字,只是不进行语义处理,const 关键字同样类似。

    public class Suggest8 {
        public static void main(String[] args) {
            // 数据定义及初始化
            int fee = 200;
            // 其他业务处理
            saveDefault:save(fee);
        }
    
        static void saveDefault() {}
    
        static void save(int fee) {}
    }
    

    Java中虽然没有了goto关键字,但是扩展了break和continue关键字,它们的后面都可以加上标号做跳转,完全实现了goto功能,同时也把goto的诟病带了进来,所以我们在阅读大牛的开源程序时,根本就看不到 break或continue后跟标号的情况,甚至是break和continue都很少看到,这是提高代码可读性的一剂良药,旧语法就让它随风而去吧!

建议9:少用静态导入
  • Java5引入的静态导入语法import static,使用静态导入可以减少程序字符输入量,但是会带来很多代码歧义,省略的类约束太少,显得程序晦涩难懂。

    import java.text.NumberFormat;
    
    import static java.lang.Math.PI;
    import static java.lang.Double.*;
    import static java.lang.Integer.*;
    import static java.text.NumberFormat.*;
    
    public class Suggest9 {
    
        // 计算圆面积
        public static double calCircleArea(double r) {
            return Math.PI * r * r;
        }
    
        // 计算圆面积(静态导入的作用是把Math类中的PI常量引入到本地中)
        public static double calCircleArea1(double r) {
            return PI * r * r;
        }
    
        // 计算球面积
        public static double calBallArea(double r) {
            return 4 * PI * r * r;
        }
    
        // 计算球面积
        public static double calBallArea1(double r) {
            return 4 * Math.PI * r * r;
        }
    
        public static void main(String[] args) {
            double s = PI * parseDouble("1");
            NumberFormat numberFormat = getInstance();
            numberFormat.setMaximumFractionDigits(parseInt("2"));
            formatMessage(numberFormat.format(s));
        }
        // 格式化消息输出
        private static void formatMessage(String ans) {
            System.out.println("圆面积是: " + ans);
        }
    }
    

    对于静态导入, 一定要遵循两个规则:

    • 不使用 *(星号) 通配符, 除非是导入静态常量类(只包含常量的类或接口)。
    • 方法名是具有明确、清晰表象意义的工具类。
建议10:不要在本类中覆盖静态导入的变量和方法
  • 例如静态导入Math包下的PI常量,类属性中又定义了一个同样名字PI的常量。编译器的“最短路径原则”将会选择使用本类中的PI常量。本类中的属性,方法优先。如果要变更一个被静态导入的方法,最好的办法是在原始类中重构,而不是在本类中覆盖

    import static java.lang.Math.PI;
    import static java.lang.Math.abs;
    
    public class Suggest10 {
        public static void main(String[] args) {
            demo1();
            demo2();
        }
    
        private static void demo1() {
            System.out.println("PI= " + PI);
            System.out.println("abs(100)=" + abs(100));
        }
    
        public static void demo2() {
    
            final   String PI = "祖冲之";
    
            System.out.println("PI= " + PI);
            System.out.println("abs(100)=" + abs1(100));
        }
    	// 可改成abs 调试
        public static int abs1(int abs) {
            return 0;
        }
    }
    
    
建议11:养成良好的习惯,显式声明UID
  • 显式声明serialVersionUID可以避免序列化和反序列化中对象不一致,JVM根据serialVersionUID来判断类是否发生改变。隐式声明由编译器在编译的时候根据包名、类名、继承关系等诸多因子计算得出,极其复杂,算出的值基本唯一。

    
    
建议12:避免用序列化类在构造函数中为不变量赋值
  • 在序列化类中,不适用构造函数为final变量赋值)(**序列化规则1:**如果final属性是一个直接量,在反序列化时就会重新计算;**序列化规则2:**反序列化时构造函数不会执行;**反序列化执行过程:**JVM从数据流中获取一个Object对象,然后根据数据流中的类文件描述信息(在序列化时,保存到磁盘的对象文件中包含了类描述信息,不是类)查看,发现是final变量,需要重新计算,于是引用Person类中的name值,而此时JVM又发现name没有赋值(因为反序列化时构造函数不会执行),不能引用,于是它不再初始化,保持原始值状态。整个过程中需要保持serialVersionUID相同
建议13:避免为final变量复杂赋值
  • 类序列化保存到磁盘上(或网络传输)的对象文件包括两部分:1、类描述信息:包括包路径、继承关系等。注意,它并不是class文件的翻版,不记录方法、构造函数、static变量等的具体实现。2、非瞬态(transient关键字)和非静态(static关键字)的实例变量值。总结:反序列化时final变量在以下情况下不会被重新赋值:1、通过构造函数为final变量赋值;2、通过方法返回值为final变量赋值;3、final修饰的属性不是基本类型
建议14:使用序列化类的私有方法巧妙解决“部分属性持久化问题”
  • 部分属性持久化问题解决方案1:把不需要持久化的属性加上瞬态关键字(transient关键字)即可,但是会使该类失去了分布式部署的功能。方案2:新增业务对象。方案3:请求端过滤。方案4:变更传输契约,即覆写writeObject和readObject私有方法,在两个私有方法体内完成部分属性持久化
建议15:break万万不可忘
  • switch语句中,每一个case匹配完都需要使用break关键字跳出,否则会依次执行完所有的case内容。

    public class Suggest15 {
        public static void main(String[] args) {
            System.out.println("2 = " + toChineseNumberCase(2));
        }
    
        private static String toChineseNumberCase(int value) {
            String chineseNumberCase ="";
            switch (value) {
                case 0: chineseNumberCase = "零";
                case 1: chineseNumberCase = "壹";
                case 2: chineseNumberCase = "贰";
                case 3: chineseNumberCase = "叁";
                case 4: chineseNumberCase = "肆";
                case 5: chineseNumberCase = "伍";
                case 6: chineseNumberCase = "陆";
                case 7: chineseNumberCase = "柒";
                case 8: chineseNumberCase = "捌";
                case 9: chineseNumberCase = "玖";
            }
            return chineseNumberCase;
        }
    }
    

    对于此类问题最简单的解决办法: 修改IDE的警告级别

建议16:易变业务使用脚本语言编写
  • 脚本语言:都是在运行期解释执行。脚本语言三大特性:

    1、灵活:动态类型;

    2、便捷:解释型语言,不需要编译成二进制,不需要像Java一样生成字节码,依靠解释执行,做到不停止应用变更代码

    3、简单:部分简单。Java使用ScriptEngine执行引擎来执行JavaScript脚本代码

建议17:慎用动态编译
  • 好处:更加自如地控制编译过程。很少使用,原因:静态编译能够完成大部分工作甚至全部,即使需要使用,也有很好的替代方案,如JRuby、Groovy等无缝的脚本语言。动态编译注意以下4点:
    • 1、在框架中谨慎使用:debug困难,成本大;
    • 2、不要在要求高性能的项目中使用:需要一个编译过程,比静态编译多了一个执行环节;
    • 3、动态编译要考虑安全问题:防止恶意代码;
    • 4、记录动态编译过程
建议18:避免instanceof非预期结果
  • instanceof用来判断一个对象是否是一个类的实例,只能用于对象的判断,不能用于基本类型的判断(编译不通过),instanceof操作符的左右操作数必须有继承或实现关系,否则编译会失败。

    例:null instanceof String返回值是false,instanceof特有规则,若左操作数是null,结果就直接返回false,不再运算右操作数是什么类

    public class Suggest18 {
        public static void main(String[] args) {
            // “String”是一个字符串,字符串又继承了对象,那当然是返回true了。
            boolean b1 = "String" instanceof Object;
            // 类对象就是它的实例
            boolean b2 = new String() instanceof String;
            // Object 是父类, 其对象当然不是String类的实例
            boolean b3 = new Object() instanceof String;
            // 编译失败 instance 只能用于对象的判断,不能用于基本类型的判断
            // boolean b4 = 'A' instanceof Character;
            // 左边是false ,直接返回 false
            boolean b5 = null instanceof String;
            // 即使做类型转换也还是 null
            boolean b6 = (String) null instanceof String;
            // Date 和 String 没有继承或者实现关系, 编译通不过
            // boolean b7 = new Date() instanceof String;
            // 编译通过, T 是 Object, "t instanceof Date" -> "Object instanceof Date"
            boolean b8 = new GenericClass<String>().isDateInstance("");
            System.out.println("b1 " + b1);
            System.out.println("b2 " + b2);
            System.out.println("b3 " + b3);
            System.out.println("b5 " + b5);
            System.out.println("b6 " + b6);
            System.out.println("b8 " + b8);
        }
    
        static class GenericClass<T> {
            // 判断是否是Date类型
            public boolean isDateInstance(T t) {
                return t instanceof Date;
            }
        }
    }
    
    
    b1 true
    b2 true
    b3 false
    b5 false
    b6 false
    b8 false
    
建议19:断言绝对不是鸡肋
  • 防御式编程中经常使用断言(Assertion)对参数和环境做出判断。断言是为调试程序服务的。

    两个特性:

    1. 默认assert不启用;
    2. assert抛出的异常AssertionError是继承自Error的

    assert虽然是做断言的,但不能将其等价于if…else…这样的条件判断,它在以下两种情况不可使用 :

    1. 在对外公开的方法中

      我们知道防御式编程最核心的一点就是:所有的外部因素(输入参数、环境变量、上下文)都是“邪恶”的,都存在着企图摧毁程序的罪恶本源,为了抵制它,我们要在程序中处处检验,满地设卡,不满足条件就不再执行后续程序,以保护主程序的正确性,处处设卡没问题,但就是不能用断言做输入校验,特别是公开方法。我们来看一个例子:

      public class Suggest19 {
          public static void main(String[] args) {
              StringUtils.encode(null);
          }
      
      }
      class StringUtils {
          public static String encode(String str) {
              assert str != null : "加密的字符串为null";
              return str;
          }
      }
      

      encode方法对输人参数做了不为空的假设,如果为空,则抛出AssertionError错误,但这段程序存在一个严重的问题,

      encode是一个public方法,这标志着是它对外公开的,任何一个类只要能够传递一个String类型的参数(遵守契约)就可以调用,

      但是Client类按照规范和契约调用enocde方法,却获得了一个AssertionError错误信息,是谁破坏了契约协定?—一是encode方法自己。

    2. 在执行逻辑代码的情况下

      assert 的支持是可选的,在开发时可以让它运行, 但在生产系统中则不需要其运行了(以便提高性能), 因此在 assert的布尔表达式中不能执行逻辑代码, 否则会因为环境不同而产生不同的逻辑。

建议20:不要只替换一个类
  • 发布应用系统时禁止使用类文件替换方式,整体WAR包发布才是完全之策)(Client类中调用了Constant类中的属性值,如果更改了Constant常量类属性的值,重新编译替换。

    而不改变或者替换Client类,则Client中调用的Constant常量类的属性值并不会改变。

    **原因:**对于final修饰的基本类型和String类型,编译器会认为它是稳定态(Immutable Status),所以在编译时就直接把值编译到字节码中了,避免了再运行期引用,以提高代码的执行效率。而对于final修饰的类(即非基本类型),编译器认为它是不稳定态(Mutable Status),在编译时建立的则是引用关系(该类型也叫作Soft Final),如果Client类引入的常量是一个类或实例,即使不重新编译也会输出最新值

    class Constant {
        public final static int MAX_AGE = 150;
    }
    
    public class Suggest20 {
    
        public static void main(String[] args) {
            System.out.println("人类寿命权限是: " + Constant.MAX_AGE);
        }
    }
    
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值