Item 38: 检查参数的有效性
比较推荐的做法是,在方法体的开头或者在构造函数之中就对参数进行有效性检查。尽早的检查,对调试的帮助越大。
在文中出现提到了java的断言(Assertions)机制。
正确使用Assert的方法应该是:
- 可以在方法体的开头的地方进行有效性检查
- 普通的运行方法,断言不会起作用(会直接略过)。除非在运行的时候加上参数:
-ea
/-enableassertaions
。 - 通常Assert的代码不应该在Production代码之中,只会出现在debug/test/dev的阶段。
除非参数检查的逻辑跟原本的逻辑就重复,或者参数检查逻辑非常消耗资源,一般建议都进行参数检查。
本文原创,原文链接:https://www.flyml.net/2017/03/13/effective-java-ch7-method/
Item 39: 必要时进行保护性拷贝(defensive copy)
保护性拷贝能防止调用者有意或者无意之中破坏类的内部逻辑。比如下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Broken "immutable" time period class public final class Period { private final Date start; private final Date end; /** * @param start the beginning of the period * @param end the end of the period; must not precede start * @throws IllegalArgumentException if start is after end * @throws NullPointerException if start or end is null */ 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; } // Remainder omitted } |
虽然在初始化的时候,已经限制了start <= end, 但是并不能防止可变类Date
在后期被改变。如下面的操作方式:
1 2 3 4 5 6 7 8 |
// Attack the internals of a Period instance Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); // Modifies internals of p! System.out.println(p.end()); // 输出:Sat Mar 11 13:54:53 CST 1978 |
可以看到, 最终的输出结果是1978年。
解决方法:在构造函数之中,新new一个Date
对象,而不是直接使用。
1 2 3 4 5 6 7 |
// Repaired constructor - makes defensive copies of parameters public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) throw new IllegalArgumentException(start +" after "+ end); } |
Q: 在构造函数之中,是否可以用
Date.clone()
的方式呢?A:不推荐。 因为Date并不是final类型, 可以被子类化。 在被子类化的同时,
clone()
有可能已经被恶意修改。 因此是不可以被信任的。另外,可以尝试使用Joda Time。Joda Time 里面的时间类型基本都是final,可以放心大胆的调用
clone()
方法。同时,Java8的
java.time
做了很多改进, 可以学习看看。
上面的代码还有一个可以被攻击的地方:
1 2 3 4 5 |
// Second attack on the internals of a Period instance Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.end().setYear(78); // Modifies internals of p! |
修改的方法类似构造函数那部分:返回的时候也new一把
1 2 3 4 5 6 7 |
// Repaired accessors - make defensive copies of internal fields public Date start() { return new Date(start.getTime()); } public Date end() { return new Date(end.getTime()); } |
PS: JAVA默认的Date在安全性上面真心蛋疼。切换到Joda Time 应该会好很多。
不过其他可变类也同样要面对这种情况。
那么是不是所有可变类都要这么干?
一般来说,如果无法容忍约束条件不能被破坏,那么在设计这个类的时候,就应该考虑进行保护性拷贝。
比较方便的是使用不可变类作为内部组件。
本文原创,原文链接:https://www.flyml.net/2017/03/13/effective-java-ch7-method/
Item 40: 谨慎设计方法签名(method signatures)
本条目是一些设计技巧的总结。
- 选择容易理解的方法名称
- 方法不要太碎片化 (PS:这个感觉非常依赖个人经验)
- 避免太长的参数列表
书中推荐不超过4个。如果有的功能确实需要好几个参数, 可以尝试
Builder
模式 - 参数类型优先使用接口而不是类(Item 52)
比如使用Map而不是使用HashMap作为输入的类型。这样客户端输入比如Hashtable / HashMap / TreeMap 等都可以正常进行工作。
- 对于
boolean
参数, 优先使用两个元素的枚举除非参数名称比较长, 否则true/false 确实很容易不清楚具体true/false各是什么
Item 41: 慎用重载(overloading)
反例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Broken! - What does this program print? public class CollectionClassifier { public static String classify(Set<?> s) { return "Set"; } public static String classify(List<?> lst) { return "List"; } public static String classify(Collection<?> c) { return "Unknown Collection"; } public static void main(String[] args) { Collection<?>[] collections = { new HashSet<String>(), new ArrayList<BigInteger>(), new HashMap<String, String>().values() }; for (Collection<?> c : collections) System.out.println(classify(c)); } } |
输出的结果是三个Unknown Collection
重载最大的问题在于具体调用哪个版本有可能在运行时才知道。 一个解决方法就是,参数列表要不太一样。比如个数、名字等等。
推荐的方式是:使用多个方法名不一样的方法,而不是重载。特别是参数数目都一样的情况之下。即使一定要这么做,也不要想上面的反例这样,因为List/Set都是Collection的子类。
PS:感觉校招题特别喜欢在重载上面做文章。
我感觉在实际coding之中, 谁瞎用或者乱用重载、继承,要被喷死的节奏。。。
Item 42: 谨慎使用可变参数
PS: 不知道这标题是不是有点吓人, 但是可变参数在现代JDK以及很多Java SDK之中的应用场景还是很多的。 最常见的常见就是在写数据库的时候,PreparedStatement
里面一般都是用的可变参数。
至少有一个可用参数时
当传入的参数至少有一个需要存在的时候, 书中提倡这样使用可变参数:
1 2 3 4 5 6 7 |
static int min(int firstArg, int... remainingArgs) { int min = firstArg; for (int arg : remainingArgs) if (arg < min) min = arg; return min; } |
即设置两个参数, 强制第一个参数有效。 这样做的好处是不需要在方法体之中检查是否至少有一个参数了。
可变参数对性能有一些影响
可变参数方法的调用会进行一次数组的分配与初始化。如果凭经验觉得数组的分配与初始化性能消耗比较严重的时候, 可以参考下面的做法:
前提:确认参数绝大部分情况下参数的个数不超过3个。
1 2 3 4 5 |
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">()</span> </span>{ } <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">(<span class="hljs-keyword">int</span> a1)</span> </span>{ } <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">(<span class="hljs-keyword">int</span> a1, <span class="hljs-keyword">int</span> a2)</span> </span>{ } <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">(<span class="hljs-keyword">int</span> a1, <span class="hljs-keyword">int</span> a2, <span class="hljs-keyword">int</span> a3)</span> </span>{ } <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">(<span class="hljs-keyword">int</span> a1, <span class="hljs-keyword">int</span> a2, <span class="hljs-keyword">int</span> a3, <span class="hljs-keyword">int</span>... rest)</span> </span>{ } |
[书中还对JDK从1.4升级到1.5的一些不妥做法做了一些说明, 主要火力是说Arrays.asList()
的改造不太靠谱, 会让编译器的类型检查失效。在这里就不详细讨论了。]
Item 43: 返回0长度的数组或者集合,而不是null
只要有可能,返回0长度的数组/集合。这样在客户端、调用者的代码之中, 逻辑就很自然, 而不是单独写几行判断是不是null的检查代码。
Item 44:为所有导出的API元素编写文档注释(doc comment)
文中提倡:
为了正确的编写API文档, 需要在每个被导出的类、接口、构造函数、方法、域声明之前增加文档注释。
但是比如Spark,很多方法级别的说明, 都是在类前面写了一大篇说明文档。
文档注释的一些简单语法规则
具体可以参考极客学院的文章:
http://wiki.jikexueyuan.com/project/java/documentation.html
注意: 不知道为什么,在很多文章之中都没有出现书中提到的@code
、@literal
以及this
在文档注释之中的特殊作用。可能使用的姿势不对, this
确实好像没有特殊作用, 而@code
@literal
等确实是可以起作用的。
包级别的文档注释
应该放在package-info.java
之中。package-info.java
也可以包含声明与注解。我们可以实际参考guava之中是如何使用package-info.java
的:
https://github.com/google/guava/blob/master/guava/src/com/google/common/io/package-info.java
https://github.com/google/guava/blob/master/guava/src/com/google/common/primitives/package-info.java
注:Guava在很多包里面都有这个package-info.java. (人工看的, 并没有一个个的去检查。。。)。因此这个应该是一个不错的实践方法。
Html validity checker
现代IDE通常可以直接显示编写出来的文档注释是否语法正确有效。
新增:关于换行的思考
之前在写文档注释的时候, 经常需要手写换行符。 参考Guava等开源项目的做法, 他们一般使用<p></p>
标签划分段落。
感觉JavaDoc 如果能支持Markdown的格式就很爽了。这个貌似可以通过第三方支持:https://dzone.com/articles/using-markdown-syntax-javadoc
如果能原生直接支持就更好了。
文章评论