当前位置: 首页 > 新闻 > 信息荟萃
编号:257
编写高质量代码改善java程序的151个建议.pdf
http://www.100md.com 2019年12月20日
第1页
第7页
第12页
第25页
第31页
第380页

    参见附件(1754KB,440页)。

     编写高质量代码改善java程序的151个建议本站提供,这是一个关于码农们必备看的书籍,用户可以有效的改善好自己的码代码习惯,让代码更加整洁!

    内容简介

    《编写高质量代码:改善Java程序的151个建议》针对每个问题所设计的应用场景都非常典型,给出的建议也都与实践紧密结合。

    书中的每一条建议都可能在你的下一行代码、下一个应用或下一个项目中崭露头角,建议你将此书搁置在手边,随时查阅,一定能使你的学习和开发工作事半功倍。

    作者介绍

    秦小波,资深软件开发工程师、系统分析师和架构师(获Sun架构师认证),从软件开发工作10余年,实践经验极其丰富。资深Java技术专家,Java语言、Spring、Struts 2、Hibernate、iBatis、jBPM等Java技术,在企业级Java应用领域积累了大量工程经验,对ESB、BPEL等整合技术也有较深入的认识。精通设计模式,对设计模式有深刻的认识和独到见解,而且创造性地提出了自己在大量实践中总结出来的新的设计模式。他撰写的《设计模式之禅》一书凭借优质的内容和良好的可读性广获读者好评,被誉为“设计模式领域的里程碑之作。此外,他还是一位优秀的DBA,具有IBM DB2 DBA资格认证,对海量数据处理有深入的研究。

    章节目录

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

    第2章 基本类型

    第3章 类、对象及方法

    第4章 字符串

    第5章 数组和集合

    第6章 枚举和注解

    第7章 泛型和反射

    编写高质量代码改善java程序的151个建议截图

    编写高质量代码

    ——改善Java程序的151个建议

    秦小波 著

    ISBN:978-7-111-36259-3

    本书纸版由机械工业出版社于2011年出版,电子版由华章分社(北京华

    章图文信息有限公司)全球范围内制作与发行。

    版权所有,侵权必究

    客服热线:+ 86-10-68995265

    客服信箱:service@bbbvip.com

    官方网址:www.hzmedia.com.cn

    新浪微博 @研发书局

    腾讯微博 @yanfabook目 录 前言

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

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

    建议2:莫让常量蜕变成变量

    建议3:三元操作符的类型务必一致

    建议4:避免带有变长参数的方法重载

    建议5:别让null值和空值威胁到变长方法

    建议6:覆写变长方法也循规蹈矩

    建议7:警惕自增的陷阱

    建议8:不要让旧语法困扰你

    建议9:少用静态导入

    建议10:不要在本类中覆盖静态导入的变量和方法

    建议11:养成良好习惯,显式声明UID

    建议12:避免用序列化类在构造函数中为不变量赋值

    建议13:避免为final变量复杂赋值

    建议14:使用序列化类的私有方法巧妙解决部分属性持久化问题

    建议15:break万万不可忘

    建议16:易变业务使用脚本语言编写

    建议17:慎用动态编译

    建议18:避免instanceof非预期结果

    建议19:断言绝对不是鸡肋

    建议20:不要只替换一个类

    第2章 基本类型

    建议21:用偶判断,不用奇判断

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

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

    建议24:边界,边界,还是边界

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

    建议26:提防包装类型的null值

    建议27:谨慎包装类型的大小比较

    建议28:优先使用整型池

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

    建议30:不要随便设置随机种子

    第3章 类、对象及方法

    建议31:在接口中不要存在实现代码建议32:静态变量一定要先声明后赋值

    建议33:不要覆写静态方法

    建议34:构造函数尽量简化

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

    建议36:使用构造代码块精炼程序

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

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

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

    建议40:匿名类的构造函数很特殊

    建议41:让多重继承成为现实

    建议42:让工具类不可实例化

    建议43:避免对象的浅拷贝

    建议44:推荐使用序列化实现对象的拷贝

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

    建议46:equals应该考虑null值情景

    建议47:在equals中使用getClass进行类型判断

    建议48:覆写equals方法必须覆写hashCode方法

    建议49:推荐覆写toString方法

    建议50:使用package-info类为包服务

    建议51:不要主动进行垃圾回收

    第4章 字符串

    建议52:推荐使用String直接量赋值

    建议53:注意方法中传递的参数要求

    建议54:正确使用String、StringBuffer、StringBuilder

    建议55:注意字符串的位置

    建议56:自由选择字符串拼接方法

    建议57:推荐在复杂字符串操作中使用正则表达式

    建议58:强烈建议使用UTF编码

    建议59:对字符串排序持一种宽容的心态

    第5章 数组和集合

    建议60:性能考虑,数组是首选

    建议61:若有必要,使用变长数组

    建议62:警惕数组的浅拷贝

    建议63:在明确的场景下,为集合指定初始容量

    建议64:多种最值算法,适时选择

    建议65:避开基本类型数组转换列表陷阱

    建议66:asList方法产生的List对象不可更改建议67:不同的列表选择不同的遍历方法

    建议68:频繁插入和删除时使用LinkedList

    建议69:列表相等只需关心元素数据

    建议70:子列表只是原列表的一个视图

    建议71:推荐使用subList处理局部列表

    建议72:生成子列表后不要再操作原列表

    建议73:使用Comparator进行排序

    建议74:不推荐使用binarySearch对列表进行检索

    建议75:集合中的元素必须做到compareTo和equals同步

    建议76:集合运算时使用更优雅的方式

    建议77:使用shuffle打乱列表

    建议78:减少HashMap中元素的数量

    建议79:集合中的哈希码不要重复

    建议80:多线程使用Vector或HashTable

    建议81:非稳定排序推荐使用List

    建议82:由点及面,一叶知秋—集合大家族

    第6章 枚举和注解

    建议83:推荐使用枚举定义常量

    建议84:使用构造函数协助描述枚举项

    建议85:小心switch带来的空值异常

    建议86:在switch的default代码块中增加AssertionError错误

    建议87:使用valueOf前必须进行校验

    建议88:用枚举实现工厂方法模式更简洁

    建议89:枚举项的数量限制在64个以内

    建议90:小心注解继承

    建议91:枚举和注解结合使用威力更大

    建议92:注意@Override不同版本的区别

    第7章 泛型和反射

    建议93:Java的泛型是类型擦除的

    建议94:不能初始化泛型参数和数组

    建议95:强制声明泛型的实际类型

    建议96:不同的场景使用不同的泛型通配符

    建议97:警惕泛型是不能协变和逆变的

    建议98:建议采用的顺序是List、List、List

    建议99:严格限定泛型类型采用多重界限

    建议100:数组的真实类型必须是泛型类型的子类型

    建议101:注意Class类的特殊性建议102:适时选择getDeclared×××和get×××

    建议103:反射访问属性或方法时将Accessible设置为true

    建议104:使用forName动态加载类文件

    建议105:动态加载不适合数组

    建议106:动态代理可以使代理模式更加灵活

    建议107:使用反射增加装饰模式的普适性

    建议108:反射让模板方法模式更强大

    建议109:不需要太多关注反射效率

    第8章 异常

    建议110:提倡异常封装

    建议111:采用异常链传递异常

    建议112:受检异常尽可能转化为非受检异常

    建议113:不要在finally块中处理返回值

    建议114:不要在构造函数中抛出异常

    建议115:使用Throwable获得栈信息

    建议116:异常只为异常服务

    建议117:多使用异常,把性能问题放一边

    第9章 多线程和并发

    建议118:不推荐覆写start方法

    建议119:启动线程前stop方法是不可靠的

    建议120:不使用stop方法停止线程

    建议121:线程优先级只使用三个等级

    建议122:使用线程异常处理器提升系统可靠性

    建议123:volatile不能保证数据同步

    建议124:异步运算考虑使用Callable接口

    建议125:优先选择线程池

    建议126:适时选择不同的线程池来实现

    建议127:Lock与synchronized是不一样的

    建议128:预防线程死锁

    建议129:适当设置阻塞队列长度

    建议130:使用CountDownLatch协调子线程

    建议131:CyclicBarrier让多线程齐步走

    第10章 性能和效率

    建议132:提升Java性能的基本方法

    建议133:若非必要,不要克隆对象

    建议134:推荐使用“望闻问切”的方式诊断性能

    建议135:必须定义性能衡量标准建议136:枪打出头鸟—解决首要系统性能问题

    建议137:调整JVM参数以提升性能

    建议138:性能是个大“咕咚”

    第11章 开源世界

    建议139:大胆采用开源工具

    建议140:推荐使用Guava扩展工具包

    建议141:Apache扩展包

    建议142:推荐使用Joda日期时间扩展包

    建议143:可以选择多种Collections扩展

    第12章 思想为源

    建议144:提倡良好的代码风格

    建议145:不要完全依靠单元测试来发现问题

    建议146:让注释正确、清晰、简洁

    建议147:让接口的职责保持单一

    建议148:增强类的可替换性

    建议149:依赖抽象而不是实现

    建议150:抛弃7条不良的编码习惯

    建议151:以技术员自律而不是工人前言

    从决定撰写本书到完稿历时9个月,期间曾经遇到过种种困难和挫

    折,但这个过程让我明白了坚持的意义,明白了“行百里者半九十”的寓

    意—坚持下去,终于到了写前言的时刻。

    为什么写这本书

    从第一次敲出“Hello World”到现在已经有15年时间了,在这15年

    里,我当过程序员和架构师,也担任过项目经理和技术顾问—基本上与

    技术沾边的事情都做过。从第一次接触Java到现在,已经有11年4个月

    了,在这些年里,我对Java可谓是情有独钟,对其编程思想、开源产

    品、商业产品、趣闻轶事、风流人物等都有所了解和研究。对于Java,我非常感激,从物质上来说,它给了我工作,帮助我养家糊口;从精神

    上来说,它带给我无数的喜悦、困惑、痛苦和无奈—一如我们的生活。

    我不是技术高手,只是技术领域的一个拓荒者,我希望能把自己的

    知识和经验贡献出来,以飨读者。在写作的过程中,我也反复地思考:

    我为谁而写这本书?为什么要写?

    希望本书能帮您少走弯路

    您是否曾经为了提供一个“One Line”的解决方案而彻夜地查看源代

    码?现在您不用了。

    您是否曾经为了理解某个算法而冥思苦想、阅览群书?现在您不用

    了。

    您是否曾经为了提升0.1秒的性能而对N种实现方案进行严格测试和

    对比?现在您不用了。

    您是否曾经为了避免多线程死锁问题而遍寻高手共同诊治?现在您

    不用了。……

    在学习和使用Java的过程中您是否在原本可以很快掌握或解决的问题上耗费了大量的时间和精力?也许您现在不用了,本书的很多内容都

    是我用曾经付出的代价换来的,希望它能帮助您少走弯路!

    希望本书能帮您打牢基础

    那些所谓的架构师、设计师、项目经理、分析师们,已经有多长时

    间没有写过代码了?代码是一切的基石,我不太信任连“Hello World”都

    没有写过的架构师。看看我们软件界的先辈们吧,Dennis M.Ritchie决定

    创造一门“看上去很好”的语言时,如果只是站在高处呐喊,这门语言是

    划时代的,它有多么优秀,但不去实现,又有何用呢?没有Dennis

    M.Ritchie的亲自编码实现,C语言不可能诞生,UNIX操作系统也不可能

    诞生。Linux在聚拢成千上万的开源狂热者对它进行开发和扩展之前,如果没有Linus的编码实现,仅凭他高声呐喊“我要创造一个划时代的操

    作系统”,有用吗?一切的一切都是以编码实现为前提的,代码是我们

    前进的基石。

    这是一个英雄辈出的年代,我们每个人都希望自己被顶礼膜拜,可

    是这需要资本和实力,而我们的实力体现了我们处理技术问题的能力:

    你能写出简单、清晰、高效的代码?——Show it!

    你能架构一个稳定、健壮、快捷的系统?——Do it!

    你能回答一个困扰N多人的问题?——Answer it!

    你能修复一个系统Bug?——Fix it!

    你非常熟悉某个开源产品?——Broadcast it!

    你能提升系统性能?——Tune it!……

    但是,“工欲善其事,必先利其器”,在“善其事”之前,先看看我们

    的“器”是否已经磨得足够锋利了,是否能够在我们前进的路上披荆斩

    棘。无论您将来的职业发展方向是架构师、设计师、分析师、管理者,还是其他职位,只要您还与软件打交道,您就有必要打好技术基础。本

    书对核心的Java编程技术进行了凝练,如果能全部理解并付诸实践,您

    的基础一定会更加牢固。希望本书能帮您打造一支技术战斗力强的团队

    在您的团队中是否出现过以下现象:

    没有人愿意听一场关于编码奥秘的讲座,他们觉得这是浪费时间;

    没有人愿意去思考和探究一个算法,他们觉得这实在是多余,Google完全可以解决;

    没有人愿意主动重构一段代码,他们觉得新任务已经堆积成山

    了,“没有坏,就不要去修它”;

    没有人愿意格式化一下代码,即便只需要按一下【Ctrl+Shift+F】快

    捷键,他们觉得代码写完就完了,何必再去温习;

    没有人愿意花时间去深究一下开源框架,他们觉得够用就好;……

    一支有实力的软件研发团队是建立在技术的基础之上的,团队成员

    之间需要经常地互相交流和切磋,尤其是基于可辨别、可理解的编码问

    题。不可否认,概念和思想也很重要,但我更看重基于代码的交流,因

    为代码不会说谎,比如SOA,10个人至少会有5个答案,但代码就不同

    了,同样的代码,结果只有一个,要么是错的,要么是对的,这才是一

    个技术团队应该有的氛围。本书中提出的这些问题绝大部分可能都是您

    的团队成员在日常的开发中会遇到的,我针对这些问题给出的建议不是

    唯一的解决方案,也许您的团队在讨论这一个个问题的时候能有更好的

    解决办法。希望通过对本书中的这些问题的争辩、讨论和实践能全面提

    升每一位团队成员的技术实力,从而增强整个团队的战斗力!

    本书特色

    深。本书不是一本语法书,它不会教您怎么编写Java代码,但是它

    会告诉您,为什么StringBuilder会比String类效率高,HashMap的自增是

    如何实现的,为什么并行计算一般都是从Executors开始的……不仅仅告

    诉您How(怎么做),而且还告诉您Why(为什么要这样做)。

    广。涉及面广,从编码规则到编程思想,从基本语法到系统框架,从JDK API到开源产品,全部都有涉猎,而且所有的建议都不是纸上谈

    兵,都与真实的场景相结合。

    点。讲解一个知识点,而不是一个知识面,比如多线程,这里不提

    供多线程的解决方案,而是告诉您如何安全地停止一个线程,如何设置

    多线程关卡,什么时候该用lock,什么时候该用synchronize,等等。

    精。简明扼要,直捣黄龙,一个建议就是对一个问题的解释和说

    明,以及提出相关的解决方案,不拖泥带水,只针对一个知识点进行讲

    解。

    畅。本书延续了我一贯的写作风格,行云流水,娓娓道来,每次想

    好了一个主题后,都会先打一个腹稿,思考如何讲才能更流畅。本书不

    是一本很无趣的书,我一直想把它写得生动和优雅,但Code就是

    Code,很多时候容不得深加工,最直接也就是最简洁的。

    这是一本建议书,想想看,在您写代码的时候,有这样一本书籍在

    您的手边,告诉您如何才能编写出优雅而高效的代码,那将是一件多么

    惬意的事情啊!

    本书面向的读者

    寻找“One Line”(一行)解决方案的编码人员。

    希望提升自己编码能力的程序员。

    期望能够在开源世界仗剑而行的有志之士。

    对编码痴情的人。

    总之,只要还在Java圈子里混就有必要阅读本书,不管是程序员、测试人员、分析师、架构师,还是项目经理,都有必要。

    如何阅读本书

    首先声明,本书不是面向初级Java程序员的,在阅读本书之前至少

    要对基本的Java语法有初步了解,最好是参与过几个项目,写过一些代

    码,具备了这些条件,阅读本书才会有更大的收获,才会觉得是一种享

    受。本书的各个章节和各个建议都是相对独立的,所以,您可以从任何

    章节的任何建议开始阅读。强烈建议您将它放在办公桌旁,遇到问题时

    随手翻阅。

    本书附带有大量的源码(下载地址见华章网站

    www.hzbook.com),建议大家在阅读本书时拷贝书中的示例代码,放

    到自己的收藏夹中,以备需要时使用。

    勘误与支持

    首先,我要为书中可能出现的错别字、多意句、歧义句、代码缺陷

    等错误向您真诚地道歉。虽然杨福川、杨绣国两位编辑和我都为此书付

    出了非常大的努力,但可能还是会有一些瑕疵,如果你在阅读本书时发

    现错误或有问题想一起讨论,请发邮件(cbf4life@126.com)给我,我

    会尽快给您回复。

    本书的所有勘误,我都会发表在我的个人博客

    (http:cbf4life.iteye.com)上。

    致谢

    首先,感谢杨福川和杨绣国两位编辑,在他们的编审下,本书才有

    了一个质的飞跃,没有他们的计划和安排,本书不可能出版。

    其次,感谢家人的支持,为了写这本书,用尽了全部的休息时间,很少有时间陪伴父母和妻儿,甚至连吃一顿团圆饭都成了奢望,他们的

    大力支持让我信心满怀、干劲十足。儿子已经6岁了,明白骑在爸爸身

    上是对爸爸的折磨,也知道玩具是可以从网上买到的,“爸爸,给我买

    一个变形金刚……你在网上查呀……今天一定要买……”儿子在不知不

    觉中长大了。

    再次,感谢交通银行“531”工程的所有领导和同事,是他们让我在

    这样超大规模的工程中学习和成长,使自己的技术和技能有了长足的进

    步;感谢我的领导李海宁总经理和周云康高级经理,他们时时迸发出的

    闪光智慧让我受益匪浅;感谢软件开发中心所有同仁对我的帮助和鼓

    励!

    最后,感谢我的朋友王骢,他无偿地把钥匙给我,让我有一个安静的地方思考和写作,有这样的朋友,人生无憾!

    当然,还要感谢您,感谢您对本书的关注。

    再次对本书中可能出现的错误表示歉意,真诚地接受大家的“轰

    炸”!

    秦小波

    2011年8月于上海第1章 Java开发中通用的方法和准则

    The reasonable man adapts himself to the world;the unreasonable one

    persists in trying to adapt the world to himself.

    明白事理的人使自己适应世界;不明事理的人想让世界适应自己。

    ——萧伯纳

    Java的世界丰富又多彩,但同时也布满了荆棘陷阱,大家一不小心

    就可能跌入黑暗深渊,只有在了解了其通行规则后才能使自己在技术的

    海洋里遨游飞翔,恣意驰骋。

    “千里之行始于足下”,本章主要讲述与Java语言基础有关的问题及

    建议的解决方案,例如常量和变量的注意事项、如何更安全地序列化、断言到底该如何使用等。建议1:不要在常量和变量中出现易混淆的字母

    包名全小写,类名首字母全大写,常量全部大写并用下划线分隔,变量采用驼峰命名法(Camel Case)命名等,这些都是最基本的Java编

    码规范,是每个Javaer都应熟知的规则,但是在变量的声明中要注意不

    要引入容易混淆的字母。尝试阅读如下代码,思考一下打印出的i等于

    多少:

    public class Client{

    public static void main(String[]args){

    long i=1l;

    System.out.println(i的两倍是:+(i+i));

    }

    }

    肯定有人会说:这么简单的例子还能出错?运行结果肯定是22!实

    践是检验真理的唯一标准,将其拷贝到Eclipse中,然后Run一下看看,或许你会很奇怪,结果是2,而不是22,难道是Eclipse的显示有问题,少了个“2”?

    因为赋给变量i的数字就是“1”,只是后面加了长整型变量的标示字

    母“l”而已。别说是我挖坑让你跳,如果有类似程序出现在项目中,当你

    试图通过阅读代码来理解作者的思想时,此情此景就有可能会出现。所

    以,为了让您的程序更容易理解,字母“l”(还包括大写字母“O”)尽量

    不要和数字混用,以免使阅读者的理解与程序意图产生偏差。如果字母

    和数字必须混合使用,字母“l”务必大写,字母“O”则增加注释。

    注意 字母“l”作为长整型标志时务必大写。建议2:莫让常量蜕变成变量

    常量蜕变成变量?你胡扯吧,加了final和static的常量怎么可能会变

    呢?不可能二次赋值的呀。真的不可能吗?看我们神奇的魔术,代码如

    下:

    public class Client{

    public static void main(String[]args){

    System.out.println(常量会变哦:+Const.RAND_CONST);

    }

    }

    接口常量

    interface Const{

    这还是常量吗?

    public static fnal int RAND_CONST=new Random.nextInt;

    }

    RAND_CONST是常量吗?它的值会变吗?绝对会变!这种常量的

    定义方式是极不可取的,常量就是常量,在编译期就必须确定其值,不

    应该在运行期更改,否则程序的可读性会非常差,甚至连作者自己都不

    能确定在运行期发生了何种神奇的事情。

    甭想着使用常量会变的这个功能来实现序列号算法、随机种子生

    成,除非这真的是项目中的唯一方案,否则就放弃吧,常量还是当常量

    使用。

    注意务必让常量的值在运行期保持不变。建议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));

    }

    }

    分析一下这段程序: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不相等了。这里可能有读者疑惑了:为什么是整型转

    为浮点,而不是浮点转为整型呢?这就涉及三元操作符类型的转换规

    则:

    若两个操作数不可转换,则不做转换,返回值为Object类型。若两个操作数是明确类型的表达式(比如变量),则按照正常的二

    进制数字来转换,int类型转换为long类型,long类型转换为float类型

    等。

    若两个操作数中有一个是数字S,另外一个是表达式,且其类型标

    示为T,那么,若数字S在T的范围内,则转换为T类型;若S超出了T类

    型的范围,则T转换为S类型(可以参考“建议22”,会对该问题进行展开

    描述)。

    若两个操作数都是直接量数字(Literal)[1]

    ,则返回值类型为范围

    较大者。

    知道是什么原因了,相应的解决办法也就有了:保证三元操作符中

    的两个操作数类型一致,即可减少可能错误的发生。

    [1]“Literal”也译作“字面量”。建议4:避免带有变长参数的方法重载

    在项目和系统的开发中,为了提高方法的灵活度和可复用性,我们

    经常要传递不确定数量的参数到方法中,在Java 5之前常用的设计技巧

    就是把形参定义成Collection类型或其子类类型,或者是数组类型,这种

    方法的缺点就是需要对空参数进行判断和筛选,比如实参为null值和长

    度为0的Collection或数组。而Java 5引入变长参数(varags)就是为了更

    好地提高方法的复用性,让方法的调用者可以“随心所欲”地传递实参数

    量,当然变长参数也是要遵循一定规则的,比如变长参数必须是方法中

    的最后一个参数;一个方法不能定义多个变长参数等,这些基本规则需

    要牢记,但是即使记住了这些规则,仍然有可能出现错误,我们来看如

    下代码:

    public class Client{

    简单折扣计算

    public void calPrice(int price, int discount){

    float knockdownPrice=pricediscount100.0F;

    System.out.println(简单折扣后的价格是:+formateCurrency(knockdownPrice));

    }

    复杂多折扣计算

    public void calPrice(int price, int……discounts){

    float knockdownPrice=price;

    for(int discount:discounts){

    knockdownPrice=knockdownPricediscount100;

    }

    System.out.println(复杂折扣后的价格是:+formateCurrency(knockdownPrice));

    }

    格式化成本的货币形式

    private String formateCurrency(float price){

    return NumberFormat.getCurrencyInstance.format(price100);

    }

    public static void main(String[]args){

    Client client=new Client;

    499元的货物,打75折

    client.calPrice(49900,75);

    }

    }

    这是一个计算商品价格折扣的模拟类,带有两个参数的calPrice方

    法(该方法的业务逻辑是:提供商品的原价和折扣率,即可获得商品的

    折扣价)是一个简单的折扣计算方法,该方法在实际项目中经常会用

    到,这是单一的打折方法。而带有变长参数的calPrice方法则是较复杂

    的折扣计算方式,多种折扣的叠加运算(模拟类是一种比较简单的实

    现)在实际生活中也是经常见到的,比如在大甩卖期间对VIP会员再度

    进行打折;或者当天是你的生日,再给你打个9折,也就是俗话说的“折上折”。

    业务逻辑清楚了,我们来仔细看看这两个方法,它们是重载吗?当

    然是了,重载的定义是“方法名相同,参数类型或数量不同”,很明显这

    两个方法是重载。但是再仔细瞧瞧,这个重载有点特殊:calPrice(int

    price, int……discounts)的参数范畴覆盖了calPrice(int price, int

    discount)的参数范畴。那问题就出来了:对于calPrice(49900,75)这

    样的计算,到底该调用哪个方法来处理呢?

    我们知道Java编译器是很聪明的,它在编译时会根据方法签名

    (Method Signature)来确定调用哪个方法,比如calPrice(499900,75,95)这个调用,很明显75和95会被转成一个包含两个元素的数组,并传递到calPrice(int price, in..discounts)中,因为只有这一个方法签名

    符合该实参类型,这很容易理解。但是我们现在面对的是

    calPrice(49900,75)调用,这个“75”既可以被编译成int类型的“75”,也可以被编译成int数组“{75}”,即只包含一个元素的数组。那到底该调

    用哪一个方法呢?

    我们先运行一下看看结果,运行结果是:

    简单折扣后的价格是:¥374.25。

    看来是调用了第一个方法,为什么会调用第一个方法,而不是第二

    个变长参数方法呢?因为Java在编译时,首先会根据实参的数量和类型

    (这里是2个实参,都为int类型,注意没有转成int数组)来进行处理,也就是查找到calPrice(int price, int discount)方法,而且确认它是否符

    合方法签名条件。现在的问题是编译器为什么会首先根据2个int类型的

    实参而不是1个int类型、1个int数组类型的实参来查找方法呢?这是个好

    问题,也非常好回答:因为int是一个原生数据类型,而数组本身是一个

    对象,编译器想要“偷懒”,于是它会从最简单的开始“猜想”,只要符合

    编译条件的即可通过,于是就出现了此问题。

    问题是阐述清楚了,为了让我们的程序能被“人类”看懂,还是慎重

    考虑变长参数的方法重载吧,否则让人伤脑筋不说,说不定哪天就陷入

    这类小陷阱里了。建议5:别让null值和空值威胁到变长方法

    上一建议讲解了变长参数的重载问题,本建议还会继续讨论变长参

    数的重载问题。上一建议的例子是变长参数的范围覆盖了非变长参数的

    范围,这次我们从两个都是变长参数的方法说起,代码如下:

    public class Client{

    public void methodA(String str, Integer……is){

    }

    public void methodA(String str, String……strs){

    }

    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);

    }

    }

    两个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,即懒人原则),按照此规则设计的方法应该很

    容易调用,可是现在在遵循规范的情况下,程序竟然出错了,这对设计

    者和开发者而言都是应该严禁出现的。

    对于client.methodA(china,null)方法,直接量null是没有类型的,虽然两个methodA方法都符合调用请求,但不知道调用哪一个,于

    是报错了。我们来体会一下它的坏味道:除了不符合上面的懒人原则

    外,这里还有一个非常不好的编码习惯,即调用者隐藏了实参类型,这

    是非常危险的,不仅仅调用者需要“猜测”该调用哪个方法,而且被调用

    者也可能产生内部逻辑混乱的情况。对于本例来说应该做如下修改:

    public static void main(String[]args){

    Client client=new Client;

    String[]strs=null;

    client.methodA(China,strs);

    }

    也就是说让编译器知道这个null值是String类型的,编译即可顺利通

    过,也就减少了错误的发生。建议6:覆写变长方法也循规蹈矩

    在Java中,子类覆写父类中的方法很常见,这样做既可以修正Bug

    也可以提供扩展的业务功能支持,同时还符合开闭原则(Open-Closed

    Principle),我们来看一下覆写必须满足的条件:

    重写方法不能缩小访问权限。

    参数列表必须与被重写方法相同。

    返回类型必须与被重写方法的相同或是其子类。

    重写方法不能抛出新的异常,或者超出父类范围的异常,但是可以

    抛出更少、更有限的异常,或者不抛出异常。

    估计你已经猜测出下面要讲的内容了,为什么“参数列表必须与被

    重写方法的相同”采用不同的字体,这其中是不是有什么玄机?是的,还真有那么一点点小玄机。参数列表相同包括三层意思:参数数量相

    同、类型相同、顺序相同,看上去好像没什么问题,那我们来看一个例

    子,业务场景与上一个建议相同,商品打折,代码如下:

    public class Client{

    public static void main(String[]args){

    向上转型

    Base base=new Sub;

    base.fun(100,50);

    不转型

    Sub sub=new Sub;

    sub.fun(100,50);

    }

    }

    基类

    class Base{

    void fun(int price, int……discounts){

    System.out.println(Base……fun);

    }

    }

    子类,覆写父类方法

    class Sub extends Base{

    @Override

    void fun(int price, int[]discounts){

    System.out.println(Sub……fun);

    }

    }请问:该程序有问题吗?—编译通不过。那问题出在什么地方呢?

    @Override注解吗?非也,覆写是正确的,因为父类的calPrice编译

    成字节码后的形参是一个int类型的形参加上一个int数组类型的形参,子

    类的参数列表也与此相同,那覆写是理所当然的了,所以加上

    @Override注解没有问题,只是Eclipse会提示这不是一种很好的编码风

    格。

    难道是“sub.fun(100,50)”这条语句?正解,确实是这条语句报

    错,提示找不到fun(int, int)方法。这太奇怪了:子类继承了父类的所

    有属性和方法,甭管是私有的还是公开的访问权限,同样的参数、同样

    的方法名,通过父类调用没有任何问题,通过子类调用却编译通不过,为啥?难道是没继承下来?或者子类缩小了父类方法的前置条件?那如

    果是这样,就不应该覆写,@Override就应该报错,真是奇妙的事情!

    事实上,base对象是把子类对象Sub做了向上转型,形参列表是由

    父类决定的,由于是变长参数,在编译时,“base.fun(100,50)”中

    的“50”这个实参会被编译器“猜测”而编译成“{50}”数组,再由子类Sub执

    行。我们再来看看直接调用子类的情况,这时编译器并不会把“50”做类

    型转换,因为数组本身也是一个对象,编译器还没有聪明到要在两个没

    有继承关系的类之间做转换,要知道Java是要求严格的类型匹配的,类

    型不匹配编译器自然就会拒绝执行,并给予错误提示。

    这是个特例,覆写的方法参数列表竟然与父类不相同,这违背了覆

    写的定义,并且会引发莫名其妙的错误。所以读者在对变长参数进行覆

    写时,如果要使用此类似的方法,请找个小黑屋仔细想想是不是一定要

    如此。

    注意 覆写的方法参数与父类相同,不仅仅是类型、数量,还包括

    显示形式。建议7:警惕自增的陷阱

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

    i++和++i, i++表示的是先赋值后加1,++i是先加1后赋值,这样理解了很

    多年也没出现问题,直到遇到如下代码,我才怀疑我的理解是不是错

    了:

    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次的循环也是一样的,最终你会发现count的值始终没有改变,仍然保持着最初的状态。

    此例中代码作者的本意是希望count自增,所以想当然地认为赋值

    给自身就成了,不曾想掉到Java自增的陷阱中了。解决方法很简单,只

    要把“count=count++”修改为“count++”即可。该问题在不同的语言环境

    有不同的实现:C++中“count=count++”与“count++”是等效的,而在PHP

    中则保持着与Java相同的处理方式。每种语言对自增的实现方式各不

    同,读者有兴趣可以多找几种语言测试一下,思考一下原理。

    下次如果看到某人T恤上印着“i=i++”,千万不要鄙视他,记住,能

    够以不同的语言解释清楚这句话的人绝对不简单,应该表现出“如滔滔

    江水”般的敬仰,心理默念着“高人,绝世高人哪”。建议8:不要让旧语法困扰你

    N多年前接手了一个除了源码以外什么都没有的项目,没需求、没

    文档、没设计,原创者也已鸟兽散了,我们只能通过阅读源码来进行维

    护。期间,同事看到一段很“奇妙”的代码,让大家帮忙分析,代码片段

    如下:

    public class Client{

    public static void main(String[]args){

    数据定义及初始化

    int fee=200;

    其他业务处理

    saveDefault:save(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语法,但还是保留了该关键字,只是不进行语义处理

    而已,与此类似的还有const关键字。

    Java中虽然没有了goto关键字,但是扩展了break和continue关键

    字,它们的后面都可以加上标号做跳转,完全实现了goto功能,同时也

    把goto的诟病带了进来,所以我们在阅读大牛的开源程序时,根本就看

    不到break或continue后跟标号的情况,甚至是break和continue都很少看

    到,这是提高代码可读性的一剂良药,旧语法就让它随风而去吧!建议9:少用静态导入

    从Java 5开始引入了静态导入语法(import static),其目是为了减

    少字符输入量,提高代码的可阅读性,以便更好地理解程序。我们先来

    看一个不使用静态导入的例子,也就是一般导入:

    public class MathUtils{

    计算圆面积

    public static double calCircleArea(double r){

    return Math.PIrr;

    }

    计算球面积

    public static double calBallArea(double r){

    return 4Math.PIrr;

    }

    }

    这是很简单的数学工具类,我们在这两个计算面积的方法中都引入

    了java.lang.Math类(该类是默认导入的)中的PI(圆周率)常量,而

    Math这个类写在这里有点多余,特别是如果MathUtils中的方法比较多

    时,如果每次都要敲入Math这个类,繁琐且多余,静态导入可解决此类

    问题,使用静态导入后的程序如下:

    import static java.lang.Math.PI;

    public class MathUtils{

    计算圆面积

    public static double calCircleArea(double r){

    return PIrr;

    }

    计算球面积

    public static double calBallArea(double r){

    return 4PIrr;

    }

    }

    静态导入的作用是把Math类中的PI常量引入到本类中,这会使程序

    更简单,更容易阅读,只要看到PI就知道这是圆周率,不用每次都要把

    类名写全了。但是,滥用静态导入会使程序更难阅读,更难维护。静态

    导入后,代码中就不用再写类名了,但是我们知道类是“一类事物的描

    述”,缺少了类名的修饰,静态属性和静态方法的表象意义可以被无限

    放大,这会让阅读者很难弄清楚其属性或方法代表何意,甚至是哪一个

    类的属性(方法)都要思考一番(当然,IDE友好提示功能是另说),特别是在一个类中有多个静态导入语句时,若还使用了(星号)通配

    符,把一个类的所有静态元素都导入进来了,那简直就是恶梦。我们来

    看一段例子:

    import static java.lang.Double.;

    import static java.lang.Math.;

    import static java.lang.Integer.;

    import static java.text.NumberFormat.;

    public class Client{

    输入半径和精度要求,计算面积

    public static void main(String[]args){

    double s=PIparseDouble(args[0]);

    NumberFormat nf=getInstance;

    nf.setMaximumFractionDigits(parseInt(args[1]));

    formatMessage(nf.format(s));

    }

    格式化消息输出

    public static void formatMessage(String s){

    System.out.println(圆面积是:+s);

    }

    }

    就这么一段程序,看着就让人火大:常量PI,这知道,是圆周率;

    parseDouble方法可能是Double类的一个转换方法,这看名称也能猜测

    到。那紧接着的getInstance方法是哪个类的?是Client本地类?不对呀,没有这个方法,哦,原来是NumberFormate类的方法,这和

    formateMessage本地方法没有任何区别了—这代码也太难阅读了,非机

    器不可阅读。

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

    不使用(星号)通配符,除非是导入静态常量类(只包含常量的

    类或接口)。

    方法名是具有明确、清晰表象意义的工具类。

    何为具有明确、清晰表象意义的工具类?我们来看看JUnit 4中使用

    的静态导入的例子,代码如下:

    import static org.junit.Assert.;

    public class DaoTest{

    @Test

    public void testInsert{

    断言

    assertEquals(foo,foo);

    assertFalse(Boolean.FALSE);}

    }

    我们从程序中很容易判断出assertEquals方法是用来断言两个值是否

    相等的,assertFalse方法则是断言表达式为假,如此确实减少了代码

    量,而且代码的可读性也提高了,这也是静态导入用到正确地方所带来

    的好处。建议10:不要在本类中覆盖静态导入的变量和方法

    如果一个类中的方法及属性与静态导入的方法及属性重名会出现什

    么问题呢?我们先来看一个正常的静态导入,代码如下:

    import static java.lang.Math.PI;

    import static java.lang.Math.abs;

    public class Client{

    public static void main(String[]args){

    System.out.println(PI=+PI);

    System.out.println(abs(100)=+abs(-100));

    }

    }

    很简单的例子,打印出静态常量PI值,计算-100的绝对值。现在的

    问题是:如果我们在Client类中也定义了PI常量和abs方法,会出现什么

    问题?代码如下:

    import static java.lang.Math.PI;

    import static java.lang.Math.abs;

    public class Client{

    常量名与静态导入的PI相同

    public fnal static String PI=祖冲之;

    方法名与静态导入的相同

    public static int abs(int abs){

    return 0;

    }

    public static void main(String[]args){

    System.out.println(PI=+PI);

    System.out.println(abs(100)=+abs(-100));

    }

    }

    以上代码中,定义了一个PI字符串类型的常量,又定义了一个abs

    方法,与静态导入的相同。首先说好消息:编译器没有报错,接下来是

    不好的消息了:我们不知道哪个属性和哪个方法被调用了,因为常量名

    和方法名相同,到底调用了哪一个方法呢?我们运行一下看看结果:

    PI=祖冲之

    abs(100)=0

    很明显是本地的属性和方法被引用了,为什么不是Math类中的属性

    和方法呢?那是因为编译器有一个“最短路径”原则:如果能够在本类中查找到的变量、常量、方法,就不会到其他包或父类、接口中查找,以

    确保本类中的属性、方法优先。

    因此,如果要变更一个被静态导入的方法,最好的办法是在原始类

    中重构,而不是在本类中覆盖。建议11:养成良好习惯,显式声明UID

    我们编写一个实现了Serializable接口(序列化标志接口)的类,Eclipse马上就会给一个黄色警告:需要增加一个Serial Version ID。为什

    么要增加?它是怎么计算出来的?有什么用?本章就来解释该问题。

    类实现Serializable接口的目的是为了可持久化,比如网络传输或本

    地存储,为系统的分布和异构部署提供先决支持条件。若没有序列化,现在我们熟悉的远程调用、对象数据库都不可能存在,我们来看一个简

    单的序列化类:

    public class Person implements Serializable{

    private String name;

    name属性的gettersetter方法省略

    }

    这是一个简单JavaBean,实现了Serializable接口,可以在网络上传

    输,也可以本地存储然后读取。这里我们以Java消息服务(Java

    Message Service)方式传递该对象(即通过网络传递一个对象),定义

    在消息队列中的数据类型为ObjectMessage,首先定义一个消息的生产者

    (Producer),代码如下:

    public class Producer{

    public static void main(String[]args)throws Exception{

    Person person=new Person;

    person.setName(混世魔王);

    序列化,保存到磁盘上

    SerializationUtils.writeObject(person);

    }

    }

    这里引入了一个工具类SerializationUtils,其作用是对一个类进行序

    列化和反序列化,并存储到硬盘上(模拟网络传输),其代码如下:

    public class SerializationUtils{

    private static String FILE_NAME=c:obj.bin;

    序列化

    public static void 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{

    Object Input in put=new Object Input Stream(new

    FileInputStream(FILE_NAME));

    obj=input.readObject;

    input.close;

    }catch(Exception e){

    e.printStackTrace;

    }

    return obj;

    }

    }

    通过对象序列化过程,把一个对象从内存块转化为可传输的数据

    流,然后通过网络发送到消息消费者(Consumer)那里,并进行反序列

    化,生成实例对象,代码如下:

    public class Consumer{

    public static void main(String[]args)throws Exception{

    反序列化

    Person p=(Person)SerializationUtils.readObject;

    System.out.println(name=+p.getName);

    }

    }

    这是一个反序列化过程,也就是对象数据流转换为一个实例对象的

    过程,其运行后的输出结果为:混世魔王。这太easy了,是的,这就是

    序列化和反序列化典型的demo。但此处隐藏着一个问题:如果消息的

    生产者和消息的消费者所参考的类(Person类)有差异,会出现何种神

    奇事件?比如:消息生产者中的Person类增加了一个年龄属性,而消费

    者没有增加该属性。为啥没有增加?!因为这是个分布式部署的应用,你甚至都不知道这个应用部署在何处,特别是通过广播(broadcast)方

    式发送消息的情况,漏掉一两个订阅者也是很正常的。

    在这种序列化和反序列化的类不一致的情形下,反序列化时会报一

    个InvalidClassException异常,原因是序列化和反序列化所对应的类版本

    发生了变化,JVM不能把数据流转换为实例对象。接着刨根问底:JVM

    是根据什么来判断一个类版本的呢?好问题,通过SerialVersionUID,也叫做流标识符(Stream Unique

    Identifier),即类的版本定义的,它可以显式声明也可以隐式声明。显

    式声明格式如下:

    private static final long serialVersionUID=XXXXXL;

    而隐式声明则是我不声明,你编译器在编译的时候帮我生成。生成

    的依据是通过包名、类名、继承关系、非私有的方法和属性,以及参

    数、返回值等诸多因子计算得出的,极度复杂,基本上计算出来的这个

    值是唯一的。

    serialVersionUID如何生成已经说明了,我们再来看看

    serialVersionUID的作用。JVM在反序列化时,会比较数据流中的

    serialVersionUID与类的serialVersionUID是否相同,如果相同,则认为

    类没有发生改变,可以把数据流load为实例对象;如果不相同,对不

    起,我JVM不干了,抛个异常InvalidClassException给你瞧瞧。这是一个

    非常好的校验机制,可以保证一个对象即使在网络或磁盘中“滚过”一

    次,仍能做到“出淤泥而不染”,完美地实现类的一致性。

    但是,有时候我们需要一点特例场景,例如:我的类改变不大,JVM是否可以把我以前的对象反序列化过来?就是依靠显式声明

    serialVersionUID,向JVM撒谎说“我的类版本没有变更”,如此,我们编

    写的类就实现了向上兼容。我们修改一下上面的Person类,代码如下:

    public class Person implements Serializable{

    private static fnal long serialVersionUID=55799L;

    其他保持不变

    }

    刚开始生产者和消费者持有的Person类版本一致,都是V1.0,某天

    生产者的Person类版本变更了,增加了一个“年龄”属性,升级为V2.0,而由于种种原因(比如程序员疏忽、升级时间窗口不同等)消费端的

    Person还保持为V1.0版本,代码如下:

    public class Person implements Serializable{

    private static final long serialVersionUID=5799L;

    private int age;

    age、name的gettersetter方法省略

    }此时虽然生产者和消费者对应的类版本不同,但是显式声明的

    serialVersionUID相同,反序列化也是可以运行的,所带来的业务问题就

    是消费端不能读取到新增的业务属性(age属性)而已。

    通过此例,我们的反序列化实现了版本向上兼容的功能,使用V1.0

    版本的应用访问了一个V2.0版本的对象,这无疑提高了代码的健壮性。

    我们在编写序列化类代码时,随手加上serialVersionUID字段,也不会给

    我们带来太多的工作量,但它却可以在关键时候发挥异乎寻常的作用。

    注意 显式声明serialVersionUID可以避免对象不一致,但尽量不要

    以这种方式向JVM“撒谎”。建议12:避免用序列化类在构造函数中为不变量赋值

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

    public class Person implements Serializable{

    private static final long serialVersionUID=71282334L;

    不变量

    public fnal String name=混世魔王;

    }

    这个Person类(此时V1.0版本)被序列化,然后存储在磁盘上,在

    反序列化时name属性会重新计算其值(这与static变量不同,static变量

    压根就没有保存到数据流中),比如name属性修改成了“德天使”(版本

    升级为V2.0),那么反序列化对象的name值就是“德天使”。保持新旧对

    象的final变量相同,有利于代码业务逻辑统一,这是序列化的基本规则

    之一,也就是说,如果final属性是一个直接量,在反序列化时就会重新

    计算。对这基本规则不多说,我们要说的是final变量另外一种赋值方

    式:通过构造函数赋值。代码如下:

    public class Person implements Serializable{

    private static final long serialVersionUID=91282334L;

    不变量初始不赋值

    public final String name;

    构造函数为不变量赋值

    public Person{

    name=混世魔王;

    }

    }

    这也是我们常用的一种赋值方式,可以把这个Person类定义为版本

    V1.0,然后进行序列化,看看有什么问题没有,序列化的代码如下所

    示:

    public class Serialize{

    public static void main(String[]args){

    序列化以持久保存

    SerializationUtils.writeObject(new Person);

    }

    }Person的实例对象保存到了磁盘上,它是一个贫血对象(承载业务

    属性定义,但不包含其行为定义),我们做一个简单的模拟,修改一下

    name值代表变更,要注意的是serialVersionUID保持不变,修改后的代

    码如下:

    public class Person implements Serializable{

    private static final long serialVersionUID=91282334L;

    不变量初始不赋值

    public final String name;

    构造函数为不变量赋值

    public Person{

    name=德天使;

    }

    }

    此时Person类的版本是V2.0,但serialVersionUID没有改变,仍然可

    以反序列化,其代码如下:

    public class Deserialize{

    public static void main(String[]args){

    反序列化

    Person p=(Person)SerializationUtils.readObject;

    System.out.println(p.name);

    }

    }

    现在问题来了:打印的结果是什么?是混世魔王还是德天使?

    答案即将揭晓,答案是:混世魔王。

    final类型的变量不是会重新计算吗?答案应该是“德天使”才对啊,为什么会是“混世魔王”?这是因为这里触及了反序列化的另一个规则:

    反序列化时构造函数不会执行。

    反序列化的执行过程是这样的:JVM从数据流中获取一个Object对

    象,然后根据数据流中的类文件描述信息(在序列化时,保存到磁盘的

    对象文件中包含了类描述信息,注意是类描述信息,不是类)查看,发

    现是final变量,需要重新计算,于是引用Person类中的name值,而此时

    JVM又发现name竟然没有赋值,不能引用,于是它很“聪明”地不再初始

    化,保持原值状态,所以结果就是“混世魔王”了。

    读者不要以为这样的情况很少发生,如果使用Java开发过桌面应用,特别是参与过对性能要求较高的项目(比如交易类项目),那么很

    容易遇到这样的问题。比如一个CS结构的在线外汇交易系统,要求提

    供24小时的联机服务,如果在升级的类中有一个final变量是构造函数赋

    值的,而且新旧版本还发生了变化,则在应用请求热切的过程中(非常

    短暂,可能只有30秒),很可能就会出现反序列化生成的final变量值与

    新产生的实例值不相同的情况,于是业务异常就产生了,情况严重的话

    甚至会影响交易数据,那可是天大的事故了。

    注意 在序列化类中,不使用构造函数为final变量赋值。建议13:避免为final变量复杂赋值

    为final变量赋值还有一种方式:通过方法赋值,即直接在声明时通

    过方法返回值赋值。还是以Person类为例来说明,代码如下:

    public class Person implements Serializable{

    private static final long serialVersionUID=91282334L;

    通过方法返回值为final变量赋值

    public fnal String name=initName;

    初始化方法名

    public String initName{

    return混世魔王;

    }

    }

    name属性是通过initName方法的返回值赋值的,这在复杂类中经常

    用到,这比使用构造函数赋值更简洁、易修改,那么如此用法在序列化

    时会不会有问题呢?我们一起来看看。Person类写好了(定义为V1.0版

    本),先把它序列化,存储到本地文件,其代码与上一建议的Serialize

    类相同,不再赘述。

    现在,Person类的代码需要修改,initName的返回值也改变了,代

    码如下:

    public class Person implements Serializable{

    private static final long serialVersionUID=91282334L;

    通过方法返回值为final变量赋值

    public final String name=initName;

    初始化方法名

    public String initName{

    return德天使;

    }

    }

    上段代码仅仅修改了initName的返回值(Person类为V2.0版本),也就是说通过new生成的Person对象的final变量值都是“德天使”。那么我

    们把之前存储在磁盘上的实例加载上来,name值会是什么呢?

    结果是:混世魔王。很诧异,上一建议说过final变量会被重新赋

    值,但是这个例子又没有重新赋值,为什么?

    上个建议所说final会被重新赋值,其中的“值”指的是简单对象。简单对象包括:8个基本类型,以及数组、字符串(字符串情况很复杂,不通过new关键字生成String对象的情况下,final变量的赋值与基本类型

    相同),但是不能方法赋值。

    其中的原理是这样的,保存到磁盘上(或网络传输)的对象文件包

    括两部分:

    (1)类描述信息

    包括包路径、继承关系、访问权限、变量描述、变量访问权限、方

    法签名、返回值,以及变量的关联类信息。要注意的一点是,它并不是

    class文件的翻版,它不记录方法、构造函数、static变量等的具体实现。

    之所以类描述会被保存,很简单,是因为能去也能回嘛,这保证反序列

    化的健壮运行。

    (2)非瞬态(transient关键字)和非静态(static关键字)的实例变

    量值

    注意,这里的值如果是一个基本类型,好说,就是一个简单值保存

    下来;如果是复杂对象,也简单,连该对象和关联类信息一起保存,并

    且持续递归下去(关联类也必须实现Serializable接口,否则会出现序列

    化异常),也就是说递归到最后,其实还是基本数据类型的保存。

    正是因为这两点原因,一个持久化后的对象文件会比一个class类文

    件大很多,有兴趣的读者可以自己写个Hello word程序检验一下,其体

    积确实膨胀了不少。

    总结一下,反序列化时final变量在以下情况下不会被重新赋值:

    通过构造函数为final变量赋值。

    通过方法返回值为final变量赋值。

    final修饰的属性不是基本类型。建议14:使用序列化类的私有方法巧妙解决部分属性持久化问

    题

    部分属性持久化问题看似很简单,只要把不需要持久化的属性加上

    瞬态关键字(transient关键字)即可。这是一种解决方案,但有时候行

    不通。例如一个计税系统和人力资源系统(HR系统)通过

    RMI(Remote Method Invocation,远程方法调用)对接,计税系统需要

    从HR系统获得人员的姓名和基本工资,以作为纳税的依据,而HR系统

    的工资分为两部分:基本工资和绩效工资,基本工资没什么秘密,根据

    工作岗位和年限自己都可以计算出来,但绩效工资却是保密的,不能泄

    露到外系统,很明显这是两个相互关联的类。先来看薪水类Salary类的

    代码:

    public class Salary implements Serializable{

    private static final long serialVersionUID=44663L;

    基本工资

    private int basePay;

    绩效工资

    private int bonus;

    public Salary(int_basePay, int_bonus){

    basePay=_basePay;

    bonus=_bonus;

    }

    gettersetter方法省略

    }

    Peron类与Salary类是关联关系,代码如下:

    public class Person implements Serializable{

    private static final long serialVersionUID=60407L;

    姓名

    private String name;

    薪水

    private Salary salary;

    public Person(String_name, Salary_salary){

    name=_name;

    salary=_salary;

    }

    gettersetter方法省略

    }

    这是两个简单的JavaBean,都实现了Serializable接口,都具备了持

    久化条件。首先计税系统请求HR系统对某一个Person对象进行序列化,把人员和工资信息传递到计税系统中,代码如下:

    public class Serialize{

    public static void main(String[]args){

    基本工资1000元,绩效工资2500元

    Salary salary=new Salary(1000,2500);

    记录人员信息

    Person person=new Person(张三,salary);

    HR系统持久化,并传递到计税系统

    SerializationUtils.writeObject(person);

    }

    }

    在通过网络传送到计税系统后,进行反序列化,代码如下:

    public class Deserialize{

    public static void main(String[]args){

    技术系统反序列化,并打印信息

    Person p=(Person)SerializationUtils.readObject;

    StringBuffer sb=new StringBuffer;

    sb.append(姓名:+p.getName);

    sb.append(\t基本工资:+p.getSalary.getBasePay);

    sb.append(\t绩效工资:+p.getSalary.getBonus);

    System.out.println(sb);

    }

    }

    打印出的结果很简单:

    姓名:张三 基本工资:1000 绩效工资:2500。

    但是这不符合需求,因为计税系统只能从HR系统中获得人员姓名

    和基本工资,而绩效工资是不能获得的,这是个保密数据,不允许发生

    泄露。怎么解决这个问题呢?你可能马上会想到四种方案:

    (1)在bonus前加上transient关键字

    这是一个方法,但不是一个好方法,加上transient关键字就标志着

    Salary类失去了分布式部署的功能,它可是HR系统最核心的类了,一旦

    遭遇性能瓶颈,想再实现分布式部署就不可能了,此方案否定。

    (2)新增业务对象

    增加一个Person4Tax类,完全为计税系统服务,就是说它只有两个属性:姓名和基本工资。符合开闭原则,而且对原系统也没有侵入性,只是增加了工作量而已。这是个方法,但不是最优方法。

    (3)请求端过滤

    在计税系统获得Person对象后,过滤掉Salary的bonus属性,方案可

    行但不合规矩,因为HR系统中的Salary类安全性竟然让外系统(计税系

    统)来承担,设计严重失职。

    (4)变更传输契约

    例如改用XML传输,或者重建一个Web Service服务。可以做,但

    成本太高。

    可能有读者会说了,你都在说别人的方案不好,你提供个优秀的方

    案看看!好的,这就展示一个优秀的方案。其中,实现了Serializable接

    口的类可以实现两个私有方法:writeObject和readObject,以影响和控制

    序列化和反序列化的过程。我们把Person类稍做修改,看看如何控制序

    列化和反序列化,代码如下:

    public class Person implements Serializable{

    private static final long serialVersionUID=60407L;

    姓名

    private String name;

    薪水

    private transient Salary salary;

    public Person(String_name, Salary_salary){

    name=_name;

    salary=_salary;

    }

    序列化委托方法

    private void writeObject(java.io.ObjectOutputStream out)throws IOException{

    out.defaultWriteObject;

    out.writeInt(salary.getBasePay);

    }

    反序列化时委托方法

    private void readObject(java.io.ObjectInputStream in)throws IOException, Class-

    NotFoundException{

    in.defaultReadObject;

    salary=new Salary(in.readInt,0);

    }

    }

    其他代码不做任何改动,我们先运行看看,结果为:

    姓名:张三 基本工资:1000 绩效工资:0。我们在Person类中增加了writeObject和readObject两个方法,并且访

    问权限都是私有级别,为什么这会改变程序的运行结果呢?其实这里使

    用了序列化独有的机制:序列化回调。Java调用ObjectOutputStream类把

    一个对象转换成流数据时,会通过反射(Reflection)检查被序列化的类

    是否有writeObject方法,并且检查其是否符合私有、无返回值的特性。

    若有,则会委托该方法进行对象序列化,若没有,则由

    ObjectOutputStream按照默认规则继续序列化。同样,在从流数据恢复

    成实例对象时,也会检查是否有一个私有的readObject方法,如果有,则会通过该方法读取属性值。此处有几个关键点要说明:

    (1)out. defaultWriteObject

    告知JVM按照默认的规则写入对象,惯例的写法是写在第一句话

    里。

    (2)in. defaultReadObject

    告知JVM按照默认规则读入对象,惯例的写法也是写在第一句话

    里。

    (3)out. writeXX和in.readXX

    分别是写入和读出相应的值,类似一个队列,先进先出,如果此处

    有复杂的数据逻辑,建议按封装Collection对象处理。

    可能有读者会提出,这似乎不是一种优雅的处理方案呀,为什么

    JDK没有对此提供一个更好的解决办法呢?比如访问者模式,或者设置

    钩子函数(Hook),完全可以更优雅地解决此类问题。我查阅了大量

    的文档,得出的结论是:无解,只能说这是一个可行的解决方案而已。

    再回到我们的业务领域,通过上述方法重构后,其代码的修改量减

    少了许多,也优雅了许多。可能你又要反问了:如此一来,Person类也

    失去了分布式部署的能力啊。确实是,但是HR系统的难点和重点是薪

    水计算,特别是绩效工资,它所依赖的参数很复杂(仅从数量上说就有

    上百甚至上千种),计算公式也不简单(一般是引入脚本语言,个性化

    公式定制),而相对来说Person类基本上都是“静态”属性,计算的可能

    性不大,所以即使为性能考虑,Person类为分布式部署的意义也不大。建议15:break万万不可忘

    我们经常会写一些转换类,比如货币转换、日期转换、编码转换

    等,在金融领域里用到最多的要数中文数字转换了,比如把“1”转换

    为“壹”,不过,开源世界是不会提供此工具类的,因为它太贴合中国文

    化了,要转换还是得自己动手写,代码片段如下:

    public class Client{

    public static void main(String[]args){

    System.out.println(2=+toChineseNumberCase(2));

    }

    把阿拉伯数字翻译成中文大写数字

    public static String toChineseNumberCase(int n){

    String chineseNumber=;

    switch(n){

    case 0:chineseNumber=零;

    case 1:chineseNumber=壹;

    case 2:chineseNumber=贰;

    case 3:chineseNumber=叁;

    case 4:chineseNumber=肆;

    case 5:chineseNumber=伍;

    case 6:chineseNumber=陆;

    case 7:chineseNumber=柒;

    case 8:chineseNumber=捌;

    case 9:chineseNumber=玖;

    }

    return chineseNumber;

    }

    }

    这是一个简单的转换类,并没有完整实现,只是一个金融项目片

    段。如此简单的代码应该不会有错吧,我们运行看看,结果是:2=玖。

    恩?错了?回头再来看程序,马上醒悟了:每个case语句后面少加

    了break关键字。程序从“case 2”后面的语句开始执行,直到找到最近的

    break语句结束,但可惜的是我们的程序中没有break语句,于是在程序

    执行的过程中,chineseNumber的赋值语句会多次执行,会从等于“贰”、等于“叁”、等于“肆”,一直变换到等于“玖”,switch语句执行结束了,于是结果也就如此了。

    此类问题发生得非常频繁,但也很容易发现,只要做一下单元测试

    (Unit Test),问题立刻就会被发现并解决掉,但如果是在一堆的case

    语句中,其中某一条漏掉了break关键字,特别是在单元测试覆盖率不

    够高的时候(为什么不够高?在大点的项目中蹲过坑、打过仗的兄弟们可能都知道,项目质量是与项目工期息息相关的,而项目工期往往不是

    由项目人员决定的,所以如果一个项目的单元测试覆盖率能够达到

    60%,你就可以笑了),也就是说分支条件可能覆盖不到的时候,那就

    会在生产中出现大事故了。

    我曾遇到过一个类似的事故,那是开发一个通过会员等级决定相关

    费率的系统,由于会员等级有100多个,所以测试时就采用了抽样测试

    的方法,测试时一切顺利,直到系统上线后,财务报表系统发现一个小

    概率的会员费率竟然出奇的低,于是就跟踪分析,发现是少了一个

    break,此事不仅造成甲方经济上的损失,而且在外部也产生了不良的

    影响,最后该代码的作者被辞退了,测试人员、质量负责人、项目经理

    都做了相应的处罚。希望读者能引以为戒,记住在case语句后面随手写

    上break,养成良好的习惯。

    对于此类问题,还有一个最简单的解决办法:修改IDE的警告级

    别,例如在Eclipse中,可以依次点击

    Performaces→Java→Compiler→ErrorsWarnings→Potential Programming

    problems,然后修改‘switch’case fall-through为Errors级别,如果你胆敢

    不在case语句中加入break,那Eclipse直接就报个红叉给你看,这样就可

    以完全避免该问题的发生了。建议16:易变业务使用脚本语言编写

    Java世界一直在遭受着异种语言的入侵,比如PHP、Ruby、Groovy、JavaScript等,这些“入侵者”都有一个共同特征:全是同一类语

    言—脚本语言,它们都是在运行期解释执行的。为什么Java这种强编译

    型语言会需要这些脚本语言呢?那是因为脚本语言的三大特征,如下所

    示:

    灵活。脚本语言一般都是动态类型,可以不用声明变量类型而直接

    使用,也可以在运行期改变类型。

    便捷。脚本语言是一种解释型语言,不需要编译成二进制代码,也

    不需要像Java一样生成字节码。它的执行是依靠解释器解释的,因此在

    运行期变更代码非常容易,而且不用停止应用。

    简单。只能说部分脚本语言简单,比如Groovy, Java程序员若转到

    Groovy程序语言上,只需要两个小时,看完语法说明,看完Demo即可

    使用了,没有太多的技术门槛。

    脚本语言的这些特性是Java所缺少的,引入脚本语言可以使Java更

    强大,于是Java 6开始正式支持脚本语言。但是因为脚本语言比较多,Java的开发者也很难确定该支持哪种语言,于是JCP(Java Community

    Process)很聪明地提出了JSR223规范,只要符合该规范的语言都可以在

    Java平台上运行(它对JavaScript是默认支持的),诸位读者有兴趣的话

    可以自己写个脚本语言,然后再实现ScriptEngine,即可在Java平台上运

    行。

    我们来分析一个案例,展现一下脚本语言是如何实现“拥抱变

    化”的。咱们编写一套模型计算公式,预测下一个工作日的股票走势

    (如果真有,那巴菲特就羞愧死了),即把国家政策、汇率、利率、地

    域系数等参数输入到公式中,然后计算出明天这支股票是涨还是跌,该

    公式是依靠历史数据推断而来的,会根据市场环境逐渐优化调整,也就

    是逐渐趋向“真理”的过程,在此过程中,公式经常需要修改(这里的修

    改不仅仅是参数修改,还涉及公式的算法修改),如果把这个公式写到

    一个类中(或者几个类中),就需要经常发布重启等操作(比如业务中

    断,需要冒烟测试(Smoke Testing)等),使用脚本语言则可以很好地

    简化这一过程,我们写一个简单公式来模拟一下,代码如下:function formula(var1,var2){

    return var1+var2factor;

    }

    这就是一个简单的脚本语言函数,可能你会很疑惑:factor(因

    子)这个变量是从哪儿来的?它是从上下文来的,类似于一个运行的环

    境变量。该JavaScript保存在C:model.js中。下一步Java需要调用

    JavaScript公式,代码如下:

    public static void main(String[]args)throws Exception{

    获得一个JavaScript的执行引擎

    ScriptEngine engine=new ScriptEngineManager.getEngineByName(javascript);

    建立上下文变量

    Bindings bind=engine.createBindings;

    bind.put(factor,1);

    绑定上下文,作用域是当前引擎范围

    engine.setBindings(bind, ScriptContext.ENGINE_SCOPE);

    Scanner input=new Scanner(System.in);

    while(input.hasNextInt){

    int first=input.nextInt;

    int sec=input.nextInt;

    System.out.println(输入参数是:+first+,+sec);

    执行js代码

    engine.eval(new FileReader(c:model.js));

    是否可调用方法

    if(engine instanceof Invocable){

    Invocable in=(Invocable)engine;

    执行js中的函数

    Double result=(Double)in.invokeFunction(formula,frst, sec);

    System.out.println(运算结果:+result.intValue);

    }

    }

    }

    上段代码使用Scanner类接受键盘输入的两个数字,然后调用

    JavaScript脚本的formula函数计算其结果,注意,除非输入了一个非int

    数字,否则当前JVM会一直运行,这也是模拟生产系统的在线变更状

    况。运行结果如下:

    输入参数是:1,2

    运算结果:3

    此时,保持JVM的运行状态,我们修改一下formula函数,代码如

    下:function formula(var1,var2){

    return var1+var2-factor;

    }

    其中,乘号变成了减号,计算公式发生了重大改变。回到JVM中继

    续输入,运行结果如下。

    输入参数是:1,2

    运算结果:2

    修改Java代码,JVM没有重启,输入参数也没有任何改变,仅仅改

    变脚本函数即可产生不同的结果。这就是脚本语言对系统设计最有利的

    地方:可以随时发布而不用重新部署;这也是我们Javaer最喜爱它的地

    方—即使进行变更,也能提供不间断的业务服务。

    Java 6不仅仅提供了代码级的脚本内置,还提供了一个jrunscript命

    令工具,它可以在批处理中发挥最大效能,而且不需要通过JVM解释脚

    本语言,可以直接通过该工具运行脚本。想想看,这是多么大的诱惑力

    呀!而且这个工具是可以跨操作系统的,脚本移植就更容易了。但是有

    一点需要注意:该工具是实验性的,在以后的JDK中会不会继续提供就

    很难说了。建议17:慎用动态编译

    动态编译一直是Java的梦想,从Java 6版本它开始支持动态编译

    了,可以在运行期直接编译.java文件,执行.class,并且能够获得相关的

    输入输出,甚至还能监听相关的事件。不过,我们最期望的还是给定一

    段代码,直接编译,然后运行,也就是空中编译执行(on-the-fly),来

    看如下代码:

    public class Client{

    public static void main(String[]args)throws Exception{

    Java源代码

    String sourceStr=public class Hello{public String sayHello(String name)

    {return\Hello,\+name+\!\;}};

    类名及文件名

    String clsName=Hello;

    方法名

    String methodName=sayHello;

    当前编译器

    JavaCompiler cmp=ToolProvider.getSystemJavaCompiler;

    Java标准文件管理器

    StandardJavaFileManager fm=cmp.getStandardFileManager(null, null, null);

    Java文件对象

    JavaFileObject jfo=new StringJavaObject(clsName, sourceStr);

    编译参数,类似于javac中的options

    ListoptionsList=new ArrayList

    编译文件的存放地方,注意:此处是为Eclipse工具特设的

    optionsList.addAll(Arrays.asList(-d,.bin));

    要编译的单元

    Listjfos=Arrays.asList(jfo);

    设置编译环境

    JavaCompiler.CompilationTask task=cmp.getTask(null, fm, null,optionsList, null, jfos);

    编译成功

    if(task.call){

    生成对象

    Object obj=Class.forName(clsName).newInstance;

    Classcls=obj.getClass;

    调用sayHello方法

    Method m=cls.getMethod(methodName, String.class);

    String str=(String)m.invoke(obj,Dynamic Compilation);

    System.out.println(str);

    }

    }

    }

    文本中的Java对象

    class StringJavaObject extends SimpleJavaFileObject{

    源代码

    private String content=;

    遵循Java规范的类名及文件

    public StringJavaObject(String_javaFileName, String_content){

    super(_createStringJavaObjectUri(_javaFileName),Kind.SOURCE);

    content=_content;

    }

    产生一个URL资源路径private static URI_createStringJavaObjectUri(String name){

    注意此处没有设置包名

    return URI.create(String:+name+Kind.SOURCE.extension);

    }

    文本文件代码

    @Override

    public CharSequence getCharContent(boolean ignoreEncodingErrors)

    throws IOException{

    return content;

    }

    }

    上面的代码较多,这是一个动态编译的模板程序,读者可以拷贝到

    项目中使用,代码中的中文注释也较多,相信读者看得懂,不多解释,读者只要明白一件事:只要是在本地静态编译能够实现的任务,比如编

    译参数、输入输出、错误监控等,动态编译就都能实现。

    Java的动态编译对源提供了多个渠道。比如,可以是字符串(例子

    中就是字符串),可以是文本文件,也可以是编译过的字节码文件

    (.class文件),甚至可以是存放在数据库中的明文代码或是字节码。汇

    总成一句话,只要是符合Java规范的就都可以在运行期动态加载,其实

    现方式就是实现JavaFileObject接口,重写getCharContent、openInputStream、openOutputStream,或者实现JDK已经提供的两个

    SimpleJavaFileObject、ForwardingJavaFileObject,具体代码可以参考上

    个例子。

    动态编译虽然是很好的工具,让我们可以更加自如地控制编译过

    程,但是在我目前所接触的项目中还是使用得较少。原因很简单,静态

    编译已经能够帮我们处理大部分的工作,甚至是全部的工作,即使真的

    需要动态编译,也有很好的替代方案,比如JRuby、Groovy等无缝的脚

    本语言。

    另外,我们在使用动态编译时,需要注意以下几点:

    (1)在框架中谨慎使用

    比如要在Struts中使用动态编译,动态实现一个类,它若继承自

    ActionSupport就希望它成为一个Action。能做到,但是debug很困难;再

    比如在Spring中,写一个动态类,要让它动态注入到Spring容器中,这

    是需要花费老大功夫的。

    (2)不要在要求高性能的项目使用动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环

    节,因此在高性能项目中不要使用动态编译。不过,如果是在工具类项

    目中它则可以很好地发挥其优越性,比如在Eclipse工具中写一个插件,就可以很好地使用动态编译,不用重启即可实现运行、调试功能,非常

    方便。

    (3)动态编译要考虑安全问题

    如果你在Web界面上提供了一个功能,允许上传一个Java文件然后

    运行,那就等于说:“我的机器没有密码,大家都来看我的隐私吧”,这

    是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的

    安全工作毁于一旦。

    (4)记录动态编译过程

    建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅

    是为了诊断,还是为了安全和审计,对Java项目来说,空中编译和运行

    是很不让人放心的,留下这些依据可以更好地优化程序。建议18:避免instanceof非预期结果

    instanceof是一个简单的二元操作符,它是用来判断一个对象是否是

    一个类实例的,其操作类似于>=、==,非常简单,我们来看段程序,代码如下:

    public class Client{

    public static void main(String[]args){

    String对象是否是Object的实例

    boolean b1=Stinginstanceof Object;

    String对象是否是String的实例

    boolean b2=new Stringinstanceof String;

    Object对象是否是String的实例

    boolean b3=new Objectinstanceof String;

    拆箱类型是否是装箱类型的实例

    boolean b4='A'instanceof Character;

    空对象是否是String的实例

    boolean b5=null instanceof String;

    类型转换后的空对象是否是String的实例

    boolean b6=(String)null instanceof String;

    Date对象是否是String的实例

    boolean b7=new Dateinstanceof String;

    在泛型类中判断String对象是否是Date的实例

    boolean b8=new GenericClass.isDateInstance;

    }

    }

    class GenericClass{

    判断是否是Date类型

    public boolean isDateInstance(T t){

    return t instanceof Date;

    }

    }

    就这么一段程序,instanceof的所有应用场景都出现了,同时问题也

    产生了:这段程序中哪些语句会编译通不过?我们一个一个地来解说。

    Stinginstanceof Object

    返回值是true,这很正常,“String”是一个字符串,字符串又继承了

    Object,那当然是返回true了。

    new Stringinstanceof String

    返回值是true,没有任何问题,一个类的对象当然是它的实例了。

    new Objectinstanceof String返回值是false, Object是父类,其对象当然不是String类的实例了。

    要注意的是,这句话其实完全可以编译通过,只要instanceof关键字的左

    右两个操作数有继承或实现关系,就可以编译通过。

    'A'instanceof Character

    这句话可能有读者会猜错,事实上它编译不通过,为什么呢?因

    为'A'是一个char类型,也就是一个基本类型,不是一个对象,instanceof

    只能用于对象的判断,不能用于基本类型的判断。

    null instanceof String

    返回值是false,这是instanceof特有的规则:若左操作数是null,结

    果就直接返回false,不再运算右操作数是什么类。这对我们的程序非常

    有利,在使用instanceof操作符时,不用关心被判断的类(也就是左操作

    数)是否为null,这与我们经常用到的equals、toString方法不同。

    (String)null instanceof String

    返回值是false,不要看这里有个强制类型转换就认为结果是true,不是的,null是一个万用类型,也可以说它没类型,即使做类型转换还

    是个null。

    new Dateinstanceof String

    编译通不过,因为Date类和String没有继承或实现关系,所以在编

    译时直接就报错了,instanceof操作符的左右操作数必须有继承或实现关

    系,否则编译会失败。

    new GenericClass. isDateInstance

    编译通不过?非也,编译通过了,返回值是false, T是个String类

    型,与Date之间没有继承或实现关系,为什么''t instanceof Date''会编译

    通过呢?那是因为Java的泛型是为编码服务的,在编译成字节码时,T

    已经是Object类型了,传递的实参是String类型,也就是说T的表面类型

    是Object,实际类型是String,那''t instanceof Date''这句话就等价

    于''Object instance of Date''了,所以返回false就很正常了。

    就这么一个简单的instanceof,你答对几个?建议19:断言绝对不是鸡肋

    在防御式编程中经常会用断言(Assertion)对参数和环境做出判

    断,避免程序因不当的输入或错误的环境而产生逻辑异常,断言在很多

    语言中都存在,C、C++、Python都有不同的断言表示形式。在Java中的

    断言使用的是assert关键字,其基本的用法如下:

    assert<布尔表达式>

    assert<布尔表达式>:<错误信息>

    在布尔表达式为假时,抛出AssertionError错误,并附带了错误信

    息。assert的语法较简单,有以下两个特性:

    (1)assert默认是不启用的

    我们知道断言是为调试程序服务的,目的是为了能够快速、方便地

    检查到程序异常,但Java在默认条件下是不启用的,要启用就需要在编

    译、运行时加上相关的关键字,这就不多说,有需要的话可以参考一下

    Java规范。

    (2)assert抛出的异常AssertionError是继承自Error的

    断言失败后,JVM会抛出一个AssertionError错误,它继承自Error,注意,这是一个错误,是不可恢复的,也就表示这是一个严重问题,开

    发者必须予以关注并解决之。

    assert虽然是做断言的,但不能将其等价于if……else……这样的条

    件判断,它在以下两种情况不可使用:

    (1)在对外公开的方法中

    我们知道防御式编程最核心的一点就是:所有的外部因素(输入参

    数、环境变量、上下文)都是“邪恶”的,都存在着企图摧毁程序的罪恶

    本源,为了抵制它,我们要在程序中处处检验,满地设卡,不满足条件

    就不再执行后续程序,以保护主程序的正确性,处处设卡没问题,但就

    是不能用断言做输入校验,特别是公开方法。我们来看一个例子:public class Client{

    public static void main(String[]args){

    StringUtils.encode(null);

    }}

    字符串处理工具类

    class StringUtils{

    public static String encode(String str){

    assert str!=null:加密的字符串为null;

    加密处理

    }

    }

    encode方法对输入参数做了不为空的假设,如果为空,则抛出

    AssertionError错误,但这段程序存在一个严重的问题,encode是一个

    public方法,这标志着是它对外公开的,任何一个类只要能够传递一个

    String类型的参数(遵守契约)就可以调用,但是Client类按照规范和契

    约调用enocde方法,却获得了一个AssertionError错误信息,是谁破坏了

    契约协定?—是encode方法自己。

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

    assert的支持是可选的,在开发时可以让它运行,但在生产系统中

    则不需要其运行了(以便提高性能),因此在assert的布尔表达式中不

    能执行逻辑代码,否则会因为环境不同而产生不同的逻辑,例如:

    public void doSomething(List list, Object element){

    assert list.remove(element):删除元素+element+失败;

    业务处理

    }

    这段代码在assert启用的环境下,没有任何问题,但是一旦投入到

    生产环境,就不会启用断言了,而这个方法也就彻底完蛋了,list的删

    除动作永远都不会执行,所以也就永远不会报错或异常,因为根本就没

    有执行嘛!

    以上两种情况下不能使用assert,那在什么情况下能够使用assert

    呢?一句话:按照正常执行逻辑不可能到达的代码区域可以放置

    assert。具体分为三种情况:

    (1)在私有方法中放置assert作为输入参数的校验

    在私有方法中可以放置assert校验输入参数,因为私有方法的使用者是作者自己,私有方法的调用者和被调用者之间是一种弱契约关系,或者说没有契约关系,其间的约束是依靠作者自己控制的,因此加上

    assert可以更好地预防自己犯错,或者无意的程序犯错。

    (2)流程控制中不可能达到的区域

    这类似于JUnit的fail方法,其标志性的意义就是:程序执行到这里

    就是错误的,例如:

    public void doSomething{

    int i=7;

    while(i>7){

    业务处理

    }

    assert false:到达这里就表示错误;

    }

    (3)建立程序探针

    我们可能会在一段程序中定义两个变量,分别代表两个不同的业务

    含义,但是两者有固定的关系,例如var1=var22,那我们就可以在程序

    中到处设“桩”,断言这两者的关系,如果不满足即表明程序已经出现了

    异常,业务也就没有必要运行下去了。建议20:不要只替换一个类

    我们经常在系统中定义一个常量接口(或常量类),以囊括系统中

    所涉及的常量,从而简化代码,方便开发,在很多的开源项目中已采用

    了类似的方法,比如在Struts2中,org.apache.struts2.StrutsConstants就是

    一个常量类,它定义了Struts框架中与配置有关的常量,而

    org.apache.struts2.StrutsStatics则是一个常量接口,其中定义了OGNL访

    问的关键字。

    关于常量接口(类)我们来看一个例子,首先定义一个常量类:

    public class Constant{

    定义人类寿命极限

    public fnal static int MAX_AGE=150;

    }

    这是一个非常简单的常量类,定义了人类的最大年龄,我们引用这

    个常量,代码如下:

    public class Client{

    public static void main(String[]args){

    System.out.println(人类寿命极限是:+Constant.MAX_AGE);

    }

    }

    运行的结果非常简单(结果省略)。目前的代码编写都是在“智能

    型”IDE工具中完成的,下面我们暂时回溯到原始时代,也就是回归到用

    记事本编写代码的年代,然后看看会发生什么奇妙事情(为什么要如

    此,稍后会给出答案)。

    修改常量Constant类,人类的寿命增加了,最大能活到180岁,代码

    如下:

    public class Constant{

    定义人类寿命极限

    public fnal static int MAX_AGE=180;

    }

    然后重新编译:javac Constant,编译完成后执行:java Client,大家想看看输出的极限年龄是多少岁吗?

    输出的结果是:“人类寿命极限是:150”,竟然没有改变为180,太

    奇怪了,这是为何?

    原因是:对于final修饰的基本类型和String类型,编译器会认为它

    是稳定态(Immutable Status),所以在编译时就直接把值编译到字节码

    中了,避免了在运行期引用(Run-time Reference),以提高代码的执行

    效率。针对我们的例子来说,Client类在编译时,字节码中就写上

    了“150”这个常量,而不是一个地址引用,因此无论你后续怎么修改常

    量类,只要不重新编译Client类,输出还是照旧。

    而对于final修饰的类(即非基本类型),编译器认为它是不稳定态

    (Mutable Status),在编译时建立的则是引用关系(该类型也叫做Soft

    Final),如果Client类引入的常量是一个类或实例,即使不重新编译也

    会输出最新值。

    千万不可小看了这点知识,细坑也能绊倒大象,比如在一个Web项

    目中,开发人员修改一个final类型的值(基本类型),考虑到重新发布

    风险较大,或者是时间较长,或者是审批流程过于繁琐,反正是为了偷

    懒,于是直接采用替换class类文件的方式发布。替换完毕后应用服务器

    自动重启,然后简单测试一下(比如本类引用final类型的常量),一切

    OK。可运行几天后发现业务数据对不上,有的类(引用关系的类)使

    用了旧值,有的类(继承关系的类)使用的是新值,而且毫无头绪,让

    人一筹莫展,其实问题的根源就在于此。

    恩,还有个小问题没有说明,我们的例子为什么不在IDE工具(比

    如Eclipse)中运行呢?那是因为在IDE中不能重现该问题,若修改了

    Constant类,IDE工具会自动编译所有的引用类,“智能”化屏蔽了该问

    题,但潜在的风险其实仍然存在。

    注意 发布应用系统时禁止使用类文件替换方式,整体WAR包发

    布才是万全之策。第2章 基本类型

    不积跬步,无以至千里;

    不积小流,无以成江海。

    ——荀子《劝学篇》

    Java中的基本数据类型(Primitive Data Types)有8个:byte、char、short、int、long、float、double、boolean,它们是Java最基本的单

    元,我们的每一段程序中都有它们的身影,但我们对如此熟悉的“伙

    伴”又了解多少呢?

    积少成多,积土成山,本章我们就来一探这最基本的8个数据类

    型。建议21:用偶判断,不用奇判断

    判断一个数是奇数还是偶数是小学里学的基本知识,能够被2整除

    的整数是偶数,不能被2整除的是奇数,这规则简单又明了,还有什么

    好考虑的?好,我们来看一个例子,代码如下:

    public class Client{

    public static void main(String[]args){

    接收键盘输入参数

    Scanner input=new Scanner(System.in);

    System.out.print(请输入多个数字判断奇偶:);

    while(input.hasNextInt){

    int i=input.nextInt;

    String str=i+->+(i%2==1?奇数:偶数);

    System.out.println(str);

    }

    }

    }

    输入多个数字,然后判断每个数字的奇偶性,不能被2整除就是奇

    数,其他的都是偶数,完全是根据奇偶数的定义编写的程序,我们来看

    看打印的结果:

    请输入多个数字判断奇偶:1 2 0-1-2

    1->奇数

    2->偶数

    0->偶数

    -1->偶数

    -2->偶数

    前三个还很靠谱,第四个参数-1怎么可能会是偶数呢,这Java也太

    差劲了,如此简单的计算也会错!别忙着下结论,我们先来了解一下

    Java中的取余(%标示符)算法,模拟代码如下:

    模拟取余计算,dividend被除数,divisor除数

    public static int remainder(int dividend, int divisor){

    return dividend-dividenddivisordivisor;

    }

    看到这段程序,相信大家都会心地笑了,原来Java是这么处理取余

    计算的呀。根据上面的模拟取余可知,当输入-1的时候,运算结果

    是-1,当然不等于1了,所以它就被判定为偶数了,也就是说是我们的判断失误了。问题明白了,修正也很简单,改为判断是否是偶数即可,代码如下:

    i%2==0?偶数:奇数

    注意 对于基础知识,我们应该“知其然,并知其所以然”。建议22:用整数类型处理货币

    在日常生活中,最容易接触到的小数就是货币,比如你付给售货员

    10元钱购买一个9.60元的零食,售货员应该找你0.4元也就是4毛钱才

    对,我们来看下面的程序:

    public class Client{

    public static void main(String[]args){

    System.out.println(10.00-9.60);

    }

    }

    我们期望的结果是0.4,也应该是这个数字,但是打印出来的却是

    0.40000000000000036,这是为什么呢?

    这是因为在计算机中浮点数有可能(注意是可能)是不准确的,它

    只能无限接近准确值,而不能完全精确。为什么会如此呢?这是由浮点

    数的存储规则所决定的,我们先来看0.4这个十进制小数如何转换成二

    进制小数,使用“乘2取整,顺序排列”法(不懂?这就没招了,太基础

    了),我们发现0.4不能使用二进制准确的表示,在二进制数世界里它

    是一个无限循环的小数,也就是说,“展示”都不能“展示”,更别说是在

    内存中存储了(浮点数的存储包括三部分:符号位、指数位、尾数,具

    体不再介绍),可以这样理解,在十进制的世界里没有办法准确表示

    13,那在二进制世界里当然也无法准确表示15(如果二进制也有分数

    的话倒是可以表示),在二进制的世界里15是一个无限循环小数。

    各位要说了,那我对结果取整不就对了吗?代码如下:

    public class Client{

    public static void main(String[]args){

    NumberFormat f=new DecimalFormat(.);

    System.out.println(f.format(10.00-9.60));

    }

    }

    打印出结果是0.4,看似解决了,但是隐藏了一个很深的问题。我

    们来思考一下金融行业的计算方法,会计系统一般记录小数点后的4位

    小数,但是在汇总、展现、报表中,则只记录小数点后的2位小数,如

    果使用浮点数来计算货币,想想看,在大批量的加减乘除后结果会有多大的差距(其中还涉及后面会讲到的四舍五入问题)!会计系统要的就

    是准确,但是却因为计算机的缘故不准确了,那真是罪过。要解决此问

    题有两种方法:

    (1)使用BigDecimal

    BigDecimal是专门为弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法。特别是与数据库

    Decimal类型的字段映射时,BigDecimal是最优的解决方案。

    (2)使用整型

    把参与运算的值扩大100倍,并转变为整型,然后在展现时再缩小

    100倍,这样处理的好处是计算简单、准确,一般在非金融行业(如零

    售行业)应用较多。此方法还会用于某些零售POS机,它们的输入和输

    出全部是整数,那运算就更简单。建议23:不要让类型默默转换

    我们出一个小学生的题目给大家做做看,光速是每秒30万公里,根

    据光线旅行的时间,计算月亮与地球、太阳与地球之间的距离。代码如

    下:

    public class Client{

    光速是30万公里秒,常量

    public static final int LIGHT_SPEED=30100001000;

    public static void main(String[]args){

    System.out.println(题目1:月亮光照射到地球需要1秒,计算月亮和地球的距离。);

    long dis1=LIGHT_SPEED1;

    System.out.println(月亮与地球的距离是:+dis1+米);

    System.out.println(--------------------------------------------);

    System.out.println(题目2:太阳光照射到地球上需要8分钟,计算太阳到地球的距离。);

    可能要超出整数范围,使用long型

    long dis2=LIGHT_SPEED608;

    System.out.println(太阳与地球的距离是:+dis2+米);

    }

    }

    估计你要鄙视了,这种小学生乘法计算有什么可做的。不错,确实

    就是一个乘法运算,我们运行一下看看结果:

    题目1:月亮光照射到地球需要1秒,计算月亮和地球的距离。

    月亮与地球的距离是:300000000米--------------------------------------------

    题目2:太阳光照射到地球上需要8分钟,计算太阳到地球的距离。

    太阳与地球的距离是:-2028888064米

    太阳和地球的距离竟然是负的,诡异。dis2不是已经考虑到int类型

    可能越界的问题,并使用了long型吗,为什么还会出现负值呢?

    那是因为Java是先运算然后再进行类型转换的,具体地说就是因为

    disc2的三个运算参数都是int类型,三者相乘的结果虽然也是int类型,但是已经超过了int的最大值,所以其值就是负值了(为什么是负值?因

    为过界了就会从头开始),再转换成long型,结果还是负值。

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

    如下:

    long dis2=LIGHT_SPEED60L8;60L是一个长整型,乘出来的结果也是一个长整型(此乃Java的基

    本转换规则,向数据范围大的方向转换,也就是加宽类型),在还没有

    超过int类型的范围时就已经转换为long型了,彻底解决了越界问题。在

    实际开发中,更通用的做法是主动声明式类型转化(注意不是强制类型

    转换),代码如下:

    long dis2=1LLIGHT_SPEED608;

    既然期望的结果是long型,那就让第一个参与运算的参数也是long

    型(1L)吧,也就是明说“嗨,我已经是长整型了,你们都跟着我一起

    转为长整型吧”。

    注意 基本类型转换时,使用主动声明方式减少不必要的Bug。建议24:边界,边界,还是边界

    某商家生产的电子产品非常畅销,需要提前30天预订才能抢到手,同时它还规定了一个会员可拥有的最多产品数量,目的是防止囤积压货

    肆意加价。会员的预定过程是这样的:先登录官方网站,选择产品型

    号,然后设置需要预订的数量,提交,符合规则即提示下单成功,不符

    合规则提示下单失败。后台的处理逻辑模拟如下:

    public class Client{

    一个会员拥有产品的最多数量

    public final static int LIMIT=2000;

    public static void main(String[]args){

    会员当前拥有的产品数量

    int cur=1000;

    Scanner input=new Scanner(System.in);

    System.out.print(请输入需要预定的数量:);

    while(input.hasNextInt){

    int order=input.nextInt;

    当前拥有的与准备订购的产品数量之和

    if(order>0&&order+cur<=LIMIT){

    System.out.println(你已经成功预定的+order+个产品!);

    }else{

    System.out.println(超过限额,预订失败!);

    }

    }

    }

    }

    这是一个简易的订单处理程序,其中cur代表的是会员已经拥有的

    产品数量,LIMIT是一个会员最多拥有的产品数量(现实中这两个参数

    当然是从数据库中获得的,不过这里是一个模拟程序),如果当前预订

    数量与拥有数量之和超过了最大数量,则预订失败,否则下单成功。业

    务逻辑很简单,同时在Web界面上对订单数量做了严格的校验,比如不

    能是负值、不能超过最大数量等,但是人算不如天算,运行不到两小时

    数据库中就出现了异常数据:某会员拥有产品的数量与预订数量之和远

    远大于限额。怎么会这样?程序逻辑上不可能有问题呀,这是如何产生

    的呢?我们来模拟一下,第一次输入:

    请输入需要预定的数量:800

    你已经成功预定的800个产品!

    这完全满足条件,没有任何问题,继续输入:请输入需要预定的数量:2147483647

    你已经成功预定的2147483647个产品!

    看到没,这个数字远远超过了2000的限额,但是竟然预订成功了,真是神奇!

    看着2147483647这个数字很眼熟?那就对了,它是int类型的最大

    值,没错,有人输入了一个最大值,使校验条件失效了,Why?我们来

    看程序,order的值是2147483647,那再加上1000就超出int的范围了,其

    结果是-2147482649,那当然是小于正数2000了!一句话可归结其原

    因:数字越界使检验条件失效。

    在单元测试中,有一项测试叫做边界测试(也有叫做临界测试),如果一个方法接收的是int类型的参数,那以下三个值是必测的:0、正

    最大、负最小,其中正最大和负最小是边界值,如果这三个值都没有问

    题,方法才是比较安全可靠的。我们的例子就是因为缺少边界测试,致

    使生产系统产生了严重的偏差。

    也许你要疑惑了,Web界面既然已经做了严格的校验,为什么还能

    输入2147483647这么大的数字呢?是否说明Web校验不严格?错了,不

    是这样的,Web校验都是在页面上通过JavaScript实现的,只能限制普通

    用户(这里的普通用户是指不懂HTML、不懂HTTP、不懂Java的简单使

    用者),而对于高手,这些校验基本上就是摆设,HTTP是明文传输

    的,将其拦截几次,分析一下数据结构,然后再写一个模拟器,一切前

    端校验就都成了浮云!想往后台提交个什么数据那还不是信手拈来?!建议25:不要让四舍五入亏了一方

    本建议还是来重温一个小学数学问题:四舍五入。四舍五入是一种

    近似精确的计算方法,在Java 5之前,我们一般是通过使用Math.round

    来获得指定精度的整数或小数的,这种方法使用非常广泛,代码如下:

    public class Client{

    public static void main(String[]args){

    System.out.println(10.5近似值:+Math.round(10.5));

    System.out.println(-10.5近似值:+Math.round(-10.5));

    }

    }

    输出结果为:

    10.5近似值:11

    -10.5近似值:-10

    这是四舍五入的经典案例,也是初级面试官很乐意选择的考题,绝

    对值相同的两个数字,近似值为什么就不同了呢?这是由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、0.004、0.003、0.002、0.001。

    因为舍弃和进位的数字是在0到9之间均匀分布的,所以对于银行家

    来说,每10笔存款的利息因采用四舍五入而获得的盈利是:

    0.000+0.001+0.002+0.003+0.004-0.005-0.004-0.003-0.002-0.001=-0.005

    也就是说,每10笔的利息计算中就损失0.005元,即每笔利息计算

    损失0.0005元,这对一家有5千万储户的银行来说(对国内的银行来

    说,5千万是个很小的数字),每年仅仅因为四舍五入的误差而损失的

    金额是:

    public class Client{

    public static void main(String[]args){

    银行账户数量,5千万

    int accountNum=500010000;

    按照人行的规定,每个季度末月的20日为银行结息日

    double cost=0.0005accountNum4;

    System.out.println(银行每年损失的金额:+cost);

    }

    }

    输出的结果是:“银行每年损失的金额:100000.0”。即,每年因为

    一个算法误差就损失了10万元,事实上以上的假设条件都是非常保守

    的,实际情况可能损失得更多。那各位可能要说了,银行还要放贷呀,放出去这笔计算误差不就抵消掉了吗?不会抵销,银行的贷款数量是非

    常有限的,其数量级根本没有办法和存款相比。

    这个算法误差是由美国银行家发现的(那可是私人银行,钱是自己

    的,白白损失了可不行),并且对此提出了一个修正算法,叫做银行家

    舍入(Banker's Round)的近似算法,其规则如下:

    舍去位的数值小于5时,直接舍去;

    舍去位的数值大于等于6时,进位后舍去;

    当舍去位的数值等于5时,分两种情况:5后面还有其他数字(非0),则进位后舍去;若5后面是0(即5是最后一个数字),则根据5前

    一位数的奇偶性来判断是否需要进位,奇数进位,偶数舍去。

    以上规则汇总成一句话:四舍六入五考虑,五后非零就进一,五后

    为零看奇偶,五前为偶应舍去,五前为奇要进一。我们举例说明,取2

    位精度:

    round(10.5551)=10.56

    round(10.555)=10.56

    round(10.545)=10.54

    要在Java 5以上的版本中使用银行家的舍入法则非常简单,直接使

    用RoundingMode类提供的Round模式即可,示例代码如下:

    public class Client{

    public static void main(String[]args){

    存款

    BigDecimal d=new BigDecimal(888888);

    月利率,乘3计算季利率

    BigDecimal r=new BigDecimal(0.0018753);

    计算利息

    BigDecimal i=d.multiply(r).setScale(2,RoundingMode.HALF_EVEN);

    System.out.println(季利息是:+i);

    }

    }

    在上面的例子中,我们使用了BigDecimal类,并且采用setScale方法

    设置了精度,同时传递了一个RoundingMode.HALF_EVEN参数表示使

    用银行家舍入法则进行近似计算,BigDecimal和RoundingMode是一个绝

    配,想要采用什么舍入模式使用RoundingMode设置即可。目前Java支持

    以下七种舍入方式:

    ROUND_UP:远离零方向舍入。

    向远离0的方向舍入,也就是说,向绝对值最大的方向舍入,只要

    舍弃位非0即进位。

    ROUND_DOWN:趋向零方向舍入。

    向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方法,但在大量与货币数字交互的项目中,一定要选择好近

    似的计算模式,尽量减少因算法不同而造成的损失。

    注意 根据不同的场景,慎重选择不同的舍入模式,以提高项目的

    精准度,减少算法损失。建议26:提防包装类型的null值

    我们知道Java引入包装类型(Wrapper Types)是为了解决基本类型

    的实例化问题,以便让一个基本类型也能参与到面向对象的编程世界

    中。而在Java 5中泛型更是对基本类型说了“不”,如想把一个整型放到

    List中,就必须使用Integer包装类型。我们来看一段代码:

    计算list中所有元素之和

    public static int f(Listlist){

    int count=0;

    for(int i:list){

    count+=i;

    }

    return count;

    }

    接收一个元素是整型的List参数,计算所有元素之和,这在统计、报表项目中很常见,我们来看看这段代码有没有问题。遍历一个列表,然后相加,应该没有问题。那我们再来写一个方法调用,代码如下:



    public static void main(String[]args){

    Listlist=new ArrayList

    list.add(1);

    list.add(2);

    list.add(null);

    System.out.println(f(list));

    }

    把1、2和空值都放到List中,然后调用方法计算,现在来思考一下

    会不会出错。应该不会出错吧,基本类型和包装类型都是可以通过自动

    装箱(Autoboxing)和自动拆箱(AutoUnboxing)自由转换的,null应

    该可以转为0吧,真的是这样吗?我们运行一下看看结果:

    Exception in threadmainjava.lang.NullPointerException

    运行失败,报空指针异常,我们稍稍思考一下很快就知道原因了:

    在程序的for循环中,隐含了一个拆箱过程,在此过程中包装类型转换为

    了基本类型。我们知道拆箱过程是通过调用包装对象的intValue方法来

    实现的,由于包装对象是null值,访问其intValue方法报空指针异常也就在所难免了。问题清楚了,修改也很简单,加入null值检查即可,代码

    如下:

    public static int f(Listlist){

    int count=0;

    for(Integer i:list){

    count+=(i!=null)?i:0;

    }

    return count;

    }

    上面以Integer和int为例说明了拆箱问题,其他7个包装对象的拆箱

    过程也存在着同样的问题。包装对象和拆箱对象可以自由转换,这不

    假,但是要剔除null值,null值并不能转化为基本类型。对于此类问题,我们谨记一点:包装类型参与运算时,要做null值校验。建议27:谨慎包装类型的大小比较

    基本类型是可以比较大小的,其所对应的包装类型都实现了

    Comparable接口也说明了此问题,那我们来比较一下两个包装类型的大

    小,代码如下:

    public class Client{

    public static void main(String[]args){

    Integer i=new Integer(100);

    Integer j=new Integer(100);

    compare(i, j);

    }

    比较两个包装对象大小

    public static void compare(Integer i, Integer j){

    System.out.println(i==j);

    System.out.println(i>j);

    System.out.println(i
    }

    }

    代码很简单,产生了两个Integer对象,然后比较两者的大小关系,既然基本类型和包装类型是可以自由转换的,那上面的代码是不是就可

    打印出两个相等的值呢?让事实说话,运行结果如下:

    false

    false

    false

    竟然是3个false,也就是说两个值之间不等,也没大小关系,这也

    太奇怪了吧。不奇怪,我们来一一解释。

    i==j

    在Java中“==”是用来判断两个操作数是否有相等关系的,如果是基

    本类型则判断值是否相等,如果是对象则判断是否是一个对象的两个引

    用,也就是地址是否相等,这里很明显是两个对象,两个地址,不可能

    相等。

    i>j和i”和“<”用来判断两个数字类型的大小关系,注意只

    能是数字型的判断,对于Integer包装类型,是根据其intValue方法

    的返回值(也就是其相应的基本类型)进行比较的(其他包装类型是根

    据相应的value值来比较的,如doubleValue、floatValue等),那很显

    然,两者不可能有大小关系的。

    问题清楚了,修改总是比较容易的,直接使用Integer实例的

    compareTo方法即可。但是这类问题的产生更应该说是习惯问题,只要

    是两个对象之间的比较就应该采用相应的方法,而不是通过Java的默认

    机制来处理,除非你确定对此非常了解。建议28:优先使用整型池

    上一建议我们解释了包装对象的比较问题,本建议将继续深入讨论

    相关问题,首先看如下代码:

    public static void main(String[]args){

    Scanner input=new Scanner(System.in);

    while(input.hasNextInt){

    int ii=input.nextInt;

    System.out.println(\n====+ii+的相等判断======);

    两个通过new产生的Integer对象

    Integer i=new Integer(ii);

    Integer j=new Integer(ii);

    System.out.println(new产生的对象:+(i==j));

    基本类型转为包装类型后比较

    i=ii;

    j=ii;

    System.out.println(基本类型转换的对象:+(i==j));

    通过静态方法生成一个实例

    i=Integer.valueOf(ii);

    j=Integer.valueOf(ii);

    System.out.println(valueOf产生的对象:+(i==j));

    }

    }

    输入多个数字,然后按照3种不同的方式产生Integer对象,判断其

    是否相等,注意这里使用了“==”,这说明判断的不是同一个对象。我们

    输入三个数字127、128、555,结果如下:

    ====127的相等判断======

    new产生的对象:false

    基本类型转换的对象:true

    valueOf产生的对象:true

    ====128的相等判断======

    new产生的对象:false

    基本类型转换的对象:false

    valueOf产生的对象:false

    ====555的相等判断======

    new产生的对象:false

    基本类型转换的对象:false

    valueOf产生的对象:false

    很不可思议呀,数字127的比较结果竟然与其他两个数字不同,它

    的装箱动作所产生的对象竟然是同一个对象,valueOf产生的也是同一

    个对象,但是大于127的数字128和555在比较过程中所产生的却不是同

    一个对象,这是为什么?我们一个一个来解释。(1)new产生的Integer对象

    new声明的就是要生成一个新的对象,没二话,这是两个对象,地

    址肯定不等,比较结果为false。

    (2)装箱生成的对象

    对于这一点,首先要说明的是装箱动作是通过valueOf方法实现

    的,也就是说后两个算法是相同的,那结果肯定也是一样的,现在的问

    题是:valueOf是如何生成对象的呢?我们来阅读一下Integer.valueOf的

    实现代码:

    public static Integer valueOf(int i){

    final int offset=128;

    if(i>=-128&&i<=127){must cache

    return IntegerCache.cache[i+offset];

    }

    return new Integer(i);

    }

    这段代码的意思已经很明了了,如果是-128到127之间的int类型转

    换为Integer对象,则直接从cache数组中获得,那cache数组里是什么东

    西,代码如下:

    static final Integer cache[]=new Integer[-(-128)+127+1];

    static{

    for(int i=0;i
    cache[i]=new Integer(i-128);

    }

    cache是IntegerCache内部类的一个静态数组,容纳的是-128到127之

    间的Integer对象。通过valueOf产生包装对象时,如果int参数在-128和

    127之间,则直接从整型池中获得对象,不在该范围的int类型则通过

    new生成包装对象。

    明白了这一点,要理解上面的输出结果就迎刃而解了,127的包装

    对象是直接从整型池中获得的,不管你输入多少次127这个数字,获得

    的对象都是同一个,那地址当然都是相等的。而128、555超出了整型池

    范围,是通过new产生一个新的对象,地址不同,当然也就不相等了。

    以上的解释也是整型池的原理,整型池的存在不仅仅提高了系统性能,同时也节约了内存空间,这也是我们使用整型池的原因,也就是在

    声明包装对象的时候使用valueOf生成,而不是通过构造函数来生成的

    原因。顺便提醒大家,在判断对象是否相等的时候,最好是用equals方

    法,避免用“==”产生非预期结果。

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

    间性能。建议29:优先选择基本类型

    包装类型是一个类,它提供了诸如构造方法、类型转换、比较等非

    常实用的功能,而且在Java 5之后又实现了与基本类型之间的自动转

    换,这使包装类型如虎添翼,更是应用广泛了,在开发中包装类型已经

    随处可见,但无论是从安全性、性能方面来说,还是从稳定性方面来

    说,基本类型都是首选方案。我们来看一段代码:

    public class Client{

    public static void main(String[]args){

    Client cilent=new Client;

    int i=140;

    分别传递int类型和Integer类型

    cilent.f(i);

    cilent.f(Integer.valueOf(i));

    }

    public void f(long a){

    System.out.println(基本类型的方法被调用);

    }

    public void f(Long a){

    System.out.println(包装类型的方法被调用);

    }

    }

    在上面的程序中首先声明了一个int变量i,然后加宽转变成long型,再调用f方法,分别传递int和long的基本类型和包装类型,诸位想想

    该程序是否能够编译?如果能编译输出结果又是什么呢?

    首先,这段程序绝对是能够编译的。不过,说不能编译的同学还是

    很动了一番脑筋的,只是还欠缺点火候,你可能会猜测以下这些地方不

    能编译:

    f方法重载有问题。定义的两个f方法实现了重载,一个形

    参是基本类型,一个形参是包装类型,这类重载很正常。虽然基本类型

    和包装类型有自动装箱、自动拆箱的功能,但并不影响它们的重载,自

    动拆箱(装箱)只有在赋值时才会发生,和重载没有关系。

    cilent. f(i)报错。i是int类型,传递到fun(long l)是没有任何问

    题的,编译器会自动把i的类型加宽,并将其转变为long型,这是基本类

    型的转换规则,也没有任何问题。

    cilent. f(Integer.valueOf(i))报错。代码中没有f(Integer i ......


您现在查看是摘要介绍页, 详见PDF附件(1754KB,440页)