第七章 方法
第七章方法(Effective Java 中文版第2版)
本章的焦点集中在可用性、健壮性和灵活性上。
38.检查参数的有效性
当编写方法或者构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写到文档中,并且在每个方法的开头处,通过显示的检查来实施这些限制。即应该在发生错误之后尽快检测出错误 ,可以避免很多异常和不正确的却无法检测的结果。
通过进行有效性检查,付出的努力远远小于带来的异常。
39.必要时进行保护性拷贝
Java是一门安全的语言,即它对于缓冲区溢出、数组越界、非法指针及其他内存破坏错误都自动免疫。但对于客户的不良行为,仍能产生错误。
例如下面的类,它声称可以表示一段不可变的时间周期。
public final class Period{
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + "after" + end);
}
this.start=start;
this.end=end;
}
public Date start(){
return start;
}
public Date end(){
return end;
}
…//
}
乍一看,这个类是不可变的,并且有一个强约束条件:周期的起始时间不能在约束时间之后。然而,因为Date类本身是可变的,因此很容易违反这个条件。
用了新的构造器之后,上述的攻击对Period实例不再有效。注意,保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象。
此时,改变Period实例仍有可能,因为它的访问方法提供了对其可变内部成员的访问能力。
例如:
Date start=new Date();
Date end=new Date();
Period p=new Period();
p.end.setYear(78);
为了使Period实例不能被改变,只需修改这两个访问方法,使它返回可变内部域的保护性拷贝即可。
public Date start(){
return new Date(start.getTime());
}
public Date end(){
return new Date(end.getTime());
}
采用了新的构造器和访问方法之后,Period是真正不可变的了。
因此,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须进行保护性的拷贝。
40.谨慎设计方法签名
API设计技巧:
- 谨慎使用方法签名。具体包括选择易于理解的和与大众认可的相一致的。
- 不要过于追求提供便利的方法
- 避免过长的参数列表。最好是四个参数或者更少。
缩短参数列表的方法:
(1)、把方法分解成多个方法。
(2)、创建辅助类。
(3)、从对象构建到方法调用都采用Builder模式。。
对于参数类型,优先选择接口而不是类。
41.慎用重载
你可能会认为这个程序会打印出Set,List,Unknown Collection,但实际上它是打印Unknown Collection三次。打印三次的原因是classify方法被重载,而要调用哪个重载方法是在编译时做出决定的。因为该参数的编译时类型为Collection<?>,所以唯一合适的重载方法只有第三个:classify(Collection<?>)。
注意:对于重载方法的选择是静态的,而对于被覆盖的方法的选择是动态的。即选择被覆盖的方法的正确版本是在运行时进行的,选择的依据是被调用方法所在对象的运行时类型。
例如下面代码:
输出结果是:
这个程序打印出的结果,正如你的预期。
重载机制安全而保守的策略是永远不要导出两个具有相同参数数目的重载方法。若果方法使用可变参数,根本不要重载它。而是用给方法起不同的名称来替代。
具体实例,ObjectOutputStream类,对于每个基本类型,以及几种引用类型,他的write方法都不同,如writeBoolean(boolean), writeInt(int),writeLong(long)。
42.慎用可变参数
Java1.5版本中增加了可变参数方法,它接收0个或对各指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法。
可变参数的应用可以将以数组当作final参数的现有方法,改造成以可变参数代替,而不影响现有的客户端。
在重视性能的情况下,使用可变参数要特别小心。可变参数方法的每次调用都会导致一次数组分配和初始化。如果不愿损失性能,又需要可变参数的灵活性,可以尝试参数个数不同的方法重载。假设确定对某个方法95%的调用会有3个或者更少的参数,就声明该方法的5个重载,每个重载方法带有0到3个普通参数,当参数的数目超过3个时,就用一个可变参数方法。具体代码如下:
public void fool() {}
public void fool(int a1) {}
public void fool(int a1,inta2) {}
public void fool(int a1,inta2,int a3) {}
public void fool(int a1,inta2,int a3,int …res) {}
此时,所有调用中只有%5参数数量超过3个的调用会创建数组。
EnumSet类对它的静态工厂使用这种方法,最大限度地减少创建枚举集合的成本。
43.返回零长度的数组或集合, 而不是null
返回类型为数组或集合的方法, 应该返回一个零长度的数组或者集合, 没理由返回null. -> 不好用, 容易出错, 使方法本身更加复杂,没有性能优势.
实际应用中,对于不返回任何元素的调用, 每次都返回同一个零长度数组是有可能的. (例如: Collections.emtpySet).
44. 为所有导出的API元素编写文档注释
Javadoc可以根据源代码自动生成API文档.
要正确地为API建立文档, 就必须在每个导出的类, 接口, 构造函数, 方法和字段声明之前加上doc注释.
方法的文档注释应该简洁地描述出它和客户端之间的约定. 这个约定应该说明这个方法做了什么, 而不是如何完成这项工作的.
方法的文档注释还应该列举出:
所有前提条件. 一般可以利用@throws, @param.
后置条件.
副作用.
线程安全性.
每个参数: @param 名词短语.
返回值: @return 名词短语. (除非和方法描述一致时, 可根据所遵循的规定省略.)
每个异常: @throws 含if的名词短语.
按惯例, @param, @return, @throws后面的短语或句子都不用句点来结束.
{@code}用来标记代码, 多行代码要加上<pre>标签, 变成: <pre>{@code xxx}</pre>. 注意代码中的注解符号@需要被省略.
按照惯例, 方法的文档注释中的"this"指代的是当前的对象.
Java 8新增@implSpec: 描述方法和子类的关系. Java 9中Javadoc utility会忽略@implSpec, 除非你在命令行加上"implSpec????️Implementation Requirements:"
如果文档中要用HTML中的元素, 比如<, >和&, 需要加上{@literal}标签.
文档注释必须在代码和生成文档中都保证可读性, 如果不能两者都保证, 生成文档的可读性优先.
每个文档注释的第一句话成了该注释所属元素的概要描述(summary description). 为了避免混乱, 在类或者接口中不应该有两个成员或者构造方法有相同的概要描述. 尤其要注意方法重载.
对于方法和构造器而言, 概要描述应该是个完整的动词短语, 它描述了该方法所执行的动作. 对于类, 接口和域, 概要描述应该是一个名词短语.
Java 9引入了index, 方面文档查询. 偶尔你需要用{@index}加入额外的index.
泛型, 枚举, 注解都需要额外的注意:
当为泛型方法写文档时, 需要为每个泛型参数写文档注释.
枚举需要为每个常量写注释.
注解需要注释每个成员. (注解的概要描述是个动词短语.)
包级文档注释: package-info.java. 模块级文档注释: module-info.java.
在文档中还应该标明:
线程安全性 -> 不论是否线程安全.
如果可序列化, 需标明序列化形式.
Javadoc可以继承方法注释. 你可以用{@inheritDoc}标签来继承部分文档注释. (tricky and has some limitations).
参考资料
https://www.cnblogs.com/cangyikeguiyuan/p/4415507.html
https://github.com/mengdd/Effective-Java-Reading-Notes/blob/master/8%20Methods.md
《Effective java》