原文:The Developer Insight Series, Part 1: Write Dumb Code -- Advice From Four Leading Java Developers
作者:Janice J. Heiss
出处:http://java.sun.com/developer/technicalArticles/Interviews/devinsight_1/
历年来,开发者总是会讨论他们最喜欢的代码、最有趣的代码、最酷的代码,以及如何编写代码,如何避免编写代码,编写好代码的障碍,与编写代码相关的爱恨情仇,以及编写代码的过程等等,他们给出了很多值得我们铭记在心的真知灼见。
在这一由多篇文章组成的系列的第一部分中,我以一个稍微常见的建议作为开始:编写傻瓜代码。
目录
- Brian Goetz:编写傻瓜式代码
- Heinz Kabutz:通过使用好的面向对象的设计模式来界定“优质”的做法
- Cay Horstmann:模式并非魔法药水
- Kirk Pepperdine:傻瓜代码的可读性更强
- 其他参考
- 讨论
Brian Goetz:编写傻瓜式代码
Brian Goetz是Sun Microsystems的一位技术传道者,自2000年以来,已经发表了75篇关于最佳实践、平台内核及并发编程方面的文章,他是Java Concurrency in Pracitce一书的主要作者,该书入围2006年的Jolt大奖,是2006年度的JavaOne会议上的最畅销书。在2006年8月加盟Sun之前,他作为其软件公司Quiotix的一位顾问工作了十五年,除了进行Java技术方面的写作之外,他还经常在各种会议上发言,就线程、Java编程语言的内存模型、垃圾收集、Java技术的性能神话以及其他的一些议题进行演讲。
此外,他也在内核、设备驱动程序、协议实现、编译器、服务器应用、web应用、科学计算、数据可视化以及企业基础设施工具等方面提供咨询。Goetz参加了几个开源项目,其中包括Lucene的全文搜索和检索系统,以及FindBugs的静态分析工具包。
在Sun公司,作为一个顾问,对那些从Java并发到Java开发者需求中拓展出来的各种各样涉猎广泛的主题,他都提供咨询,为Java平台的开发贡献他的力量。
开发者如何才能编写出运行良好的代码呢?
答案似乎是违反直觉的,通常,编写Java应用的快速代码的方法是编写傻瓜代码——简单、干净并且遵循了最明显的面向对象准则的代码,这需要配合动态编译器的特性,他们是大的模式匹配引擎。因为编译器是由那些受着进度和时间预算限制的人来编写的,因此编译器的开发者把他们的精力集中在最常见的代码模式上,因为在这些地方他们的工作能得到最大体现。因此如果你用简单的面向对象准则来编写代码的话,那么比起编写那些粗糙的、看起来很聪明但编译器却无法有效优化的破解(hacked-up)代码或者位拆列(bit-banging)代码来说,你能够获得更好的编译器优化。
因此,正好与用C语言做开发所教给我们的相反,干净的、傻瓜式的代码通常都会比真正聪明的代码运行得更快一些。在C语言中,聪明的源代码可转换成预期的机器代码级别的专有语句,但在Java应用中它不能以这种方式来工作。我的意思并不是说,Java编译器过于愚蠢,不能把聪明的代码转换成相应的机器代码,实际上,其对Java代码的有效优化甚于C编译器所能做的。
我的建议是:编写简单直接的代码,然后,如果性能还是没有“足够好”的话,就进行优化。不过“足够好”这一概念背后所隐含的意思是你必需有明确的性能指标,没有这些指标,你将永远都不会知道何时应该进行优化。你手边还必须有一个实际的、可重复进行的测试程序来确定这些指标是否被满足,一旦你能够在实际的操作情况下检测程序的性能,那么就可以开始进行调整了,因为这样你会知道调整是否有所帮助。但是,如果只是做假设:“哎呀,我想如果修改这个地方,它会运行得更快,”通常这在Java编程中会适得其反。
因为Java代码是动态编译的,所以现实的测试条件是至关重要的,如果你从上下文中拿出一个类,那么对它的编译将不同于其在应用中的情况,这意味着性能必须是在现实的条件下进行衡量的。因此,性能指标应该与有商业价值的指数挂钩——每秒钟事务数、平均服务时间、最坏情况下的延迟——这些你的客户会感知到的因素。在微观水平上关注性能特征通常会产生误导并难以进行测试,因为很难为一些脱离了上下文环境的小代码块制作出符合实际情况的测试用例。
在2003年的时候你曾说过:“开发者喜欢优化代码,并且有着充分的理由,这是如此的令人满足和充满乐趣。不过,知道何时进行优化才更为重要,不幸的是,就哪方面才真正是应用的性能问题这一情况来说,开发者一般都有着糟糕的直觉。”你现在还是这样认为吗?
与四年之前比起来,这一说法现在更为确实,且相对于C开发者来说,与Java开发者的情况更吻合。大部分情况下的性能调整都会让我想起一个关于在厨房中找钥匙的家伙的老笑话,虽然钥匙是被丢在大街上了,但是厨房的光线更好一些。我们是如此密切地熟知自己编写的代码,我们所依赖的外部服务,无论是库还是外部代理,诸如数据库和web service等,都在我们的意识和视线之外,因此,当遇到某个性能问题时,我们倾向于在自己的代码中考虑我们能想象的性能问题出现的地方,但通常情况下,那并不是性能问题的根源所在——问题出在应用架构的其他方面。
现如今,大部分的性能问题都是架构导致的后果,而非编码——做了太多的数据库调用或者是无数次序列化每一样东西到XML然后反序列化,这些代码通常每天都在你编写和看到的代码之外运行着,不过他们才是性能问题的真正根源所在。因此,如果只是在你熟悉的地方寻找的话,那么你就是正在厨房中找钥匙,这是一个开发者总是会犯的错误,应用越是复杂,性能越是取决于不是你编写的代码,因此,问题越有可能是处于你的代码之外。
越是简单的地方,Java编程中的性能分析就越难于C,因为C具有与汇编语言相当的相似性,从C代码到机器代码的映射是相当直接的,在其不能表达的地方,编译器能直接显示机器代码。Java应用与C不一样,运行时会不断基于变化的条件和观察所得修改代码,其在开始时会解释代码并编译之,但可能会根据配置数据或者载入的其他来类来使已编译的代码失效,然后再重新编译它。结果,你的代码的性能特征会极大戏剧化地依赖于代码所运行的环境,这使得很难这样说:“这一代码快于那一代码”,因为你需要考虑更多的上下文环境以便做出更合理的性能分析。另外,还存在着一些诸如时机、编译特性、载入类的交互以及垃圾收集等一类的非确定性因素,因此,相对于C来说,更难于对Java代码进行这种微性能优化。
同时,编译在执行时完成这一事实意味着,与C编译器相比,优化器有着更多可用于工作的信息,其知道哪些类已被载入,以及已经编译的方法实际上是如何被使用的,因此,相对于一个静态的编译器来说,其可以做出更好的优化决策,这对于性能来说是好事,但意味着更难于预测给定代码块的性能。
查阅对Brian Goetz所做的完整访谈。
Heinz Kabutz:通过使用好的面向对象的设计模式来界定“优质”的做法
荣获Java Champion称号的Heinz Kabutz在South Africa的Cape Town长大,其在初中时,就爱在一台ZX Spectrum计算机上捣鼓,培养出了对编程的兴趣。他获得了Cape Town大学的B.S.学位,并在25岁时获得了Ph.D.学位,两个学位都是属于计算机科学方面的。1998年,他开始成立了自己的软件开发公司Java Specialists,编写合同软件、接受咨询并提供Java技术和设计模式方面的课程。
Kebutz作为免费的Java Specialists’ Newsletter的创建者而为人所知,其目标是帮助Java开发者更加专业化。
我询问Kabutz如何看待Brian Goetz的关于编写“傻瓜式”代码的这一建议。
就我的经验来说,好的面向对象设计会产生更快且更易于维护的Java代码。但什么是好的代码呢?我发现通过使用好的面向对象的设计模式来界定“优质”会更容易一些,我通常都鼓励软件开发公司在设计模式方面培训他们的所有的开发者,包括从最初级的人员到最聪明的架构师。
采用了好的设计模式的团队会发现调整代码的工作会更容易一些,代码会不那么脆弱而且需要更少的复制与粘贴操作。Java.util.Arrays就是糟糕代码的一个很好的例子,其包含了两个mergeSort(Object[])方法,一个用到了Comparator,而另一个则用 Comparable,这两个方法几乎是一样的,可以通过引入一个使用Comparable方法的DefaultComparator来把它们合并成一个,这一策略模式(strategy pattern)能够避免这样的设计缺陷。
通过Ctrl-C和Ctrl-V来编码还有可能会隐藏了性能问题,假设你有几个算法,几乎都相同,但位于系统的不同地方,
如果衡量性能的话,你可能会发现每个算法占用了CPU的5%,不过如果把它们加在一起的话,那几乎就是20%,好的设计让你能够更轻松地修改代码并检测到瓶颈所在,让我们举个例子来证明Brian Goetz的观点。
在Java编程的早期,我有时会求助于“聪明的”代码,例如,我在优化德国一家公司编写的一个系统时,在已经优化了系统的架构和设计之后,我想对事情做一些改善,于是我修改了String这一类型的使用,使用StringBuffer类型作为代替,不要去读太多的微基准评测(microbenchmark),性能提升来自于好的设计和适当的架构。
我们以一个基于+=操作的基本的串联接为开始:
public static String concat1(String s1, String s2, String s3, String s4, String s5, String s6) { String result = ""; result += s1; result += s2; result += s3; result += s4; result += s5; result += s6; return result; }
String是不可变的,因此编译后的代码会创建出许多的中间String对象,这些对象会使垃圾收集器工作负荷加大,一种常见的补救办法是引用StringBuffer,使得代码看起来像这个样子:
public static String concat2(String s1, String s2, String s3, String s4, String s5, String s6) { StringBuffer result = new StringBuffer(); result.append(s1); result.append(s2); result.append(s3); result.append(s4); result.append(s5); result.append(s6); return result.toString(); }
不过代码会变得不那么易读,在这一方面是不太令人满意的。
使用JDK 6.0_02和服务器HotSpot的编译器,我可以在2010毫秒之内执行concat1()一百万次,但执行concat2()相同的次数只需要734毫秒,就这一点来说,我很高兴自己让代码的运行速度加快了三倍,不过,如果只是0.1%的程序的速度变得加快三倍的话,用户是不会注意到这一点的。
追溯回到JDK1.3的年代,我还有第三个用来使自己的代码运行得更快的方法,作为创建一个空的StringBuffer的替代,我让变量的大小符合所需的字符的个数,像这样:
public static String concat3(String s1, String s2, String s3, String s4, String s5, String s6) { return new StringBuffer( s1.length() + s2.length() + s3.length() + s4.length() + s5.length() + s6.length()).append(s1).append(s2). append(s3).append(s4).append(s5).append(s6).toString(); }
我设法使得在604毫秒之内调用该方法一百万次,甚至快过concat2(),不过,这是最好的添加串的方法吗?而什么样的方法才是最简单的呢?
Concat4()中的方法给出了另一种方式:
public static String concat4(String s1, String s2, String s3, String s4, String s5, String s6) { return s1 + s2 + s3 + s4 + s5 + s6; }
几乎不可能有比这更简单的做法了,有趣的是,在Java SE 6中,我可以在578毫秒之内调用这段代码一百万次,这甚至比远更复杂的concat3()好得多。与我们之前最好的努力结果比起来,这个方法更清晰、更易懂且速度更快。
Sun在J2SE 5.0中引入了StringBuilder,除了不是线程安全的这一点之外,其几乎与StringBuffer相同,对于StringBuffer来说,线程安全通常是不必要的,因为其很少在线程之间共享。在使用+操作符来添加串时,J2SE 5.0和Java SE 6的编译器会自动地使用StringBUilder,如果StringBuffer已是硬编码了的话,则这一优化将不会发生。
当应用中的一个时间关键的方法引发了明显的瓶颈时,这样做有可能会提高串连接的速度:
public static String concat5(String s1, String s2, String s3, String s4, String s5, String s6) { return new StringBuilder( s1.length() + s2.length() + s3.length() + s4.length() + s5.length() + s6.length()).append(s1).append(s2). append(s3).append(s4).append(s5).append(s6).toString(); }
不过,这样的做法会妨碍Java平台的未来版本对系统的自动提速,同样,其使得代码更难于读懂。
查阅对Heinz Kabutz所做的完整访谈。