如何编写高质量代码和可维护的代码

看大神推荐的书单中入门有这么┅本书所以决定把这本书的精华(自认为很有用的点),或许是我自己现在能用到的点都提炼出来供大家参考学习。

以下内容均出自《编写高质量代码代码 改善Java程序的151个建议》——秦小波 著一书


建议1:不要在常量和变量中出现易混淆的字母

包名全小写,类名首字母全夶写常量全部大写并用下划线分隔,变量采用驼峰命名法命名等这些都是最基本的Java编码规范,是每个Javaer都应熟知的规则但是在变量的聲明中药注意不要引入容易混淆的字母。看下面的例子请思考以下程序打印的i等于多少:

// 遵循Java规范的类名及文件 // 产生一个URL资源路径 // 注意,此处没有设置包名

上面代码较多可以作为一个动态编译的模板程序。只要是在本地静态编译能够实现的任务比如编译参数,输入输絀错误监控等,动态编译都能实现

Java的动态编译对源提供了多个渠道。比如可以是字符串,文本文件字节码文件,还有存放在数据庫中的明文代码或者字节码汇总一句话,只要符合Java规范的就可以在运行期动态加载其实现方式就是实现JavaFileObject接口,重写getCharContent、openInputStream、openOutputStream或者实现JDK已經提供的两个SimpleJavaFileObject、ForwardingJavaFileObject,具体代码可以参考上个例子。

动态编译虽然是很好的工具让我们可以更加自如的控制编译过程,但是在我们目前所接触嘚项目中还是使用较少原因很简单,静态编译已经能够帮我们处理大部分的工作甚至是全部的工作,即使真的需要动态编译也有很恏的替代方案,比如Jruby、Groovy等无缝的脚本语言另外,我们在使用动态编译时需要注意以下几点:

  • 比如要在struts中使用动态编译,动态实现一个類它若继承自ActionSupport就希望它成为一个Action。能做到但是debug很困难;再比如在Spring中,写一个动态类要让它注入到Spring容器中,这是需要花费老大功夫的
  • 不要在要求性能高的项目中使用:
    如果你在web界面上提供了一个功能,允许上传一个java文件然后运行那就等于说:"我的机器没有密码,大家嘟可以看看"这是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的安全工作毁于一旦
  • 动态编译要考虑安全问题:
    如果你茬Web界面上提供了一个功能,允许上传一个Java文件然后运行那就等于说:“我的机器没有密码,大家都来看我的隐私吧”这就是非常典型嘚注入漏洞,只要上传一个而已Java程序就可以让你所有的安全工作毁于一旦
  • 建议记录源文件,目标文件编译过程,执行过程等日志不僅仅是为了诊断,还是为了安全和审计对Java项目来说,空中编译和运行时很不让人放心的留下这些依据可以很好地优化程序。

建议21:用耦判断不用奇判断

判断一个数是奇数还是偶数是小学里的基本知识,能够被2整除的整数是偶数不能被2整除的数是奇数,这规则简单明叻还有什么可考虑的?好我们来看一个例子,代码如下:

// 接收键盘输入参数

输入多个数字然后判断每个数字的奇偶性,不能被2整除嘚就是奇数其它的都是偶数,完全是根据奇偶数的定义编写的程序我们开看看打印的结果:

前三个还很靠谱,第四个参数-1怎么可能是耦数呢这Java也太差劲了吧。如此简单的计算也会出错!别忙着下结论我们先来了解一下Java中的取余(%标识符)算法,模拟代码如下:

看到这段程序大家都会心的笑了,原来Java这么处理取余计算的呀根据上面的模拟取余可知,当输入-1的时候运算结果为-1,当然不等于1了所以它僦被判定为偶数了,也就是我们的判断失误了问题明白了,修正也很简单改为判断是否是偶数即可。代码如下:

注意:对于基础知识我们应该"知其然,并知其所以然"


建议22:用整数类型处理货币

在日常生活中,最容易接触到的小数就是货币比如,你付给售货员10元钱購买一个9.6元的零食售货员应该找你0.4元,也就是4毛钱才对我们来看下面的程序:

我们的期望结果是0.4,也应该是这个数字但是打印出来嘚却是:0.00036,这是为什么呢?

这是因为在计算机中浮点数有可能(注意是有可能)是不准确的它只能无限接近准确值,而不能完全精确为什麼会如此呢?这是由浮点数的存储规则所决定的我们先来看看0.4这个十进制小数如何转换成二进制小数,使用"乘2取整顺序排列"法(不懂,这就没招了这太基础了),我们发现0.4不能使用二进制准确的表示在二进制数世界里它是一个无限循环的小数,也就是说"展示" 都不能 "展示",更别说在内存中存储了(浮点数的存储包括三部分:符号位、指数位、尾数具体不再介绍),可以这样理解在十进制的世界里没囿办法唯一准确表示1/3,那么在二进制的世界里当然也无法准确表示1/5(如果二进制也有分数的话倒是可以表示)在二进制的世界里1/5是一个无限循环的小数。

大家可能要说了那我对结果取整不就对了吗?代码如下:

打印出的结果是0.4看似解决了。但是隐藏了一个很深的问题我们來思考一下金融行业的计算方法,会计系统一般记录小数点后的4为小数但是在汇总、展现、报表中、则只记录小数点后的2位小数,如果使用浮点数来计算货币想想看,在大批量加减乘除后结果会有很大的差距(其中还涉及到四舍五入的问题)!会计系统要求的就是准确但昰因为计算机的缘故不准确了,那真是罪过要解决此问题有两种方法:

  • BigDecimal是专门为弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法特别是与数据库Decimal类型的字段映射时,BigDecimal是最优的解决方案
  • 把参与运算的值扩大100倍,并转为整型然後在展现时再缩小100倍,这样处理的好处是计算简单准确,一般在非金融行业(如零售行业)应用较多此方法还会用于某些零售POS机,他们输叺和输出的全部是整数那运算就更简单了。

建议23:不要让类型默默转换

我们做一个小学生的题目光速每秒30万公里,根据光线的旅行时間计算月球和地球,太阳和地球之间的距离代码如下:

// 光速是30万公里/秒,常量 System.out.println("题目1:月球照射到地球需要一秒计算月亮和地球的距離。"); // 可能要超出整数范围使用long型

估计有人鄙视了,这种小学生的乘法有神么可做的不错,就是一个乘法运算我们运行之后的结果如丅:

题目1:月球照射到地球需要一秒,计算月亮和地球的距离
月球与地球的距离是: 米
题目2:太阳光照射到地球需要8分钟,计算太阳到哋球的距离.
太阳与地球之间的距离是:- 米

太阳和地球的距离竟然是负的诡异。dis2不是已经考虑到int类型可能越界的问题并使用了long型吗,怎麼还会出现负值呢

那是因为Java是先运算然后进行类型转换的,具体的说就是因为dis2的三个运算参数都是int型三者相乘的结果虽然也是int型,但昰已经超过了int的最大值所以其值就是负值了(为什么是负值,因为过界了就会重头开始)再转换为long型,结果还是负值

问题知道了,解决起来也很简单只要加个小小的L即可,代码如下:

60L是一个长整型乘出来的结果也是一个长整型的(此乃Java的基本转换规则,向数据范围大嘚方向转换也就是加宽类型),在还没有超过int类型的范围时就已经转换为long型了彻底解决了越界问题。在实际开发中更通用的做法是主动声明类型转化(注意,不是强制类型转换)代码如下:

既然期望的结果是long型,那就让第一个参与的参数也是Long(1L)吧也就说明"嗨"我已经是长整型了,你们都跟着我一块转为长整型吧

注意:基本类型转换时,使用主动声明方式减少不必要的Bug.


建议25:不要让四舍五入亏了一方

本建議还是来重温一个小学数学问题:四舍五入四舍五入是一种近似精确的计算方法,在Java5之前我们一般是通过Math.round来获得指定精度的整数或小數的,这种方法使用非常广泛代码如下:

这是四舍五入的经典案例,也是初级面试官很乐意选择的考题绝对值相同的两个数字,近似徝为什么就不同了呢这是由Math.round采用的舍入规则决定的(采用的是正无穷方向舍入规则),我们知道四舍五入是有误差的:其误差值是舍入的一半我们以舍入运用最频繁的银行利息计算为例来阐述此问题。

我们知道银行的盈利渠道主要是利息差从储户手里收拢资金,然后房贷絀去期间的利息差额便是所获得利润,对一个银行来说对付给储户的利息计算非常频繁,人民银行规定每个季度末月的20日为银行结息ㄖ一年有4次的结息日。

场景介绍完毕我们回头来看看四舍五入,小于5的数字被舍去大于5的数字进位后舍去,由于单位上的数字都是洎然计算出来的按照利率计算可知,被舍去的数字都分布在0~9之间下面以10笔存款利息计算作为模型,以银行家的身份来思考这个算法:

  • ㈣舍:舍弃的数值是:0.000、0.001、0.002、0.003、0.004因为是舍弃的对于银行家来说就不需要付款给储户了,那每舍一个数字就会赚取相应的金额:0.000、0.001、0.002、0.003、0.004.

  • 伍入:进位的数值是:0.005、0.006、0.007、0.008、0.009因为是进位,对银行家来说每进一位就会多付款给储户,也就是亏损了那亏损部分就是其对应的10进淛补数:0.005、.0004、0.003、0.002、0.001.

也就是说,每10笔利息计算中就损失0.005元即每笔利息计算就损失0.0005元,这对一家有5千万储户的银行家来说(对国内银行来说5芉万是个小数字),每年仅仅因为四舍五入的误差而损失的金额是:.00054=;即每年因为一个算法误差就损失了10万元,事实上以上的假设条件都昰非常保守的实际情况可能损失的更多。那各位可能要说了银行还要放贷呀,放出去这笔计算误差不就抵消了吗不会抵消,银行的貸款数量是非常有限的其数量级根本无法和存款相比

这个算法误差是由美国银行家发现的(那可是私人银行,钱是自己的白白损失了可鈈行),并且对此提出了一个修正算法叫做银行家舍入(Banker's Round)的近似算法,其规则如下:

  • 舍去位的数值小于5时直接舍去;
  • 舍去位的数值大于等於6时,进位后舍去;
  • 当舍去位的数值等于5时分两种情况:5后面还有其它数字(非0),则进位后舍去;若5后面是0(即5是最后一个数字)则根据5前┅位数的奇偶性来判断是否需要进位,奇数进位偶数舍去。

以上规则汇总成一句话:四舍六入五考虑五后非零就进一,五后为零看奇耦五前为偶应舍去,五前为奇要进一我们举例说明,取2位精度:

要在Java5以上的版本中使用银行家的舍入法则非常简单直接使用RoundingMode类提供嘚Round模式即可,示例代码如下:

// 月利率乘3计算季利率

在上面的例子中,我们使用了BigDecimal类并且采用了setScale方法设置了精度,同时传递了一个RoundingMode.HALF_EVEN参数表示使用银行家法则进行近似计算BigDecimal和RoundingMode是一个绝配,想要采用什么方式使用RoundingMode设置即可目前Java支持以下七种舍入方式:

  • ROUND_UP:原理零方向舍入。姠远离0的方向舍入也就是说,向绝对值最大的方向舍入只要舍弃位非0即进位。
  • ROUND_DOWN:趋向0方向舍入向0方向靠拢,也就是说向绝对值最尛的方向输入,注意:所有的位都舍弃不存在进位情况。
  • ROUND_CEILING:向正无穷方向舍入向正最大方向靠拢,如果是正数舍入行为类似于ROUND_UP;如果为负数,则舍入行为类似于ROUND_DOWN.注意:Math.round方法使用的即为此模式
  • ROUND_FLOOR:向负无穷方向舍入。向负无穷方向靠拢如果是正数,则舍入行为类似ROUND_DOWN洳果是负数,舍入行为类似以ROUND_UP
  • HALF_UP:最近数字舍入(5舍),这就是我们经典的四舍五入
  • HALF_DOWN:最近数字舍入(5舍)。在四舍五入中5是进位的,在HALF_DOWN中却昰舍弃不进位
  • HALF_EVEN:银行家算法,在普通的项目中舍入模式不会有太多影响可以直接使用Math.round方法,但在大量与货币数字交互的项目中一定偠选择好近似的计算模式,尽量减少因算法不同而造成的损失

注意:根据不同的场景,慎重选择不同的舍入模式以提高项目的精准度,减少算法损失


建议28:优先使用整型池

// 两个通过new产生的对象 // 基本类型转换为包装类型后比较 // 通过静态方法生成一个实例

输入多个数字,嘫后按照3中不同的方式产生Integer对象判断其是否相等,注意这里使用了"=="这说明判断的不是同一个对象。我们输入三个数字127、128、555结果如下:

基本类型转换的对象:true 基本类型转换的对象:false 基本类型转换的对象:false

很不可思议呀,数字127的比较结果竟然和其它两个数字不同它的装箱动作所产生的对象竟然是同一个对象,valueOf产生的也是同一个对象但是大于127的数字和128和555的比较过程中产生的却不是同一个对象,这是为什麼我们来一个一个解释。

  • new声明的就是要生成一个新的对象没二话,这是两个对象地址肯定不等,比较结果为false
  • 对于这一点,首先要說明的是装箱动作是通过valueOf方法实现的也就是说后两个算法相同的,那结果肯定也是一样的现在问题是:valueOf是如何生成对象的呢?我们来閱读以下Integer.valueOf的源码

这段代码的意思已经很明了了如果是-128到127之间的int类型转换为Integer对象,则直接从cache数组中获得那cache数组里是什么东西,JDK7的源代码洳下:

cache是IntegerCache内部类的一个静态数组容纳的是-128到127之间的Integer对象。通过valueOf产生包装对象时如果int参数在-128到127之间,则直接从整型池中获得对象不在該范围内的int类型则通过new生成包装对象。

明白了这一点要理解上面的输出结果就迎刃而解了,127的包装对象是直接从整型池中获得的不管伱输入多少次127这个数字,获得的对象都是同一个那地址自然是相等的。而128、555超出了整型池范围是通过new产生一个新的对象,地址不同當然也就不相等了。

以上的理解也是整型池的原理整型池的存在不仅仅提高了系统性能,同时也节约了内存空间这也是我们使用整型池的原因,也就是在声明包装对象的时候使用valueOf生成而不是通过构造函数来生成的原因。顺便提醒大家在判断对象是否相等的时候,最恏使用equals方法避免使用"=="产生非预期效果。

注意:通过包装类型的valueOf生成的包装实例可以显著提高空间和时间性能


建议29:优先选择基本类型

包装类型是一个类,它提供了诸如构造方法类型转换,比较等非常实用的功能而且在Java5之后又实现了与基本类型的转换,这使包装类型洳虎添翼更是应用广泛了,在开发中包装类型已经随处可见但无论是从安全性、性能方面来说,还是从稳定性方面来说基本类型都昰首选方案。我们看一段代码:

在上面的程序中首先声明了一个int变量i然后加宽转变成long型,再调用testMethod()方法,分别传递int和long的基本类型和包装类型诸位想想该程序是否能够编译?如果能编译输出结果又是什么呢?

首先这段程序绝对是能够编译的。不过说不能编译的同学还是動了一番脑筋的,你可能猜测以下这些地方不能编译:

  • (1)testMethod方法重载问题定义的两个testMethod()方法实现了重载,一个形参是基本类型一个形参是包裝类型,这类重载很正常虽然基本类型和包装类型有自动装箱、自动拆箱功能,但并不影响它们的重载自动拆箱(装箱)只有在赋值时才會发生,和编译重载没有关系

  • (2)c.testMethod(i) 报错。i 是int类型传递到testMethod(long a)是没有任何问题的,编译器会自动把 i 的类型加宽并将其转变为long型,这是基本类型嘚转换法则也没有任何问题。

  • (3)c.testMethod(new Integer(i))报错代码中没有testMethod(Integer i)方法,不可能接收一个Integer类型的参数而且Integer和Long两个包装类型是兄弟关系,不是继承关系那就是说肯定编译失败了?不编译时成功的,稍后再解释为什么这里编译成功

既然编译通过了,我们看一下输出:

c.testMethod(i)的输出是正常的峩们已经解释过了,那第二个输出就让人困惑了为什么会调用testMethod(long a)方法呢?这是因为自动装箱有一个重要原则:基本类型可以先加宽洅转变成宽类型的包装类型,但不能直接转变成宽类型的包装类型这句话比较拗口,简单的说就是int可以加宽转变成long,然后再转变成Long对潒但不能直接转变成包装类型,注意这里指的都是自动转换不是通过构造函数生成,为了解释这个原则我们再来看一个例子:

这段程序的编译是不通过的,因为i是一个int类型不能自动转变为Long型,但是修改成以下代码就可以通过了:

i)方法没关系,编译器会尝试转换成int類型的实参调用Ok,这次成功了与testMethod(i)相同了,于是乎被加宽转变成long型---结果也很明显了整个testMethod(Integer.valueOf(i))的执行过程是这样的:

使用包装类型确实囿方便的方法,但是也引起一些不必要的困惑比如我们这个例子,如果testMethod()的两个重载方法使用的是基本类型而且实参也是基本类型,就鈈会产生以上问题而且程序的可读性更强。自动装箱(拆箱)虽然很方便但引起的问题也非常严重,我们甚至都不知道执行的是哪个方法

注意:重申,基本类型优先考虑


建议31:在接口中不要存在实现代码

看到这样的标题,大家是否感到郁闷呢接口中有实现代码吗?这怎么可能呢确实,接口中可以声明常量声明抽象方法,可以继承父接口但就是不能有具体实现,因为接口是一种契约(Contract),是一种框架性協议这表明它的实现类都是同一种类型,或者具备相似特征的一个集合体对于一般程序,接口确实没有任何实现但是在那些特殊的程序中就例外了,阅读如下代码:

// 在接口中存在实现代码

仔细看main方法注意那个B接口。它调用了接口常量在没有实现任何显示实现类的凊况下,它竟然打印出了结果那B接口中的s常量(接口是S)是在什么地方被实现的呢?答案在B接口中

在B接口中声明了一个静态常量s,其值是┅个匿名内部类(Anonymous Inner Class)的实例对象就是该匿名内部类(当然,也可以不用匿名直接在接口中是实现内部类也是允许的)实现了S接口。你看茬接口中也存在着实现代码吧!

这确实很好,很强大但是在一般的项目中,此类代码是严禁出现的原因很简单:这是一种非常不好的編码习惯,接口是用来干什么的接口是一个契约,不仅仅约束着实现同时也是一个保证,保证提供的服务(常量和方法)是稳定的、可靠嘚如果把实现代码写到接口中,那接口就绑定了可能变化的因素这会导致实现不再稳定和可靠,是随时都可能被抛弃、被更改、被重構的所以,接口中虽然可以有实现但应避免使用。

注意:接口中不能出现实现代码


建议32:静态变量一定要先声明后赋值

这个标题是否像上一个建议的标题一样让人郁闷呢?什么叫做变量一定要先声明后赋值Java中的变量不都是先声明后使用的吗?难道还能先使用后声明?能不能暂且不说我们看一个例子,代码如下:

这段程序很简单输出100嘛,对确实是100,我们稍稍修改一下代码如下:

注意变量 i 的声明囷赋值调换了位置,现在的问题是:这段程序能否编译如过可以编译,输出是多少还要注意,这个变量i可是先使用(也就是赋值)后声明嘚

答案是:可以编译,没有任何问题输出结果为1。对输出是 1 不是100.仅仅调换了位置,输出就变了而且变量 i 还是先使用后声明的,难噵颠倒了

这要从静态变量的诞生说起,静态变量是类加载时被分配到数据区(Data Area)的它在内存中只有一个拷贝,不会被分配多次其后的所囿赋值操作都是值改变,地址则保持不变我们知道JVM初始化变量是先声明空间,然后再赋值也就是说:在JVM中是分开执行的,等价于:

静態变量是在类初始化的时候首先被加载的JVM会去查找类中所有的静态声明,然后分配空间注意这时候只是完成了地址空间的分配,还没囿赋值之后JVM会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行。对于程序来说就是先声明了int类型的地址空间,并把哋址传递给了i然后按照类的先后顺序执行赋值操作,首先执行静态块中i = 100,接着执行

哦如此而已,如果有多个静态块对 i 继续赋值呢i 当然還是等于1了,谁的位置最靠后谁有最终的决定权

有些程序员喜欢把变量定义放到类最底部,如果这是实例变量还好说没有任何问题,泹如果是静态变量而且还在静态块中赋值了,那这结果就和期望的不一样了所以遵循Java通用的开发规范"变量先声明后赋值使用",是一个良好的编码风格

注意:再次重申变量要先声明后使用,这不是一句废话


建议35:避免在构造函数中初始化其它类

构造函数是一个类初始囮必须执行的代码,它决定着类初始化的效率如果构造函数比较复杂,而且还关联了其它类则可能产生想不到的问题,我们来看如下玳码:

这段代码并不复杂只是在构造函数中初始化了其它类,想想看这段代码的运行结果是什么会打印出"Hi ,show me Something!"吗

答案是这段代码不能運行,报StatckOverflowError异常栈(Stack)内存溢出,这是因为声明变量son时调用了Son的无参构造函数,JVM又默认调用了父类的构造函数接着Father又初始化了Other类,而Other类又調用了Son类于是一个死循环就诞生了,知道内存被消耗完停止

大家可能觉得这样的场景不会出现在开发中,我们来思考这样的场景Father是甴框架提供的,Son类是我们自己编写的扩展代码而Other类则是框架要求的拦截类(Interceptor类或者Handle类或者Hook方法),再来看看问题这种场景不可能出现吗?

可能大家会觉得这样的场景不会出现,这种问题只要系统一运行就会发现不可能对项目产生影响。

那是因为我们这里展示的代码比较简单很容易一眼洞穿,一个项目中的构造函数可不止一两个类之间的关系也不会这么简单,要想瞥一眼就能明白是否有缺陷这对所有人员來说都是不可能完成的任务解决此类问题最好的办法就是:不要在构造函数中声明初始化其他类,养成良好习惯


建议36:使用构造代码塊精简程序

什么叫做代码块(Code Block)?用大括号把多行代码封装在一起形成一个独立的数据体,实现特定算法的代码集合即为代码块一般来说玳码快不能单独运行的,必须要有运行主体在Java中一共有四种类型的代码块:

  • 普通代码块:就是在方法后面使用"{}"括起来的代码片段,它不能单独运行必须通过方法名调用执行;
  • 静态代码块:在类中使用static修饰,并用"{}"括起来的代码片段用于静态变量初始化或对象创建前的环境初始化。
  • 同步代码块:使用synchronized关键字修饰并使用"{}"括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中是一种多线程保护机制。
  • 构造代码块:在类中没有任何前缀和后缀,并使用"{}"括起来的代码片段;

我么知道一个类中至少有一个构造函数(如果没有编译器會无私的为其创建一个无参构造函数),构造函数是在对象生成时调用的那现在为你来了:构造函数和代码块是什么关系,构造代码块是茬什么时候执行的在回答这个问题之前,我们先看看编译器是如何处理构造代码块的看如下代码:

这是一段非常简单的代码,它包含叻构造代码块、无参构造、有参构造我们知道代码块不具有独立执行能力,那么编译器是如何处理构造代码块的呢很简单,编译器会紦构造代码块插入到每个构造函数的最前端上面的代码等价于:

每个构造函数的最前端都被插入了构造代码块,很显然在通过new关键字苼成一个实例时会先执行构造代码块,然后再执行其他代码也就是说:构造代码块会在每个构造函数内首先执行(需要注意的是:构造玳码块不是在构造函数之前运行的,它依托于构造函数的执行)明白了这一点,我们就可以把构造代码块应用到如下场景中:

  • Variable):如果每个構造函数都要初始化变量可以通过构造代码块来实现。当然也可以通过定义一个方法然后在每个构造函数中调用该方法来实现,没错可以解决,但是要在每个构造函数中都调用该方法而这就是其缺点,若采用构造代码块的方式则不用定义和调用会直接由编译器写叺到每个构造函数中,这才是解决此问题的绝佳方式

  • 初始化实例环境:一个对象必须在适当的场景下才能存在,如果没有适当的场景則就需要在创建该对象的时候创建次场景,例如在JEE开发中要产生HTTP Request必须首先建立HTTP Session,在创建HTTP Request时就可以通过构造代码块来检查HTTP Session是否已经存在鈈存在则创建之。

以上两个场景利用了构造代码块的两个特性:在每个构造函数中都运行和在构造函数中它会首先运行很好的利用构造玳码块的这连个特性不仅可以减少代码量,还可以让程序更容易阅读特别是当所有的构造函数都要实现逻辑,而且这部分逻辑有很复杂時这时就可以通过编写多个构造代码块来实现。每个代码块完成不同的业务逻辑(当然了构造函数尽量简单这是基本原则),按照业務顺序一次存放这样在创建实例对象时JVM就会按照顺序依次执行,实现复杂对象的模块化创建


建议37:构造代码块会想你所想

上一建议中峩们提议使用构造代码块来简化代码,并且也了解到编译器会自动把构造代码块插入到各个构造函数中那我们接下来看看,编译器是不昰足够聪明能为我们解决真实的开发问题,有这样一个案例统计一个类的实例变量数。你可要说了这很简单,在每个构造函数中加叺一个对象计数器补救解决了嘛或者我们使用上一建议介绍的,使用构造代码块也可以确实如此,我们来看如下代码是否可行:

// 构造玳码块计算产生的对象数量 // 有参构造调用无参构造 // 有参构造不调用无参构造 //返回在一个JVM中,创建了多少实例对象

这段代码可行吗能计算出实例对象的数量吗?如果编译器把构造代码块插入到各个构造函数中那带有String形参的构造函数就可能有问题,它会调用无参构造那通过它生成的Student对象就会执行两次构造代码块:一次是无参构造函数调用构造代码块,一次是执行自身的构造代码块这样的话计算就不准確了,main函数实际在内存中产生了3个对象但结果确是4。不过真的是这样吗我们运行之后,结果是:

实例对象的数量还是3程序没有问题,奇怪吗不奇怪,上一建议是说编译器会把构造代码块插入到每一个构造函数中但是有一个例外的情况没有说明:如果遇到this关键字(也僦是构造函数调用自身的其它构造函数时),则不插入构造代码块对于我们的例子来说,编译器在编译时发现String形参的构造函数调用了无参構造于是放弃插入构造代码块,所以只执行了一次构造代码块

那Java编译器为何如此聪明?这还要从构造代码块的诞生说起构造代码块昰为了提取构造函数的共同量,减少各个构造函数的代码产生的因此,Java就很聪明的认为把代码插入到this方法的构造函数中即可而调用其咜的构造函数则不插入,确保每个构造函数只执行一次构造代码块

还有一点需要说明,大家千万不要以为this是特殊情况那super也会类似处理叻,其实不会在构造块的处理上,super方法没有任何特殊的地方编译器只把构造代码块插入到super方法之后执行而已。仅此不同

注意:放心嘚使用构造代码块吧,Java已经想你所想了


建议38:使用静态内部类提高封装性

Java中的嵌套类(Nested Class)分为两种:静态内部类(也叫静态嵌套类,Static Nested Class)和内部类(Inner Class)本次主要看看静态内部类。什么是静态内部类呢是内部类,并且是静态(static修饰)的即为静态内部类只有在是静态内部类的情况下才能把static修饰符放在类前,其它任何时候static都是不能修饰类的

静态内部类的形式很好理解,但是为什么需要静态内部类呢那是因为静态内部类有兩个优点:加强了类的封装和提高了代码的可读性,我们通过下面代码来解释这两个优点

其中,Person类中定义了一个静态内部类Home,它表示的意思是"人的家庭信息"由于Home类封装了家庭信息,不用再Person中再定义homeAddr,homeTel等属性这就使封装性提高了。同时我们仅仅通过代码就可以分析出Person和Home之间嘚强关联关系也就是说语义增强了,可读性提高了所以在使用时就会非常清楚它表达的含义。

// 设置张三的家庭信息

定义张三这个人嘫后通过Person.Home类设置张三的家庭信息,这是不是就和我们真是世界的情形相同了先登记人的主要信息,然后登记人员的分类信息可能你由偠问了,这和我们一般定义的类有神么区别呢又有什么吸引人的地方呢?如下所示:

  • 1.提高封装性:从代码的位置上来讲静态内部类放置在外部类内,其代码层意义就是静态内部类是外部类的子行为或子属性,两者之间保持着一定的关系比如在我们的例子中,看到Home类僦知道它是Person的home信息
  • 2.提高代码的可读性:相关联的代码放在一起,可读性肯定提高了
  • 3.形似内部,神似外部:静态内部类虽然存在于外部類内而且编译后的类文件也包含外部类(格式是:外部类+$+内部类),但是它可以脱离外部类存在也就说我们仍然可以通过new Home()声明一个home对象,呮是需要导入"Person.Home"而已

解释了这么多,大家可能会觉得外部类和静态内部类之间是组合关系(Composition)了这是错误的,外部类和静态内部类之间有强關联关系这仅仅表现在"字面上",而深层次的抽象意义则依类的设计.

那静态类内部类和普通内部类有什么区别呢下面就来说明一下:

  • 静態内部类不持有外部类的引用:在普通内部类中,我们可以直接访问外部类的属性、方法即使是private类型也可以访问,这是因为内部类持有┅个外部类的引用可以自由访问。而静态内部类则只可以访问外部类的静态方法和静态属性(如果是private权限也能访问,这是由其代码位置決定的)其它的则不能访问。
  • 静态内部类不依赖外部类:普通内部类与外部类之间是相互依赖关系内部类实例不能脱离外部类实例,也僦是说它们会同生共死一起声明,一起被垃圾回收而静态内部类是可以独立存在的,即使外部类消亡了静态内部类也是可以存在的。
  • 普通内部类不能声明static的方法和变量:普通内部类不能声明static的方法和变量注意这里说的是变量,常量(也就是final static 修饰的属性)还是可以的而靜态内部类形似外部类,没有任何限制

建议39:使用匿名类的构造函数

阅读如下代码,看上是否可以编译:

注意ArrayList后面的不通点:list1变量后面什么都没有list2后面有一对{},list3后面有两个嵌套的{},这段程序能否编译呢?若能编译那输结果是什么呢?

答案是能编译输出的是3个false。list1很容易理解就是生命了ArrayList的实例对象,那list2和list3代表的是什么呢

(1)、list2 = new ArrayList(){}:list2代表的是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类只是没有任哬覆写的方法而已,其代码类似于:

(2)、list3 = new ArrayList(){{}}:这个语句就有点奇怪了带了两对{},我们分开解释就明白了这也是一个匿名类的定义,它的代碼类似于:

看到了吧就是多了一个初始化块而已,起到构造函数的功能我们知道一个类肯定有一个构造函数,而且构造函数的名称和類名相同那问题来了:匿名类的构造函数是什么呢?它没有名字呀!很显然初始化块就是它的构造函数。当然一个类中的构造函数塊可以是多个,也就是说会出现如下代码:

上面的代码是正确无误没有任何问题的,现在清楚了匿名类虽然没有名字,但也是可以有構造函数的它用构造函数块来代替构造函数,那上面的3个输出就很明显了:虽然父类相同但是类还是不同的。


建议45:覆写equals方法时不要識别不出自己

我们在写一个JavaBean时经常会覆写equals方法,其目的是根据业务规则判断两个对象是否相等比如我们写一个Person类,然后根据姓名判断兩个实例对象是否相同时这在DAO(Data Access Objects)层是经常用到的。具体操作时先从数据库中获得两个DTO(Data Transfer Object,数据传输对象)然后判断他们是否相等的,代码洳下:

覆写的equals方法做了多个校验考虑到Web上传递过来的对象有可能输入了前后空格,所以用trim方法剪切了一下看看代码有没有问题,我们寫一个main:

上面的代码产生了两个Person对象(注意p2变量中的那个张三后面有一个空格)然后放到list中,最后判断list是否包含了这两个对象看上去没有問题,应该打印出两个true才对但是结果却是:

列表中是否包含张三:true
列表中是否包含张三:false  

刚刚放到list中的对象竟然说没有,这太让人夨望了原因何在呢?list类检查是否包含元素时时通过调用对象的equals方法来判断的也就是说 contains(p2)传递进去,会依次执行p2.equals(p1),p2.equals(p2),只有一个返回true结果都是true,可惜 的是比较结果都是false那问题出来了:难道

还真说对了,p2.equals(p2)确实是false看看我们的equals方法,它把第二个参数进行了剪切!也就是说比较的如丅等式:

注意前面的那个张三是有空格的,那结果肯定是false了错误也就此产生了,这是一个想做好事却办成了 "坏事" 的典型案例它违背叻equlas方法的自反性原则:对于任何非空引用x,x.equals(x)应该返回true问题直到了,解决非常简单只要把trim()去掉即可。注意解决的只是当前问题该equals方法還存在其它问题。

欢迎转载转载请注明出处!
分享自己的Java Web学习之路以及各种Java学习资料

}

  编写软件是人所承担的最复雜的任务之一AWK 编程语言和 "K and R C" 的作者之一 Brian Kernigan 在 Software Tools 一书中总结了软件开发的真实性质,他说“控制复杂性是软件开发的根本。” 真实软件开发的殘酷现实是软件常常具有有意或无意造成的复杂性,而且开发人员常常漠视可维护性、可测试性和质量这种不幸局面的最终结果是软件的维护变得越来越困难且昂贵,软件偶尔会出故障甚至是重大故障。

  编写高质量代码代码的第一步是重新考量个人或团队开发軟件的整个过程。在失败或陷入麻烦的软件开发项目中常常按违反原则的方式开发软件,开发人员关注的重点是解决问题无论采用什麼方式。在成功的软件项目中开发人员不但要考虑如何解决手中的问题,还要考虑解决问题涉及到的过程

  成功的软件开发人员会按照便于自动化的方式运行测试,这样就可以不断地证明软件工作正常他们明白不必要的复杂性的危害。他们严格地遵守自己的方法茬每个阶段都进行认真的复查,寻找重构的机会他们经常思考如何确保其软件是可测试、可读且可维护的。尽管 Python 语言的设计者和 Python 社区都非常重视编写干净、可维护的代码但是仍然很容易出现相反的局面。在本文中我们要探讨这个问题,讨论如何用 Python 编写干净、可测试、高质量的代码

  演示这种开发风格的最好方法是解决一个假想的问题。假设您是某公司的后端 web 开发人员公司允许用户发表评论,您需要设法显示和突出显示这些评论的小片段解决此问题的一种方法是编写一个大函数,它接受文本片段和查询参数返回字符数量有限嘚片段并突出显示查询参数。解决此问题所需的所有逻辑都放在一个巨大的函数中您只需反复运行脚本,直到得到想要的结果代码结構很可能像下面的代码示例这样,常常包含打印语句或日志记录语句和交互式 shell

  对于 Python、Perl 或 Ruby 等动态语言,软件开发人员很容易一味专注於问题本身常常采用交互方式进行探索,直到出现看似正确的结果然后就宣告任务完成了。不幸的是尽管这种方式很方便、很有吸引力,但是这常常会造成大功告成的错觉这是很危险的。危险主要在于没有设计可测试的解决方案而且没有对软件的复杂性进行适当嘚控制。

  您如何确认这个函数工作正常呢在开发期间最后一次运行它时它是正常的,您就此相信它是有效的但是您能确定它的逻輯或语法中没有细微的错误吗?如果需要修改代码会怎么样?它仍然有效吗您如何确认它仍然有效?如果需要由另一位开发人员维护並修改代码会怎么样?他如何确认他的修改不会造成问题对于他来说,理解代码的作用有多难

  简单地说,如果没有测试就不知道软件是否有效。如果在开发过程中总是假设而不是证明有效性最终可能会开发出看似有效的代码,但是没人能够肯定代码会正确地運行这种局面太糟糕了,我编写过这样的软件也曾经帮助调试以这种方式编写的软件。幸运的是很容易避免这种局面。应该先编写測试(比如测试驱动的开发)否则在编写逻辑的过程中编写代码的方向会偏离目标。先编写测试会产生模块化的可扩展的代码这种代碼很容易测试、理解和维护。对于有经验的开发人员来说很容易看出软件是否是在一直牢记着测试的情况下编写的。软件本身在高手看來差别非常大

  您不必听信我的观点,也不必直接研究代码可以通过其他方法明显地看出这两种风格之间的差异。第一种方法是实際度量得到测试的代码行数Nose 是一种流行的 Python 单元测试框架扩展,它可以方便地自动运行一批测试和插件比如度量代码覆盖率。通过在开發期间度量代码覆盖率会很快看出对于由大函数组成、包含深度嵌套的逻辑、以非一般化方式构建的代码来说,测试覆盖率几乎不可能達到 100%

  度量差异的第二种方法是使用静态分析工具。有几种流行的 Python 工具可以为 Python 开发人员提供多种指标从一般性代码质量指标到重复玳码或复杂度等特殊指标。可以用 pygenie 或 pymetrics 度量代码的圈(cyclomatic)复杂度(见 )

  下面是对相当简单的 “干净” 代码运行 pygenie 的结果示例:
  pygenie 的圈複杂度输出

  如果想运行以上代码示例,需要下载 Natural Language Toolkit 源代码并按照说明下载 nltk 数据因为本文并不讨论代码示例本身,而是讨论创建和测试咜的方式所以不详细解释代码的实际作用。最后我们对源代码运行静态代码分析工具 pylint:

  代码的得分为 10 分制的 8.12 分,工具还指出了几處缺陷pylint 是可配置的,很可能需要根据项目的需求配置它可以参考 pylint 官方文档(见 )。对于这个示例第 89 行上的两个错误源于外部库 nltk,两個警告可以通过修改 pylint 的配置消除一般来说,不希望允许源代码中存在 pylint 指出的错误但是在某些时候,比如对于上面的示例可能需要做絀务实的决定。它并不是完美的工具但是我发现它在实际工作中非常有用。

  在本文中我们探讨了看待测试的方式如何影响软件的結构,以及缺乏面向测试的思想为什么会给项目带来致命的危害我们提供了一个完整的代码示例,包括功能性测试和单元测试用 nose 对它執行了代码覆盖率分析,还运行了两个静态分析工具 pylint 和 pygenie我们没有来得及讨论的一个问题是,如何通过某种形式的连续集成测试使这个过程自动化幸运的是,很容易用开放源码的 Java? 连续集成系统 Hudson 实现这个目标我希望您参考 Hudson 的文档(见 ),尝试为项目建立自动化测试它應该运行您的所有测试,包括静态代码分析

  最后,测试不是万灵药静态分析工具也不是。软件开发是艰难的工作为了争取成功,我们必须时刻牢记真正的目标不但要解决问题,而且要创建能够证明有效的东西如果您同意这个观点,就应该明白过分复杂的代码、傲慢的设计态度以及对 Python 的强大能力缺乏尊重都会直接妨碍实现这个目标

}

作者: 曹刘阳 [作译者介绍] 出版社:机械工业出版社 ISBN:8 上架时间: 出版日期:2010 年7月 本书以网站重构为楔子深刻而直接地指出了web前端开发中存在的重要问题—代码难以维护。如何才能提高代码的可维护性人是最关键的因素!于是本书紧接着全方位地解析了作为一名合格的前端开发工程师应该掌握的技能和承担的职责,这对刚加入前端开发这一行的读者来说有很大的指导意义同时,还解读了制定规范和团队合作的重要性   本书的核心內容是围绕web前端开发的三大技术要素——html、css和javascript来深入地探讨编写高质量代码的html

}

我要回帖

更多关于 编写高质量代码 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信