第7章 主要是讨论方法设计的几个方面:如何处理参数和返回值,如何设计方法签名,如何编写文档。
条目38:检查参数的有效性
绝大多数方法和构造器对于传递给它们的参数值都会有些限制。例如索引值必须是非负数,甚至在一定范围之内,对象引用不能为 null ,等等,这些都是很常见的。
可以在文档中或方法注释中指明所有的这些限制,并且在方法体的开头处检查参数,以强制施加这些限制。这是"应该在发生错误之后尽快检测出错误"这一普遍原则的一个具体情况。
如果方法接收到无效的参数,并检测出来,可以清晰地抛适当的异常,比如 IllegalArgumentException, IndexOutOfBoundsException 或 NullPointerException,因为异常抛出时,我们可以清楚地看到调用栈,在调试时定位问题来源时能提供不少帮助。
对于有些参数,方法本身并没有用到,却被保存起来供以后使用,检验这类参数的有效性尤为重要。如果没有检验,等到用到的时候因为参数有问题再来定位问题,难度会增加不少。构造器正是这样,所有检查构造器参数的有效性是非常重要的,这样可以避免构造出来的对象违反了这个类的约束条件。
当然检查方法参数的有效性,这一原则也有例外的时候,当检查工作非常昂贵,或者根本就不切实际的,而且有效性检查已隐含在方法执行过程中。
虽然本条目鼓励对方法的参数进行检查,但并不是对参数的任何限制都是件好事。相反,在设计方法时,应该使它们尽可能地通用(约束越少越好),并符合实际的需求。
条目39:必要时进行保护性拷贝
没有对象的帮助时,虽然另一个类不可能修改对象的内部状态,但是对象很容易在无意识的情况下提供这种帮助。比如我们想要一个不可变的类,里面的域是可变对象,不允许外界对它进行修改,这样这个类就是不可变的。如果我们这个类中的域是从外界传进来的参数,或者在方法中直接返回这个域,那么外界就可以直接在修改这个域,从而使得这个类内部信息受到攻击,是可变的。
如果类信任它的调用者不会修改内部的组件,比如因为类及其客户端都是同一个包的双方,那么不进行保护性拷贝也是可以的。在这种情况下,类的文档中必须清楚地说明,调用者决不能修改受到影响的参数或者返回值。
条目40:谨慎设计方法签名
这是一些 API 设计技巧,它们不足以单独开设一个条目,所以合成一个。
- 谨慎地选择方法的名称。方法名字能说明方法的作用,那么这个方法就没什么大问题,命名上尽量选择与大众认可的名称相一致的名字,参考 Java 类库的 API。
- 不要过于追求提供便利的方法。对于类和接口所支持的每个动作,都提供一个功能齐全的方法,尽其所能,但方法太多会使得类难以学习、使用。所以只有当一项操作被经常用到的时候,才考虑为它提供快捷方式。
- 避免过长的参数列表。目标是四个参数,或者更少。参数太多,记忆难,使用难。
对于参数类型,要优先使用接口而不是类。客户端可以传入该类型下的其他实现。
对于 boolean 参数,要优先使用两个参数的枚举类型。它使得代码更容易阅读和编写,可扩展。
条目41:慎用重载
覆盖:子类拥有一个方法签名和父类一样的方法。
重载:方法名一样,接受的参数不一样。
被覆盖的方法在在运行时选择的依据是被调用方法所在的对象的运行时类型,是动态的。
而重载方法的选择是静态的,调用方法的对象的引用类型是什么,就走哪个方法,哪怕是父类引用指向子类实现。
覆盖容易满足多态的期望,而重载容易使期望落空。所以应该避免胡乱地使用重载机制,安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。如果方法使用可变参数,保守的策略是根本不要重载它。
如果遵守这些限制,那么客户端就不会陷入到"对于任何一组实际的参数,哪个重载方法是适用的"的疑问中,完全可以通过给方法起不同的名字,而不适用重载机制。
条目42:慎用可变参数
可变参数方法接受0个或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最好将数组传递给方法。
如果你真正需要让一个方法带有不定数量的参数时,可变参数就非常有效。
在重视性能的情况下,使用可变参数机制要特别小心。因为每次调用都会导致进行一次数组分配和初始化。如果凭经验确定无法承受这一成本,但又需要可变参数的灵活性,还有一个模式可以让你如愿以偿。假设确定对某个方法95%的调用会有3个或者更少的参数,就声明该方法的5个重载,每个重载方法带有0至3个普通参数,当参数的数目超过3个时,就使用一个可变参数方法。其实在 android 中的 View 的构造器就有多个重载。
条目43:返回零长度的数组或者集合,而不是 null
但返回的是 null时,客户端每次调用的时候都必须有额外的代码来处理 null 返回值,这样做容易出错,因为客户端可能会忘记处理 null 值。
如果担心每次都分配数组需要开销,可以每次都返回同一个零长度数组,因为零长度数组是不可变的,不可变对象可以被自由地共享。比如标准做法把一些元素从一个集合转存到一个类型化的数组时,它正是这样做的:
private final List<Cheese> chessesInStock = ...;
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheese(){
return cheeseInStock.toArray(EMPTY_CHEESE_ARRAY);
}
在这种习惯做法中,零长度数组常量被传递给 toArray 方法,以指明所期望的返回类型。正常情况下,toArrays 方法分配了返回的数组,但是,如果集合是空的,它将使用零长度的输入数组。Collection.toArray 的规范保证:如果输入数组大到足够容纳这个集合,它就将返回这个输入数组,否则就创建新数组,所以这种做法永远也不会分配零长度的数组。
条目44:为所有导出的 API 元素编写文档注释
一个 API 有文档,可以让客户端查阅时清楚的了解这个 API 的用法和注意事项。Javadoc 可以利用特殊格式的文档注释,根据源代码自动产生 API 文档。而不用人工生成文档。
方法的文档注释应该简洁地描述出它和客户端之间的约定。这个约定应该说明这个方法做了什么,而不是说明它是如何完成这项工作的。列举出这个方法的所有前提条件,和后置条件(调用成功完成之后,哪些条件必须要满足)。
一般情况下,前提条件是有 @throws 标签针对未受检的异常进行描述,一些受影响的参数可以用 @param 标记来描述。
如果方法有副作用,系统状态中可以观察到的变化,它不是为了获得后置条件而明确要求的变化,那么也应该在文档中描述出来。方法的线程安全性也应该描述。
如果方法的返回类型不是 void,那么可以用@return 来描述返回类型。
第8章,通用程序设计
这章主要是讨论 Java 语言的具体细节,讨论了局部变量的处理、控制结构、类库的用法、各种数据类型的用法,以及两种不是语言本身提供的机制(反射机制和本地方法)的用法。
第45条:将局部变量的作用域最小化
这一条和那条 使类和成员的可访问性最小化 本质上是类似的,将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。
要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。过早的声明变量不仅会使它的作用域过早地扩展,而且结束得也过晚了。当程序退出需要用到该变量之后,该变量仍是可见的。如果变量在它的目标使用区域之前或之后被意外地使用的话,后果将可能是灾难性的。
第46条:for-each 循环优先于传统的 for 循环
在没有 for-each 之前,传统的 for 循环是这样的:
for (Iterator i = c.iterator(); i.hasNext();){
doSomething((Element)i.next());
}
for (int i = 0; i < a.length; i++) {
doSomething(a[i]);
}
而 for-each 循环是这样的:
for (Element e: elements) {
doSomething(e);
}
for-each 循环比传统 for循环好在循环中隐藏迭代器和索引变量,避免了混乱和出错的可能。
有三种常见的情况下无法使用 for-each 循环:
- 过滤:需要在遍历集合时需要删除选定的元素,这需要使用显式的迭代器,调用 remove 方法。
- 转换:需要在遍历列表或数组时,取代它的部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。
- 平行迭代:需要并行地遍历多个集合,就需要显式地控制迭代器或者索引变量,以便所有的迭代器或者索引变量都可以得到同步前移。
第47条:了解和使用类库
通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验。
第48条:如果需要精确的答案,请避免使用 float 和 double
float 和 double 类型主要是为了科学计算和工程计算而设计,它们执行二进制浮点运算,为了在广泛的数值范围上提供较为精确的快速近似计算而设计,然后并没有提供完全精确的结果。所以不合适用在需要精确结果的场合,特别是货币计算。
如果用于货币计算,可以使用 BigDecimal,int 或者 long
使用 BitDecimal 有两个缺点:无法和基本运算类型一样使用简单的运算符,运算慢。
如果性能非常关键,并且又不介意自己记录十进制小数点,而且所涉及的数值又不太大,就可以使用 int 或者 long。如果数值范围没有超过9位十进制数字,就可以使用 int;如果不超过18位数字,就可以使用 long。如果数值可能超过18位数值,就必须使用 BigDecimal。
第49条:基本类型优先于装箱基本类型
Java 有一个类型系统由两部分组成,包含基本类型,如 int、double 和 boolean,和引用类型。每个基本类型都有一个对应的应用类型,称作装箱基本类型。
由于自动装箱和自动拆箱的存在,基本类型和装箱类型使用上比较模糊,但他们之间还是有区别的。
它们的区别主要在于:基本类型只有值,不是对象;而装箱类型是拥有值的对象,可为 null,两个具备相同值的装箱对象有不同的同一性。基本类型比装箱类型更节省时间和空间。
当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱。
Integer a = new Integer(42);
Integer b = new Integer(42);
a == b; -> false ,不同的对象
Integer c;
c == 42; // null 自动拆箱抛出异常
装箱基本类型有几个合理的用处:作为集合中的元素、键和值;在参数化类型中作为类型参数;进行反射的方法调用时。
第50条:如果其他类型更合适,则尽量避免使用字符串
字符串被用来表示文本,但当数据类型不是文本信息时,那么应该尽量选择其他类型。
如果是数值,就应该被转换层适当的数值类型,比如 int、float等。如果是 "是 - 或 - 否",就应该转换成 boolean 类型。
字符串不合适代替枚举类型,也不合适代替聚集类型(最好是编写一个类来描述数据集),也不适合代替能力表(不可伪造的键)。
第51条:担心字符串连接的性能
字符串连接操作符 + 是把多个字符串合并为一个字符串的便利途径,但由于每一次操作都会拷贝两个字符串的内容从而产生一个新的对象,如果连接的字符串数量为 n,那么需要 n 的平方级的时间。如果数量多的话,用 StringBuilder。
第52条:通过接口引用对象
优先使用接口而不是类来引用对象,如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。
这样可以更加灵活,当需要替换实现时,只需要改变引用的象就行,而其他使用该接口的地方都不需要做修改。
如果没有合适的接口存在,完全可以用类而不是接口来引用对象。
第53条:接口优先于反射机制
核心反射机制提供了通过程序来访问关于已装载的类的信息的能力。给定一个 Class 实例,就可以获得 Contructor、Method 和 Field 实例,这些对象提供了通过程序来访问类的成员名称、域类型、方法签名等信息的能力。
这种能力需要付出一定的代价:
- 丧失了编译是类型检查的好处,包括异常检查。如果程序企图用反射方式调用不存在的或者不可访问的方法,在运行时它将会失败。
- 执行反射访问所需要的代码非常笨拙和冗长。
- 性能损失。反射方法调用比普通方法调用慢了许多。
通常普通应用程序在运行时不应该以反射反射访问对象,当程序必须在获得编译时无法获取到的类时,可以以非常有限的形式使用反射机制,虽然也要付出少许代价,但是却可以获得许多好处。
如果你要编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。
一些依赖注入的框架也是这么实现的,使用接口作为引用,然后反射注入具体的实现。
第54条:谨慎地使用本地方法
Java Native Interface 允许 Java 应用程序可以调用本地方法,本地方法在本地语言中可以执行任意的计算任务,并返回到 Java 程序设计语言。
本地方法提供了这些用途:访问特定于平台的机制的能力,访问遗留代码库的能力,另外还可以用来编写一部分注重性能的功能。
但是随着 Java 版本的升级,一部分的特定于平台的能力 Java 语言也可以访问了;JVM 实现变得越来越快,对于大多数任务,即使不使用本地方法也可以获得与之相当的性能。
使用本地方法有一些缺点:本地语言不是安全的,需要自己管理内存;不再是可自由移植的;调试上也更难;进入和退出本地方法都需要相关的固定开销。
第55条:谨慎地进行优化
不要因为性能而牺牲合理的结构。要努力编写好的程序而不是快的程序。好的程序体现了信息隐藏的原则:只要有可能,它们就会把设计决策集中在单个模块中,因此,可以改变单个决策,而不会影响到系统的其他部分。
努力避免那么限制性能的设计决策。当一个系统设计完成之后,其中最难以更改的组件是那些指定了模块之间交互关系以及模块与外界交互关系的组件。这些设计组件中,最主要的是API、线路层协议以及永久数据格式。这些组件不仅在事后难以甚至不可能改变,而且它们都有可能对系统本该达到的性能产生严重的限制。改变的话,基本就是大的重构了。
在每次试图做优化之前和之后,要对性能进行测量。这要可以看出优化的效果。
在优化之前,通过测试工具查到哪里是性能最差的,最影响整个系统性能的,然后针对此处做优化,也许更容易取得成效。
第56条:遵守普遍接受的命名惯例
名字是起给程序员看的,而不是机器。当你的命名是普遍接受的命名惯例时,别的程序员在阅读,使用你的代码时就容易理解,也不易出错。