改善Java程序的151个建议

所需积分/C币:10 2012-08-22 01:03:44 2.97MB PDF
收藏 收藏
举报

在通往“Java技术殿堂”的路上,本书将为你指点迷津!内容全部由Java编码的最佳实践组成,从语法、程序设计和架构、工具和框架、编码风格和编程思想等五大方面对Java程序员遇到的各种棘手的疑难问题给出了经验性的解决方案,为Java程序员如何编写高质量的Java代码提出了151条极为宝贵的建议。对于每一个问题,不仅以建议的方式从正反两面给出了被实践证明为十分优秀的解决方案和非常糟糕的解决方案,而且还分析了问题产生的根源,犹如醍醐灌顶,让人豁然开朗。
第1章Java开发中通用的方法和准则 /*接口常量*/ interface Cons /这还是常量吗? public static final int RAND CONST new Random().nextInt o)i RAND CONST是常量吗?它的值会变吗?绝对会变!这种常量的定义方式是极不可取 的,常量就是常量,在编译期就必须确定其值,不应该在运行期更改,否则程序的可读性会 非常差,甚至连作者自己都不能确定在运行期发生了何种神奇的事情 甭想着使用常量会变的这个功能来实现序列号算法、随机种子生成,除非这真的是项目 中的唯一方案,否则就放弃吧,常量还是当常量使用。 注意务必让常量的值在运行期保持不变。 建议3:三元操作符的类型务必一致 三元操作符是if-else的简化写法,在项目中使用它的地方很多,也非常好用,但是好用 又简单的东西并不表示就可以随便用,我们来看看下面这段代码 public class client public static void main(string [ args) nt 1=80: String s= String valueOf(i<100?90: 100); String sl = String valueOf(1<100?90: 100.0) System.out, print1n("两者是否相等:"+s.equa1s(s1)); 分析一下这段程序:i是80,那它当然小于100,两者的返回值肯定都是90,再转成 String类型,其值也绝对相等,毋庸置疑的。恩,分析得有点道理,但是变量s中三元操作 符的第二个操作数是100,而sl的第二个操作数是100.0,难道没有影响吗?不可能有影响 吧,三元操作符的条件都为真了,只返回第一个值嘛,与第二个值有一毛钱的关系吗?貌似 有道理。 果真如此吗?我们通过结果来验证一下,运行结果是:“两者是否相等: false”,什么? 不相等,Why? 问题就岀在了100和100.0这两个数字上,在变量s中,三元操作符中的第一个操作数 (90)和第二个操作数(100)都是int类型,类型相同,返回的结果也就是int类型的90 而变量sl的情况就有点不同了,第一个操作数是90(int类型),第二个操作数却是100.0 而这是个浮点数,也就是说两个操作数的类型不一致,可三元操作符必须要返回一个数据 编写高质量代码:改善Java程序的151个建议 而且类型要确定,不可能条件为真时返回int类型,条件为假时返回 float类型,编译器是不 允许如此的,所以它就会进行类型转换了,int型转换为浮点数90.0,也就是说三元操作符的 返回值是浮点数90.0,那这当然与整型的90不相等了。这里可能有读者疑惑了:为什么是 整型转为浮点,而不是浮点转为整型呢?这就涉及三元操作符类型的转换规则: 口若两个操作数不可转换,则不做转换,返回值为 Object类型。 口若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换, int类型转换为long类型,long类型转换为 float类型等 口若两个操作数中有一个是数字S,另外一个是表达式,且其类型标示为T,那么,若 数字S在T的范围内,则转换为T类型;若S超出了T类型的范围,则T转换为S 类型(可以参考“建议22”,会对该问题进行展开描述)。 口若两个操作数都是直接量数字( Literal)③,则返回值类型为范围较大者。 知道是什么原因了,相应的解决办法也就有了:保证三元操作符中的两个操作数类型 致,即可减少可能错误的发生 建议4:避免带有变长参数的方法重载 在项目和系统的开发中,为了提高方法的灵活度和可复用性,我们经常要传递不确定数 量的参数到方法中,在Java5之前常用的设计技巧就是把形参定义成 Collection类型或其子 类类型,或者是数组类型,这种方法的缺点就是需要对空参数进行判断和筛选,比如实参为 nul值和长度为0的 Collection或数组。而Java5引入变长参数( varas)就是为了更好地 提高方法的复用性,让方法的调用者可以“随心所欲”地传递实参数量,当然变长参数也是 要遵循一定规则的,比如变长参数必须是方法中的最后一个参数;一个方法不能定义多个变 长参数等,这些基夲规则需要牢记,但是即使记住了这些规则,仍然有可能出现错误,我们 来看如下代码: public class client I /简单折扣计算 public void calPriceint price, int discount)[ float knockdownPrice =price discount /100OF System.out, print1n("简单折扣后的价格是:"+ formateCurrency( knockdownprice)); /复杂多折扣计算 public void calPrice (int price, int... discounts float knockdownPrice= price for(int discount: discounts)i knockdownPrice= knockdownPrice discount /100 分“ Literal”也译作“字面量”。 第1章Java开发中通用的方法和准则 System,out. println("复杂折扣后的价格是:"+ formateCurrency( knockdownprice)) //格式化成本的贷币形式 private String formateCurrency(float price)i return NumberFormat. getCurrency Instance(). format(price/100) public static void main (String [ args) Client client new Client ( //499元的贷物,打75折 client. calPrice(49900, 75) 这是一个计算商品价格折扣的模拟类,带有两个参数的 capRice方法(该方法的业务逻 辑是:提供商品的原价和折扣率,即可获得商品的折扣价)是一个简单的折扣计算方法,该 方法在实际项目中经常会用到,这是单一的打折方法。而带有变长参数的 capRice方法则是 较复杂的折扣计算方式,多种折扣的叠加运算(模拟类是一种比较简单的实现)在实际生活 中也是经常见到的,比如在大甩卖期间对ⅤIP会员再度进行打折;或者当天是你的生日,再 给你打个9折,也就是俗话说的“折上折” 业务逻辑清楚了,我们来仔细看看这两个方法,它们是重载吗?当然是了,重载的定义 是“方法名相同,参数类型或数量不同”,很明显这两个方法是重载。但是再仔细瞧瞧,这 个重载有点特殊: capRice( Int price,int.. discounts)的参数范畴覆盖了 capRice( Int price,int discount)的参数范畴。那问题就出来了:对于 capRice(49900,75)这样的计算,到底该调 用哪个方法来处理呢? 我们知道Java编译器是很聪明的,它在编译时会根据方法签名( Method Signature)来 确定调用哪个方法,比如 capRice(499900,75,95)这个调用,很明显75和95会被转成一个 包含两个元素的数组,并传递到 capRice( Int price, In. discounts)中,因为只有这一个方法 签名符合该实参类型,这很容易理解。但是我们现在面对的是 capRice(49900,75)调用,这 个“75”既可以被编译成int类型的“75”,也可以被编译成int数组“{75}”,即只包含一个 元素的数组。那到底该调用哪一个方法呢? 我们先运行一下看看结果,运行结果是 简单折扣后的价格是:¥37425。 看来是调用了第一个方法,为什么会调用第一个方法,而不是第二个变长参数方法呢? 因为Java在编译时,首先会根据实参的数量和类型(这里是2个实参,都为int类型,注意 没有转成int数组)来进行处理,也就是査找到 capRice( Int price, int discount)方法,而且确 认它是否符合方法签名条件。现在的问题是编译器为什么会首先根据2个int类型的实参而 不是1个int类型、1个int数组类型的实参来查找方法呢?这是个好问题,也非常好回答 编写高质量代码:改善Java程序的151个建议 因为int是一个原生数据类型,而数组本身是一个对象,编译器想要“偷懒”,于是它会从最 简单的开始“猜想”,只要符合编译条件的即可通过,于是就出现了此问题。 冋题是阐述清楚了,为了让我们的程序能被“人类”看懂,还是慎重考虑变长参数的方 法重载吧,否则让人伤脑筋不说,说不定哪天就陷入这类小陷阱里了 建议5:别让nu值和空值威胁到变长方法 上一建议讲解了变长参数的重载问题,本建议还会继续讨论变长参数的重载问题。上 建议的例子是变长参数的范围覆盖了非变长参数的范围,这次我们从两个都是变长参数的方 法说起,代码如下 publ lass Client public void methodA(String str, Integer public void methodA(String str, String.. strs)( public static void main (String [ args) Client client= new Client o lient methodA ("Ch client methodA("China", "People") t. methodA("China") client methodA ("China" null)i 两个 methodA都进行了重载,现在的问题是:上面的代码编译通不过,问题出在什么地 方?看似很简单哦 有两处编译通不过: client, methodA(" China")和 client. methodA(" China",null),估计 你已经猜到了,两处的提示是相同的:方法模糊不清,编译器不知道调用哪一个方法,但这 两处代码反映的代码味道可是不同的。 对于 methoda(" China")方法,根据实参“ China”( String类型),两个方法都符合形 格式,编译器不知道该调用哪个方法,于是报错。我们来思考这个问题: Client类是一个 复杂的商业逻辑,提供了两个重载方法,从其他模块调用(系统內本地调用或系统外远程调 用)时,调用者根据变长参数的规范调用,传入变长参数的实参数量可以是N个(N>=0), 那当然可以写成 client. methodA(" china")方法啊!完全符合规范,但是这却让编译器和调 用者都很郁闷,程序符合规则却不能运行,如此冋题,谁之责任呢?是 Client类的设计者 他违反了KISS原则( Keep It Simple, Stupid,即懒人原则),按照此规则设计的方法应该很 容易调用,可是现在在遵循规范的情况下,程序竟然出错了,这对设计者和开发者而言都是 第1章Java开发中通用的方法和准则 应该严禁出现的。 对于 client methoda(" china",nul)方法,直接量null是没有类型的,虽然两个 methodA 方法都符合调用请求,但不知道调用哪一个,于是报错了。我们来体会一下它的坏味道:除 了不符合上面的懒人原则外,这里还有一个非常不好的编码习惯,即调用者隐藏了实参类 型,这是非常危险的,不仅仅调用者需要“猜测”该调用哪个方法,而且被调用者也可能产 生内部逻辑混乱的情况。对于本例来说应该做如下修改: public static void main (String [ args) Client client new Client o string [ strs null client. methodA("China", strs) 也就是说让编译器知道这个nul值是 String类型的,编译即可顺利通过,也就减少了错 误的发生。 建议δ:覆写变长方法也循规蹈矩 在Java中,子类覆写父类中的方法很常见,这样做既可以修正Bug也可以提供扩展的 业务功能支持,同时还符合开闭原则(open- Closed Principle),我们来看一下覆写必须满足 的条件: 口重写方法不能缩小访问权限。 口参数列表必须与被重写方法相同。 口返回类型必须与被重写方法的相同或是其子类。 口重写方法不能抛出新的异常,或者超出父类范围的异常,但是可以抛出更少、更有限 的异常,或者不抛出异常 估计你已经猜测出下面要讲的内容了,为什么“参数列表必须与被重写方法的相同”采 用不同的字体,这其中是不是有什么玄机?是的,还真有那么一点点小玄机。参数列表相同 包括三层意思:参数数量相同、类型相同、顺序相同,看上去好像没什么问题,那我们来看 个例子,业务场景与上一个建议相同,商品打折,代码如下: public class client public static void main(String[] args) //向上转型 Basebase new Sub() base. fun(100, 50) //不转型 Subsub= new Sub( sub.fun(100,50) 编写高质量代码:改善Java程序的151个建议 //基类 lass Base void fun(int price,int.. discounts)I System. out. println("Base fun") //子类,覆写父类方法 class Sub extends Base @Override void fun(int price, int [ discounts) System. out. println ("Sub 请问:该程序有问题吗?——编译通不过。那问题出在什么地方呢? Override注解吗?非也,覆写是正确的,因为父类的 capRice编译成字节码后的形参是 个int类型的形参加上一个int数组类型的形参,子类的参数列表也与此相同,那覆写是理所当 然的了,所以加上@ l Override注解没有问题,只是 Eclipse会提示这不是一种很好的编码风格 难道是“ sub. fun(100,50)”这条语句?正解,确实是这条语句报错,提示找不到fun (int,int)方法。这太奇怪了:子类继承了父类的所有属性和方法,甭管是私有的还是公开的 访问权限,同样的参数、同样的方法名,通过父类调用没有任何问题,通过子类调用却编译 通不过,为啥?难道是没继承下来?或者子类缩小了父类方法的前置条件?那如果是这样, 就不应该覆写,@ Override就应该报错,真是奇妙的事情! 事实上,base对象是把子类对象Sub做了向上转型,形参列表是由父类决定的,由于是 变长参数,在编译时,“base.fun(100,50)”中的“50”这个实参会被编译器“猜测”而编译 成“{50}”数组,再由子类Sυb执行。我们再来看看直接调用子类的情况,这时编译器并不 会把“50”做类型转换,因为数组本身也是一个对象,编译器还没有聪明到要在两个没有继 承关系的类之间做转换,要知道Java是要求严格的类型匹配的,类型不匹配编译器自然就会 拒绝执行,并给予错误提示 这是个特例,覆写的方法参数列表竟然与父类不相同,这违背了覆写的定义,并且会引 发莫名其妙的错误。所以读者在对变长参数进行覆写时,如果要使用此类似的方法,请找个 小黑屋仔细想想是不是一定要如此。 注意覆写的方法参数与父类相同,不仅仅是类型、数量,还包括显示形式。 建议7:警惕自增的陷阱 记得大学刚开始学C语言时,老师就说:自增有两种形式,分别是计+和++i,i++表 第1章Java开发中通用的方法和准则 9 示的是先赋值后加1,+i是先加1后赋值,这样理解了很多年也没出现问题,直到遇到如下 代码,我才怀疑我的理解是不是错了 public class client public static void main(String [] args) nt count =0 for(int i=0; 1<10; i++) count=count++i System. out. println ("count=+count)i 这个程序输出的 count等于几?是 count自加10次吗?答案等于10?可以非常肯定地 告诉你,答案错误!运行结果是 count等于0。为什么呢? count+是一个表达式,是有返回值的,它的返回值就是 count自加前的值,Java对自 加是这样处理的:首先把 count的值(注意是值,不是引用)拷贝到一个临时变量区,然后 对 count变量加1,最后返回临时变量区的值。程序第一次循环时的详细处理步骤如下 步骤1JVM把 count值(其值是0)拷贝到临时变量区。 步骤2 count值加1,这时候 count的值是1 步骤3返回临时变量区的值,注意这个值是0,没修改过。 步骤4返回值赋值给 count,此时 count值被重置成0。 “ count= count-+-”这条语句可以按照如下代码来理解: public static int mockAdd(int count)I //先保存初始值 nt temp =count i /做自增操作 count count+1 //返回原始值 return temp i 于是第一次循环后 count的值还是0,其他9次的循环也是一样的,最终你会发现 count 的值始终没有改变,仍然保持着最初的状态。 此例中代码作者的本意是希望 count自增,所以想当然地认为赋值给自身就成了,不 曾想掉到Java自增的陷阱中了。解决方法很简单,只要把“ count= count++”修改为 “ count-+”即可。该问题在不同的语言环境有不同的实现:C++中“ count= count++”与 “ count-艹-”是等效的,而在PHP中则保持着与Java相同的处理方式。毎种语言对自增的实 现方式各不同,读者有兴趣可以多找几种语言测试一下,思考一下原理。 下次如果看到某人T恤上印着“i-i++”,千万不要鄙视他,记住,能够以不同的语言解 释清楚这句话的人绝对不简单,应该表现岀“如滔滔江水”般的敬仰,心理默念着“高人, 0◆编写高质量代码:改善Java程序的151个建议 绝世高人哪”。 建议8:不要让旧语法困扰你 N多年前接手了一个除了源码以外什么都没有的项目,没需求、没文档、没设计,原创 者也已鸟兽散了,我们只能通过阅读源码来进行维护。期间,同事看到一段很“奇妙”的代 码,让大家帮忙分析,代码片段如下: lass client i public static void main(String[] args)I //数据定义及初始化 int fee=200 /其他业务处理 Defaul七 (fee) //其他业务处理 static void saveDefault( static void save(int fee) 该代码的业务含乂是计算交易的手续费,最低手续费是2元,其业务逻辑大致看懂了 但是此代码非常神奇,“ saveDefault:save(fee)”这句代码在此处出现后,后续就再也没有 与此有关的代码了,这做何解释呢?更神奇的是,编译竟然还没有错,运行也很正常。Java 中竟然有冒号操作符,一般情况下,它除了在唯一一个三元操作符中存在外就没有其他地方 可用了呀。当时连项目组里的高手也是一愣一愣的,翻语法书,也没有介绍冒号操作符的内 容,而且,也不可能出现连括号都可以省掉的方法调用、方法级联啊!这也太牛了吧 隔壁做C项目的同事过来串门,看我们在讨论这个问题,很惊奇地说“耶,Java中还有 标号呀,我以为Java这么高级的语言已经抛弃goto语句了……”,一语点醒梦中人:项目的 原创者是C语言转过来的开发人员,所以他把C语言的goto习惯也带到项目中了,后来由 于经过N手交接,重构了多次,到我们这里goto语句已经被重构掉了,但是跳转标号还保 留着,估计上一届的重构者也是稀里糊涂的,不敢贸然修改,所以把这个重任留给了我们。 goto语句中有着“ double face”作用的关键字,它可以让程序从多层的循环中跳出,不 用一层一层地退出,类似高楼着火了,来不及一楼一楼的下,goto语句就可以让你“biu~” 的一声从十层楼跳到地面上。这点确实很好,但同时也带来了代码结构混乱的问题,而且程 序跳来跳去让人看着就头晕,还怎么调试?!这样做甚至会隐祸连连,比如标号前后对象构 造或变量初始化,一旦跳到这个标号,程序就不可想象了,所以Java中抛弃了goto语法

...展开详情
试读 82P 改善Java程序的151个建议
立即下载 低至0.43元/次 身份认证VIP会员低至7折
    一个资源只可评论一次,评论内容不能少于5个字
    zljk000 只有51个建议,不是全本
    2012-10-08
    回复
    accplxm126 不是很全面,内容还可以。再看一下。
    2012-09-24
    回复
    liusichun 不是很全面,内容还可以。
    2012-09-24
    回复
    LegendAniu 只有51个建议,不是全本
    2012-09-10
    回复
    关注 私信 TA的资源
    上传资源赚积分,得勋章
    最新推荐
    改善Java程序的151个建议 10积分/C币 立即下载
    1/82
    改善Java程序的151个建议第1页
    改善Java程序的151个建议第2页
    改善Java程序的151个建议第3页
    改善Java程序的151个建议第4页
    改善Java程序的151个建议第5页
    改善Java程序的151个建议第6页
    改善Java程序的151个建议第7页
    改善Java程序的151个建议第8页
    改善Java程序的151个建议第9页
    改善Java程序的151个建议第10页
    改善Java程序的151个建议第11页
    改善Java程序的151个建议第12页
    改善Java程序的151个建议第13页
    改善Java程序的151个建议第14页
    改善Java程序的151个建议第15页
    改善Java程序的151个建议第16页
    改善Java程序的151个建议第17页
    改善Java程序的151个建议第18页
    改善Java程序的151个建议第19页
    改善Java程序的151个建议第20页

    试读已结束,剩余62页未读...

    10积分/C币 立即下载 >