目录
Item 49: Check parameters for validity
Item 50: Make defensive copies when needed
Item 51: Design method signatures carefully
Item 52: Use overloading judiciously
Item 53: Use varargs judiciously
Item 54: Return empty collections or arrays, not nulls
Item 55: Return optionals judiciously
Item 56: Write doc comments for all exposed API elements
Item 49: Check parameters for validity
检查参数的有效性
术语:
- failure atomicity:失败原子性。
应在方法体的开头处对入参进行有效性检查,并在文档中清楚地指明参数的所有限制。
例外情况:
- 有效性检查消耗很大或不实际。
- 在代码处理过程中已经隐含了这种检查,如Collections.sort(List)隐含了对未继承Comparable接口的对象抛出ClassCastException。
Item 50: Make defensive copies when needed
必要时进行保护性拷贝
术语:
defensive copies:保护性拷贝,指通过深度拷贝等措施来防止类的成员变量(field)面临被篡改的危险。
防止类成员变量被调用代码篡改的方法:
(1)保护性拷贝:
- 在给类的成员变量赋值的位置,如构造函数、set函数;
- 供外部的访问类成员变量的函数,如get函数;
(2)使用不可变(immutalbe)变量作为类的成员变量,如用Instant / LocalDateTime / ZoneDateTime代替Date类型变量。
注意:
- 长度非0的数组是可变的(mutable)。
- 保护性拷贝应该在参数校验之前。
- 如果保护性拷贝代价太大,而类信任调用它的客户端,则可以在文档中指明不得修改受影响的成员变量。
Item 51: Design method signatures carefully
谨慎设计方法签名
- 谨慎选择方法名称:遵循标准命名习惯,采用大众熟悉的名称,要易于理解、风格统一。
- 不要过于追求提供便利的方法(?不解啥意思)。
- 避免过长的参数列表:不要超过4个。
- 对于参数类型,优先使用接口而不是类:即用接口来代替实现该接口的类,如定义入参为HashMap的方法时,入参类型推荐使用Map,而非HashMap。
- 对于boolean参数,优先使用两个元素的枚举类型,除非boolean的含义对于方法名是清晰的:枚举类型可读性强、易于扩展,示例:
// 温度计类Thermometer有如下一个静态工厂方法,后续它能很容易增加KELVIN这个新元素
public enum TemperatureScale { FAHRENHEIT, CELSIUS }
Item 52: Use overloading judiciously
慎用重载
术语:
- overloading method:重载方法,指同名不同参数的函数。编译时决定调用哪个重载方法(静态)。
- overriding method:覆盖方法,指子类中重写父类的方法。运行时决定调用哪个子类的重载方法(动态)。
调用哪个重载方法是在编译时做出决定的,看如下代码:
public class CollectionClassify {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> l) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?>[] cs = {
new HashSet<String>(),
new ArrayList<String>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : cs) {
System.out.println(classify(c)); // 3次打印的都是 Unknown Collection
}
}
}
虽然运行时每次遍历的集合类型都不一样,但因为在编译时已经确定了哪个重载方法,所以调用的是同一个方法。如果需要运行时决定具体类型,可将上面函数改写成子类重载的方式,或者用instanceof显式判断:
public static String classify (Collection < ? > c){
return c instanceof Set ? "Set" :
c instanceof List ? "List" : "Unknown Collection";
}
安全保守的做法:
- 不要对可变参数的函数进行重载。
- 尽量不要对有相同参数个数的函数进行重载;如果不可避免,则至少存在一个彼此之间不能类型转换的参数,如int和List;如果还是不可避免,则应该保证传入相同参数时,各重载方法的结果是一致的。
一个常见的容易混淆的重载方法(java库中少数容易混淆的api):
List<Integer> list = new ArrayList<>();
list.add(0); list.add(1);
list.remove(1); // 表示删除index为1的元素
list.remove((Integer)1); // 表示删除值为1的元素
Item 53: Use varargs judiciously
慎用可变参数
可变参数varags方法的正式名为variable arity methods(可匹配不同长度的变量的方法),可接受0或者多个指定类型的参数。
实现机制:首先创建一个数组(大小为调用位置所传递的参数数量),然后将参数值赋值给数组,最后将数组传递给方法。
在重视性能的场景下,要小心使用可变参数方法。每次调用都会导致一次数组的分配和初始化。如果即要性能又要可变参数,可将常用的参数个数的情况设计为重载方法,如:
// 实际调查发现,95%调用的参数个数少于或等于2,则可重载0-2个参数的方法,来提升性能
public void foo(){}
public void foo(int a1){}
public void foo(int a1, int a2){}
public void foo(int a1, int a2, int... rest){}
Item 54: Return empty collections or arrays, not nulls
返回零长度的数组或者集合,而不是null
List<String> list= ...;
// 返回空集合
return new ArrayList<>(list); // 常用情况
return list.isEmpty() ? Collections.emptyList() : new ArrayList<>(list); // 对性能有严格要求情况,Collections.emptyList()是不可变的immutable
// 返回空数组
return list.toArray(new String[0]); // 常用情况,数组长度不为0也用此表达式
private static final String[] EMPTY_STRING_ARRAY = new String[0];
return list.toArray(new String[EMPTY_STRING_ARRAY ]); // 对性能有严格要求情况,长度为0的数组是不可变的
// list转数组时,不推荐使用预分配内存方法(即使数组长度不为0),否则有损性能,即
// return list.toArray(new String[new String[list.size()]]);
// 参考:https://m.imooc.com/wenda/detail/664892
当list和传入的数组大小一致时,toArray()直接向传入的数组进行赋值;当不一致时,toArray()会重新创建一个创建新内存空间存放数组并赋值。
探究list和toArray()和传入数组的内存地址关系:
final String[] ES = new String[0];
List<String> sl1 = Arrays.asList("a", "g");
List<String> sl2 = Arrays.asList("d", "t");
List<String> sl3 = new ArrayList<>();
List<String> sl4 = new ArrayList<>();
System.out.println(ES + " -->ES地址");
System.out.println(sl3.toArray(ES) + " -->sl3.toArray(ES)地址");
System.out.println(sl4.toArray(ES) + " -->sl4.toArray(ES)地址");
System.out.println(sl1.toArray(ES) + " -->sl1.toArray(ES)地址");
System.out.println(sl2.toArray(ES) + " -->sl2.toArray(ES)地址");
System.out.println(sl3.toArray(new String[0]) + " -->sl3.toArray(new String[0])地址");
System.out.println(sl4.toArray(new String[0]) + " -->sl4.toArray(new String[0])地址");
打印结果:
[Ljava.lang.String;@2d98a335 -->ES地址
[Ljava.lang.String;@2d98a335 -->sl3.toArray(ES)地址
[Ljava.lang.String;@2d98a335 -->sl4.toArray(ES)地址
[Ljava.lang.String;@16b98e56 -->sl1.toArray(ES)地址
[Ljava.lang.String;@7ef20235 -->sl2.toArray(ES)地址
[Ljava.lang.String;@27d6c5e0 -->sl3.toArray(new String[0])地址
[Ljava.lang.String;@4f3f5b24 -->sl4.toArray(new String[0])地址
Item 55: Return optionals judiciously
谨慎返回optional
当函数中存在无法返回值的情况时,一般有如下3种处理方式:
- 抛出异常,消耗大,因为要捕获整个堆栈轨迹。
- 返回null:调用位置要增加检查值是否为null的逻辑。
- (推荐)用Optional类型替换原来的返回类型:使用更灵活、不易出错。
永远不要对Optional返回类型的方法返回null(违背设计本意)。
optional不适用类型:自带空元素类型,如container types(含collection、map、stream、array、optional)。
不适用场合:
- 注重性能的时候,因为optional需要额外开销;
- map的key和value;
- collection和array中的元素。
永远不应该使用int/long/double这3种基本包装类型的Optional,而应该使用基本类型的(开销更低),但使用形式有所不同:OptionalInt、OptionalLong、OptionalDouble。(其他基本类型和常规用法一样)
Item 56: Write doc comments for all exposed API elements
第56条:为所有导出的API元素编写文档注释
在How to Write Doc Comments [Javadoc-guild]网站有教程。
应在类、接口、方法、字段等声明的前面添加文档注释。
pulbic类不应使用默认构造器,因为无法添加文档注释。
为了便于维护,应该对未导出(或非public)的类、接口、方法、字段也提供注释。
方法的注释应简洁地描述出它和客户端之间的约定(约束):
- 描述做什么(what)而非怎么做(how),用动词短语;
- 描述方法的前置条件和后置条件,前置条件有@throw中的unchecked exception、@param中的参数校验;
- 描述方法副作用,如启动了后台线程。
常用注释注解:
- @param:对每个参数都要描述,用名词短语或算术表达式。
- @throws:对每个受检和不受检异常,用if描述发生异常的条件。
- @retrun:非void的返回值,如果和description部分一样,则可以忽略。
- @implSpec:用于描述为了继承的父类中使用了自用模式(self-use)的方法
- {@code}:用于包裹注释中的代码
- {@litera}:用于包裹需要转义html元素符号的句子,如<、>、&等。
- {@index}:添加为生成的html注释文档的搜索关键词。
- {@inheritDoc}:表示从父类继承注释
这些注释后一般不用加英文句号,且句子出现英文句号后紧跟空格/换行符的,需要用{@litera}包裹,否则导致注释过早结束。
文档注释中可以添加html的tags元素(如<p><i>),javadoc工具会将注释转换为html文档。
javadoc有继承方法注释的能力,接口文档注释优于超类的注释。