《Effective Java》读书笔记 - 7.方法
Chapter 7 Methods
Item 38: Check parameters for validity
直接举例吧:
/**
* ...其他的被我省略了
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0)
throw new ArithmeticException("Modulus <= 0: " + m);
... // Do the computation
}
这里记得要写文档。
对于那些unexported method(也就是自己内部用,而不公开到API里的方法),一般是用assertions,比如:
private static void sort(long a[], int offset, int length) {
assert a != null;
assert offset >= 0 && offset <= a.length;
... // Do the computation
}
想让他们有效,需要指定-ea (or -enableassertions) flag to the java interpreter。
Item 39: Make defensive copies when needed
主要是如果你的类有指向mutable对象的field的话,要进行defensive copy,举个例子,假设下面这个类中的date这个field是“内部实现”:
public final class Example{
private final Date date;
public Example(Date date,) {
this.date = date;
}
public Date getDate() {
return date;
}
Java中的Date是mutable的,所以很容易破坏这个类的内部field:
Date date = new Date();
Example example = new Example(date);
date.setYear(78);
所以我们要把constructor改成:
public Example(Date date,) {
this.date= new Date(date.getTime());
//对date这个field需要满足的条件进行验证
if (this.date...) {throw ...}
}
为什么要先defensive copy,再进行验证?因为如果先验证再copy的话,可能中间会被别的线程修改。
注意这里不能用Date的clone方法,因为Date是非final的,所以clone方法返回的可能是一个恶意的子类对象。
接着,getter方法也需要修改,因为很容易通过getter破坏内部field:
Date date = example.getDate();date.setYear(78);
所以需要这么修改,返回一个自己private field的copy:
public Date getDate() {
return new Date(date.getTime());
}
注意这里我们是可以用clone的,因为刚才由于我们对constructor的修改,内部的date field已经可以保证绝对就是一个Date对象,而不是什么Date子类对象。
其实这个例子告诉我们的真正教训是:最好用immutable的对象作为component。
由于defensive copy导致额外的性能损失,有的时候如果能保证调用方是可信的,比如
这个被调用的class和调用方都在同一个package,那么可以不用defensive copy,但是必须在文档中说明“谁都不能改这个对象”。
Item 40: Design method signatures carefully
本条item主要是讲你在设计API时候需要注意的事项:
一.Choose method names carefully。遵守标准的命名约定,以及同一个package中命名的一致性。
二.不要追求提供convenience method。因为这意味着更多的文档,维护,测试。只有真正需要时,再提供。如果不确定,就别提供。
三.不要定义过多参数,最好四个或四个以下。有一些技巧,比如将方法拆分成几个不同的方法,或者创建(inner)helper classes,或者使用item2介绍的Builder pattern。
四.For parameter types, favor interfaces over classes。
五.Prefer two-element enum types to boolean parameters。这是为了可读性和可维护性考虑的。
Item 41: Use overloading judiciously
对于重载方法的选择,是发生在编译时的。比如如果有两个重载方法,唯一不同的是接收参数的类型分别是Set<?>
和Collection<?>
,然后你定义了一个变量:
Collection<?> c = new HashSet<String>()
然后把c传给刚才的重载方法里去的时候,由于编译时c的静态类型是Collection<?>
,所以会选Collection<?>
的版本,而不会选Set<?>
的版本。
所以在API中尽量不要设计让人感到疑惑的方法重载,也就是让调用方无法确定哪个方法会被调用,保守的做法是尽量不要定义参数数量相同的重载方法,因为最多不过就是改个名字的事儿,比如ObjectOutputStream中的writeBoolean(boolean), writeInt(int), 和writeLong(long)。或者说如果两个类“完全没关系”,也就是neither one is an instance of the other one,那么重载也是安全的。如果不遵照这种简单的做法,编译器如何做出选择的规则是十分复杂的(我记得在看C# in depth的时候就是),在Java language specification中占了非常多篇幅,而很少有人真得理解这些规则。
有的时候如果两个重载方法做的事实际上一模一样,那么你可以违背上面说的简单规则,比如String中有两个重载方法分别接受StringBuffer和CharSequence类型的参数(CharSequence是被StringBuffer, StringBuilder, String, CharBuffer等类实现的接口),想让这两个方法的行为完全一致,只要让“范围小的”去调用“范围更大的那个”就行了:
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence) sb);
}
所以我们完全不在意编译器会选择哪个方法,因为反正最后的行为都是一样的。当然,Java库中也有一些违背上述简单原则的重载方法,可能会造成困惑,所以尽量避免。
下面举个造成困惑的例子:
Set<Integer> set = new TreeSet<Integer>();
List<Integer> list = new ArrayList<Integer>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
System.out.println(set + " " + list);
这里有个很贱的陷阱,其中两个remove的签名分别是Set.remove(Object o)和List.remove(int index),所以Set的remove是会移除某个元素,而List的remove是会移除某个位置的元素。这其中的历史原因是因为在Java引进自动装箱之前,List的remove(int index)和remove(Object o)是不可能导致困惑的,因为int和Object基本就是完全不搭嘎的两个玩意儿,而引入自动装箱后,List.remove方法“遭到破坏”。要得到期望的结果,需要改成list.remove((Integer) i)。
Item 42: Use varargs judiciously
你不应该把一个 以一个数组作为最后一个参数的方法 修改成varargs parameter,虽然这么做不会影响到已经存在的client code。为什么呢?举个例子(我自己想的例子):比如你把public static void test(Object[] objs)改成了public static void test(Object... objs),那么你如果int[] ints = {1,2,3};test(ints),就只能是一个“有一个元素的Object[]数组”,这个唯一的元素是一个数组对象{1,2,3},而你实际期待的行为应是:Integer[] ints =...;test(ints),这样的话就是一个“有三个元素的Object[]数组,三个元素分别是1,2,3的Integer对象”了。
所以慎用varargs,只有真正需要的时候再用。如果为了性能考虑(因为创建数组需要开销)你可以用以下方法:
public void foo() { }
public void foo(int a1) { }
public void foo(int a1, int a2) { }
public void foo(int a1, int a2, int a3) { }
public void foo(int a1, int a2, int a3, int... rest) { }
这让我想到了Console.WriteLine对各种primitive type都写了重载方法,避免不要的装箱。
Item 43: Return empty arrays or collections, not nulls
先举个反例:
private final List<Cheese> cheesesInStock = ...;
public Cheese[] getCheeses() {
if (cheesesInStock.size() == 0) return null;
...
}
这样会造成client每次都要做额外的null检查,其实是不必要的。所以你应该直接返回一个empty的List。另外,如果要对性能考虑的话,一个长度为0的数组对象肯定是immutable的对吧,那么你可以每次都返回这个相同的空数组对象(如果你需要返回一个空的数组或一个空的集合的时候),比如:
// The right way to return an array from a collection
private final List<Cheese> cheesesInStock = ...;
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
List的toArray的文档中说了:你传给它的参数决定了它返回数组的类型,如果你传给它的参数(一个数组)的容量够的话,那就返回你传的这个数组,否则它会自己new一个。所以上面的方法中,如果对应的List是empty的话,每次都会返回同一个数组(EMPTY_CHEESE_ARRAY )。如果你要返回一个Collection的话,可以用比如Collections.emptyList()。
Item 44: Write doc comments for all exposed API elements
你应该用doc comment给每一个要export的class, interface, constructor, method, field declaration都注上文档。你甚至应该为大部分unexported的类写上文档,为了更好的可维护性。
doc方法的时候应该简要地写出这个方法是干嘛的,而不是how it does the job(Item 17中说的专门用来被继承的方法例外),并应该说明参数应该满足的条件(一般是通过@throws
这个tag说明),以及造成的各种“side effects”(比如改变了某个对象,或者start了一个background thread)。以及还应该说明thread safety和是否能被序列化。具体来说,对于每一个参数都要写对应的@param tag
,除非是void否则就要写@return tag
,用@throws
说明每一个不管che不check的Exception。根据约定,@param tag
和@return tag
后面都是先跟一个名词,说明这玩意儿代表什么,而且@param, @return, or @throws tag
都不以句号结束,请看示例:
/**
* Returns the element at the specified position in this list.
*
* <p>This method is <i>not</i> guaranteed to run in constant time. In some implementations it
* may run in time proportional to the element position.
*
* @param index index of element to return; must be non-negative and less than the size of this list
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= this.size()})
*/
E get(int index);
你可以用HTML标签,但没必要弄得太花俏和复杂。注意{@code} tag
的作用是第一让代码高亮,第二别让HTML解释成标签(也就是说你可以随便使用大于小于号,而不需要用HTML的转义字符)。如果有多行代码,请用:<pre>{@ 多行代码}</pre>
By convention,注释中的this这个词儿一般都是指instance method的对象。
如果你懒得用HTML转义字符,那么可以用{@literal} tag,示例如下:
* The triangle inequality is {@literal |x + y| < |x| + |y|}.
大于小于号随便写。这个tag和{@code}
类似,只是不提供代码高亮功能。作者说用这个是为了让注释文档在源代码中也有很好的可读性,我非常同意。
注意每一个doc comment的第一句话是一句概括性的说明(summary description),比如上面的“Returns the element at the specified position in this list.”。同一个类中的两个或多个constructors,以及重载的方法,都不应该有相同的summary description。另外,在这个summary description中以“句号后面跟一个空格”作为结束符(我估计就是显示成HTML的时候只显示前面这么多作为总的概括说明),所以你需要把并不是作为结束符的某些字符用{@literal }
括起来,如{@literal M.S.}
。对于方法来说,这句summary description一般以动词开头,对于class和Interface以一个名词开头。
如果有泛型参数的话,记得也要描述一下,比如:
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
对于enum类型,每个constant都要写注释,书上是写在对应的constant的上面。
对于annotation类型,它的每一个成员也都要写注释。
{@inheritDoc}
可以复用doc comments,但是不在本书讨论范围内。
IDE有插件可以检查你对这些doc comments规则的的遵守。
《Effective Java》读书笔记 - 7.方法的更多相关文章
- Effective Java 读书笔记之六 方法
一.检查参数的有效性 1.考虑参数有哪些限制,把限制写到文档中,在方法的开头处通过显式地检查来实施这些限制. 二.必要时进行保护性拷贝 1.如果类具有从客户端得到或者返回的可变组件,类就必须考虑保护性 ...
- Effective Java 读书笔记(一):使用静态工厂方法代替构造器
这是Effective Java第2章提出的第一条建议: 考虑用静态工厂方法代替构造器 此处的静态工厂方法并不是设计模式,主要指static修饰的静态方法,关于static的说明可以参考之前的博文&l ...
- Effective Java读书笔记——第三章 对于全部对象都通用的方法
第8条:覆盖equals时请遵守通用的约定 设计Object类的目的就是用来覆盖的,它全部的非final方法都是用来被覆盖的(equals.hashcode.clone.finalize)都有通用约定 ...
- Effective java读书笔记
2015年进步很小,看的书也不是很多,感觉自己都要废了,2016是沉淀的一年,在这一年中要不断学习.看书,努力提升自己 计在16年要看12本书,主要涉及java基础.Spring研究.java并发.J ...
- Effective Java读书笔记完结啦
Effective Java是一本经典的书, 很实用的Java进阶读物, 提供了各个方面的best practices. 最近终于做完了Effective Java的读书笔记, 发布出来与大家共享. ...
- Effective java 读书笔记(2)
第四条:通过私有构造器强化不可实例化的能力 有时可能需要编写只包含静态方法和静态域的类,这样的工具类不希望被实例化,因为实例化对它来说没有意义. 然而,在缺少显式构造器的情况下,系统会自动提供一个缺省 ...
- [Effective Java 读书笔记] 第三章 对所有对象都通用的方法 第八 ---- 九条
这一章主要讲解Object类中的方法, Object类是所有类的父类,所以它的方法也称得上是所有对象都通用的方法 第八条 覆盖equals时需要遵守的约定 Object中的equals实现,就是直接对 ...
- Effective Java读书笔记--对所有对象都通用的方法
1.覆盖equals请遵守通用规定.不需要覆写equals的场景:a.类的每个实例都是唯一的.b.类不需要提供"逻辑相等"的测试功能.c.超类已经覆盖了equals的方法.d.类是 ...
- Effective Java 读书笔记之二 对于所有对象都通用的方法
尽管Object是一个具体的类,但设计它主要是为了扩展.它的所有非final方法都有明确的通用约定.任何一个类在override时,必须遵守这些通用约定. 一.覆盖equals时请遵守通用的约定 1. ...
- 【Effective Java读书笔记】创建和销毁对象(一):考虑使用静态工厂方法代替构造器
类可以提供一个静态方法,返回类的一个静态实例,如Boolean包装类的一个获取实例的静态方法 public static Boolean valueOf(boolean b) { return (b ...
随机推荐
- java实现spark常用算子之cogroup
import org.apache.spark.SparkConf;import org.apache.spark.api.java.JavaPairRDD;import org.apache.spa ...
- MongoDB 学习小笔记
1.配置:mongod --dbpath=D:\MongoDB\data mongo2.基本的增删查改 find() update()-- 整体更新,局部更新. 修改器: $inc db.person ...
- saiku数据实现实时更新
(1) # vim saiku-server/tomcat/webapps/ROOT/js/saiku/Settings.yaml 将 LOCALSTORAGE_EXPIRATION: 3600000 ...
- Django中数据库的增删改查
本随笔使用的是pycharm专业版2019.1.3.Django==1.9.8.Python2.7 这里的Django后台使用了ORM(Object Relational Mapping),全称对象关 ...
- Hive的安装搭建(三)
03 Hive的安装搭建 Hive可以从源码中编译安装,也可以直接使用官网下载的安装包,在此处我们选择安装包解压安装的方式. Hive中最最重要的角色就是metastore 因此按照metastore ...
- linux 生成密钥和公钥,实现免密登录
1. 在相应的用户根目录下生成密钥公钥,输入如下命令: ssh-keygen -t rsa 2. 直接三次回车:会生成两个文件:id_rsa / id_rsa.pub,分别为密钥和公钥 3. 打开公 ...
- 测试工具Telerik Test Studio发布R2 2019|支持VS 2019
Telerik Test Studio是一个用于功能性Web.桌面和移动测试的直观测试自动化工具,它能轻松地实现自动化测试.同时会为GUI.性能.加载和API测试提供完整的自动化测试解决方案. |更多 ...
- react axios 跨域问题
周末又是补充知识点的时候了,用了react axios 跨越问题,貌似是要比vue 稍微麻烦一点 它请求http好像是没有问题的,但是https还是有跨域问题的, 我用的刚好是create-react ...
- windows窗口启动redis
必须先得配置好环境变量,才能在窗口开启 启动服务端:redis-server 启动客户端:redis-cli
- 【leetcode】1227. Airplane Seat Assignment Probability
题目如下: n passengers board an airplane with exactly n seats. The first passenger has lost the ticket a ...