《Effective Java》学习笔记七——通用程序设计

《Effective Java》学习笔记

将局部变量的作用于最小化

要使局部变量的作用于最小化,最有力的方法就是在第一次使用它的地方声明。

几乎每个局部变量的声明都应该包含一个初始化表达式。

如果在循环终止之后不再需要循环变量的内容,for循环就优先于while循环。

使方法小而集中。如果把两个操作合并到一个方法中,与其中一个操作相关的局部变量就有可能会出现在执行另一个操作的代码范围之内。为了防止这种情况发生,只要把这个方法分成两个,每个方法各执行一个操作。

for-each循环优先于传统的for循环

当见到冒号(:)时,可以把他读作“在…里面”。因此for-each循环可以读作“对于元素中的每个元素e”。注意,利用for-each循环不会有性能损失,甚至用于数组也一样。实际上,在某些情况下,比起普通的for循环,他还稍有些性能优势,因为他对数组索引的边界值只计算一次。虽然可以手工完成这项工作,但程序员并不总会这么做。
在对多个集合进行嵌套式迭代时,for-each循环相对于传统for循环的这种优势还会更加明显。
for-each循环不仅让你遍历集合和数组,还让你遍历任何实现Iterable接口的对象。
实现Iterable接口并不难。如果你在编写的类型表示的是一组元素,即使你选择不让他实现Collection,也要让他实现Iterable。这样可以允许用户利用for-each循环遍历你的类型,会令用户永远感激不尽的。

总之,for-each循环在简洁性和预防Bug方面有着传统的for循环无法比拟的优势,并且没有性能损失。应该尽可能的使用for-each循环。遗憾的是,有三种常见的情况无法使用for-each循环:

  • 过滤——如果需要遍历集合,并删除选定的额元素,就需要使用显式的迭代器,以便可以调用它的remove方法;
  • 转换——如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值;
  • 平行迭代——如果需要并行的遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。

了解和使用类库

通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验。

在每个重要的发行版本中,都会有许多新的特性被加入到类库中,所以与这些新特性保持同步是值得的。每个程序员都应该熟悉java.lang、java.util,某种程度上还有java.io中的内容。

如果需要精确的答案,请避免使用float和double

float和double主要为了科学计算和工程计算而设计,执行二进制浮点运算,这是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们没有提供完全精确的结果,所以不适合用于需要精确结果的场合,尤其是货币计算。

BigDecimal允许完全控制舍入,如果业务要求涉及多种舍入方式,使用BigDecimal很方便,如果性能很关键,涉及的数值不大,就可以使用int或者float,如果数值范围没有超过9位十进制数字,可以使用int,如果不超过18位数字,使用long,如果数值可能超过18位,就必须用BigDecimal。

基本类型优先于装箱基本类型

Java 1.5增加自动装箱和自动拆箱,对应基本类型int、double、boolean,装箱基本类型是Integer、Double、Boolean。

基本类型和装箱基本类型之间的三个主要区别:

  1. 基本类型只有值,而装箱基本类型具有与它们的值不同的同一性(两个装箱基本类型可以具有相同的值和不同的同一性)
  2. 基本类型只有功能完备的值,而每个装箱基本类型除了它对应的基本类型的所有功能值之外,还有个非功能值:null
  3. 基本类型通常比装箱基本类型更节省空间和时间。

适合使用装箱基本类型的地方:

  1. 作为集合中的元素,键和值;
  2. 在参数化类型中,必须使用装箱基本类型作为类型参数;
  3. 在进行反射方法调用时,必须使用装箱基本类型。

如果其他类型更适合,则尽量避免使用字符串

不应该使用字符串的情形:

  1. 字符串不适合代替其他的值类型;
  2. 字符串不适合代替枚举类型;
  3. 字符串不适合代替聚集类型;
  4. 字符串也不适合代替能力表;

当心字符串连接的性能

为连接n各字符串而重复地使用字符串连接操作符,需要n的平方级的时间。这是由于字符串不可变而导致的结果。当两个字符串被连接在一起时,它们的内容都要被拷贝。

应该使用StringBuilder的append方法,另一种方法是,使用字符数组,或者每次只处理一个字符串,而不是将它们组合起来。

通过接口引用对象

如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。

如果你养成了用接口作为类型的习惯,你的程序将会更加灵活。当你决定更换实现时,所要做的就只是改变构造器中类的名称(或者使用一个不同的静态工厂)。

如果没有合适的接口存在,完全可以用类而不是接口来引用对象。

如果对象属于这种基于类的框架,就应该用相关的基类来引用这个对象,而不是用它的实现类。

接口优先于反射机制

核心反射机制java.lang.reflect提供了“通过程序来访问关于已装载的类的信息”的能力,给定一个Class实例,可以获得Constructor、Method、Field实例,这些对象提供“通过程序来访问类的成员名称、域类型、方法签名等信息”的能力。

反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在,存在的代价:

  1. 失去编译时类型检查的好处,包括异常检查。
  2. 执行反射访问所需的代码很长。
  3. 性能上的损失。

反射功能只是在设计时被用到,通常,普通应用程序在运行时不应该以反射的方式访问对象。
有些复杂的应用程序需要使用反射机制,包括类浏览器、对象检测器、代码分析工具、解释型的内嵌式系统。在RPC中使用反射机制也是合适的,这样就不再需要存根编译器。
对于有些程序,必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过它们可以引用这个类,就可以以反射的方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。

对于复杂的系统编程任务,反射是必要的,如果编写的程序必须与编译时未知的类一起工作,如果可能,就应该仅仅使用放射机制来实例化对象,而访问对象时则用编译时已知的某个接口或者超类。

谨慎地使用本地方法

Java Native Interface(JNI)允许Java应用程序可以调用本地方法(native method),所谓本地方法是指用本地程序设计语言(比如C或者C++)来编写的特殊方法。本地方法在本地语言中可以执行任意的计算任务,并返回到Java程序设计语言。

从历史上看,本地方法主要有三种用途,它们提供了“访问特定于平台的机制”的能力,比如访问注册表和文件锁。它们还提供了访问遗留代码库的能力,从而可以访问遗留数据。最后,本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。

使用本地方法来提高性能的做法不值得提倡。

本地语言不是安全的。使用本地方法的应用程序也不再能免受内存毁坏错误的影响。因为本地语言是平台相关的,使用本地方法的应用程序也不再是可自由移植的。使用本地方法的应用程序也更难调试。在进入和退出本地代码时,需要行管的固定开销。

谨慎地进行优化

有三条与优化有关的格言是每个人都应该知道的。

很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任何其他的原因——甚至包括盲目地做傻事。

不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。

在优化方面,我们应该遵守两条规则:

  1. 规则1:不要进行优化;
  2. 规则2(仅针对专家):还是不要进行优化——也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。

要编写好的程序而不是快的程序。好的程序体现了信息隐藏的原则:只要有可能,它们就会把设计决策集中在单个模块中,因此,可以改变单个决策,而不会影响到系统的其他部分。

努力避免那些限制性能的设计决策。

要考虑API设计决策的性能后果。

为获得好的性能而对API进行包装,这是一种非常不好的想法。

在每次试图做优化之前和之后,要对性能进行测量。

遵守普遍接受的命名异常

把标准的命名惯例当作一种内在的机制来看待。

字面惯例的例子

标识符类型 例子
com.google.inject, org.joda.time.format
类或者接口 Timer, FutureTask, LinkedHashMap, HttpServlet
方法或者域 remove, ensureCapacity, getCrc
常量域 MIN_VALUE, NEGATIVE_INFINITY
局部变量 i, xref, houseNumber
类型参数 T, E, K, V, X, T1, T2