最近在看《Effective Java》这本书,顺便就记录一些笔记,记录一下书中的一些知识点以及对知识点的总结。一般情况会记录所有的知识点,但是知识点太过简单或者无归纳点总结的就不做详细记录。本片计划写第七、八、九章
七、方法
38、检查参数的有效性
非公有方法通常应该使用断言(assertion)来检查他们的参数,具体做法:
private void dosomething(long a[],int start,int end){
assert a != null;
assert start > 0 && end <a.lenght;
}
和一般的有效性检查相比,断言失败,将会抛出AssertionError;如果没有起到作用也不会有成本开销。
一些参数,方法本身没有用到,但是却被保存起来供以后使用,这样的情况下,参数的有效性尤为重要。例如一些构造函数。
总之,编写方法或者构造器的时候,应该考虑它的参数有哪些限制,应该将这些限制写到文档中,并且在方法的开始处,通过显示的检查。
39、必要时进行保护性拷贝
一个类中含有一个可变对象域,如果其他类在使用该类的对象时,获取到可变对象域的对象,然后恶意修改其数据,那么这样的类是不安全的,需要进行保护性拷贝。
public final class Period {
private final Date start;
private final Date end;
public Priod(Date start,Date end) {
if(start.compareTo(end)>0 {
throw new IllegalArgumentException(start +"after" +end);
}
this.start = start;
this.end = end;
}
pulbic Date start(){
return start;
}
public Date end() {
return end;
}
}
上例中的类Peroid就是不安全的,如果其他类如下面的操作,就会破坏安全性:
Data start = new Date();
Data end = new Date();
Peroid p = new Peroid(start,end);
//……
start.setYear(2016);//危险操作1
Date mstart = p.start();
start.setYear(2022);//危险操作2
这样的操作之后start和end的前后比较关系就会遭到破坏,这样就不安全了,所以需要保护性拷贝。修改如下:
public final class Period {
private final Date start;
private final Date end;
public Priod(Date start,Date end) {
this.start = new Date(start);
this.end = new Date(end);//这样可以避免上例中的**危险操作1**
if(start.compareTo(end)>0 {
throw new IllegalArgumentException(start +"after" +end);
}
}
pulbic Date start(){
return new Date(start);//这样可以避免上例中的**危险操作2**
}
public Date end() {
return new Date(end);
}
}
保护性拷贝可能会带了相关的性能损失,这种说法并不总正确,。如果类信任他的调用者不会修改其内部的组件,可能因为类及调用者类属于同一包内,那么不进行保护性拷贝也是可以的,但是文档中还是要必须清楚的说明,调用者绝不能修改会受影响的参数。
总之,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性的拷贝这些组件。如果拷贝的成本比较大,而且信任它的调用者不会不恰当的修改组件,那么也可以不进行保护性拷贝,但需要在文档中说明。
40、谨慎设计方法签名
- 谨慎选择方法名称,方法命名应该遵循标准的命名习惯:1、应该易于理解,且与同一个包中的其他名称风格一致;2、应该选择与大众认可的名称相一致。
- 不要过于追求提供便利的方法:方法应该尽其所能,方法太多会使类难以学习、使用、文档化、测试和维护。
- 避免过长的参数列表,目标是4个,或者更少。但是个人觉得,核心api层不向外暴露的方法,还是可以更多的参数。减少参数列表的方法:1、方法拆分成多个方法;2、创建辅助类,用来保存参数;3、从对象构建到方法调用都采用Builder模式。
- 对参数类型,要优先使用接口,而不是具体类。eg:要定义一个HashMap对象,那么定义该对象的时候应该使用Map来定义它的类型,Map map = new Hashmap();
- 对于boolean参数,要优先使用两个元素的枚举类型,使代码更易于阅读和编写。
41、慎用重载
先举个例子:
public class CollectionClassifier{
public static String classify(Set<?> s){
return "Set";
}
public static String classify(List<?> l){
return "List";
}
public static String classify(Collection<?> c){
return "Unknow Collection";
}
}
该例中的3个重载方法,是不正确的,重载方法是静态的,但是程序执行的时候选择方法是动态的,该例中程序执行的时候会调用第三个方法(Collection),会得到不期望的结果,从而引起程序错误。而安全而保守的策略是永远导出两个具有相同参数数目的重载方法。可以将方法变形,变成不同的方法签名,比如:ObjectOutputStream,具有writeBoolean、writeLong、writeInt等方法。
JDK1.5之后,基本类型有了引用类型之后,自动装箱会引起一些问题,例如:
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.print(set+","+list);
该程序期望的是删除集合中的(0、1、2),然后输出[-3,-2,-1],[-3,-2,-1],但是实际上,打印结果是[-3,-2,-1],[-2,0,2],这是我们不想要结果。
查看API可知,Set中remove只有一个方法boolean remove(Object o)方法,所以set.remove(i)的时候,会自动将i装箱为Integer,然后删除掉,所以set的打印结果是正确的;而根据API可知,List中的remove方法有public void remove(int position)方法和public void remove(String item)方法,两个方法是重载方法,这时候,list.remove(i),会根据传入类型来判断调用哪个函数,而传入的参数是int型的i,所以就会去调用public void remove(int position)方法,这个方法根据index来删除元素,所以就会隔一个删除一个。
为了解决这种问题,我们可以在list.remove的时候将i手动装箱:list.remove((Integer)i);或者list.remove(Integer.valueof(i));
所以在对于基本类型的自动装箱和泛型,要谨慎的处理。
java中有一些类在版本更新的过程中不可避免的出现了违背这些原则的情况,例如Jdk1.5中增加了一个接口CharSequence,用来作为StringBuffer、StringBuilder、String、CharBuffer等类的公共接口,String也出现了contentEquals的重载方法,这违背了本条目的原则,但是这两个重载的方法执行相同的功能,而且执行结果是一样的,这样就不会带来危险和错误。
所以,我们要谨慎的重载方法;而在不可避免的需要重载方法的时候,就要做到重载方法行为一致,参数类型谨慎处理。
42、慎用可变参数
可变参数方法,需要检测数组是否为空和数组长度。解决方法是方法参数为2个,第一个参数为原来的可变参数数组中的第一个元素,第二个参数是原来的可变参数数组中的其他元素。eg:
static int min(int firstArg,int... remainingArgs){
int min = firstArg;
for( int arg :remainingArgs){
if(arg<min){
min = arg;
}
}
return min;
}
可变参数方法不可过多滥用。
43、返回0长度的数组或集合,避免返回null
返回0长度的数组或集合可以让代码逻辑中少了一些判断,jdk中Collections提供了emptySet、emptyList、emptyMap,都是static、final的。
44、为所有导出的API元素编写文档注释
编写文档,必须在每个被导出的类、接口、构造器、方法、和域声明之前增加一个文档注释,描述、约定、注意事项、线程安全性、可序列化性等。
八、通用程序设计
45、将局部变量的作用最小化
java允许你在任何可以出现语句的地方声明变量,要使局部变量的作用最小化,最有力的方法就是在第一次使用它的地方声明。如果在循环终止不再需要循环变量的内容,在for循环和while循环中优先选择for循环。这样可以避免一些复制-粘贴的错误,例如:
Iterator<Element> i = c.iterator();
while(i.hasNext()){
dosomething(i.next());
}
Iterator<Element> i2 = c.iterator();
while(i.hasNext()){//这里由于复制粘贴而忘记修改循环变量,造成错误
dosomething(i2.next());
}
如果使用for循环,这样的错误就可以避免。
另外for循环还有另外一个优势:更简短。
for(int i=0;n=somesize();i<n;i++){
dosomething(i);
}
最后一种将局部变量的作用域最小化的方法是使方法小而集中。
46、for-each循环优于传统的for循环
java1.5之后引入了一种for循环,增强for循环:for-each,万千隐藏迭代器或者索引变量,避免了混乱和出错的可能。
但是有三种情况不能使用for-each循环:
- 过滤——如果需要遍历集合并删除特定的元素,就需要使用显示的迭代器,以便可以调用它的remove方法
- 转换——如果需要遍历列表或者数组,并取代它部分或者全部的元素,就需要列表迭代器或者数组索引,以便设定元素的值
- 平行迭代——如果需要并行的遍历多个集合,就需要显示的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。
一定要避免上述3中情况。
47、了解和使用类库
要熟练使用标准类库;不要重复发明轮子,可以使用一些优秀的开源类库。
48、如果需要精确的答案,请避免使用float和double
float和double主要是为了科学计算和工程计算而设计的,它们执行二进制浮点运算,为了在广泛的数值范围上提供较为精确的快速近似计算而设计的,但是没有提供完全精确的计算结果。float和double类型尤其不适合用于货币和计算,可以使用BigDecimal、int或者long进行货币计算。使用BigDecimal有2个缺点:与使用基本类型相比比较麻烦,且比较慢。
49、基本类型优先于装箱基本类型
java1.5中增加了自动装箱和自动拆箱。
基本类型和装箱基本类型之间的三个主要区别:第一,基本类型只有值,而装箱基本类型具有与它们的值不同的统一性;第二,基本类型只有功能完备的值,而装箱类型除了对应基本类型的值之外,还有个非功能值,null;第三,基本类型通常比装箱基本类型更节省时间和空间。
基本类型的比较可以直接使用比较运算符,但是装箱类型的比较不能直接通过比较运算符。
当程序进行设计装箱和拆箱基本类型的混合计算时,它会进行拆箱,当程序进行拆箱时,会抛出NullPointerException异常。
装箱类型会造成不必要的开销,可以参照第5条。
50、如果其他类型更适合,择尽量避免使用字符串
字符串不适合代替其他的值类型。
字符串不适合代替枚举类型。
字符串不适合代替聚集类型。
字符串不适合代替能力表。一些功能的授权,使用字符串容易出现不安全的访问,最好是提供一个不可伪造的key来控制。
51、当心字符串连接的性能
字符串的连接,普通的做法就是使用字符连接符“+”,但是String是final的,对于这种连接是比较耗时和浪费开销的。可以使用StringBuilder代替String。
52、通过接口引用对象
应该优先使用接口而不是用类作为参数的类型,例如List list = new Vector();
但是如果原来的实现提供了某种特殊的功能,而这种功能又不是接口中的方法,那么新的实现也要提供通用的功能。
53、接口优先于反射机制
核心反射机制java.lang.reflect,提供了通过程序来访问关于已装载的类的信息的能力。
这种能力需要付出一些代价:
- 丧失了编译时类型检查的好处,包括异常检查。
- 执行反射访问所需要的代码非常的笨拙和冗长。
- 性能损失,反射方法的调用比普通方法调用慢了非常多。
54、谨慎的使用本地方法
Java Native Interface(JNI)允许java应用程序可以调用本地方法,本地方法是指本地程序设计语言(C或者C++)。
历史上来看,本地方法主要有中用途:提供了特定于平台的机制的能力,比如访问访问注册表,文件锁;还提供了访问遗留代码库的能力,从而可以访问遗留数据;通过本地语言,编写应用程序中可以提供一些性能上的提升。
55、谨慎地进行优化
不要因为性能而牺牲合理的结构。要努力编写好的程序而不是快的程序。在设计系统的时候,特别是在设计API、线路层协议和永久数据格式的时候,一定要考虑性能的因素。构建完系统之后,要测试它的性能;如果不够快,可以在性能剖析器的帮助下查找原因,重要的检查所使用的算法,算法不当,再多的优化也无济于事。
56、遵守普通接受的命名习惯
好的命名习惯使代码看起来直接明了,也使得代码维护起来减少一些麻烦。
九、异常
57、只针对异常的情况才使用异常
异常应该只用于异常的情况下;它们永远不应该用于正常的控制流而使用异常。
58、对可恢复的情况使用受检异常,对编程错误使用运行时异常
java提供了三种可抛出的结构(throwable):受检的异常(checked exception)、运行时异常(run-time exception)和错误(error)。
决定使用受检异常或是为受检的异常时的原则是:如果期望调用者能够适当的恢复,对于这种情况应该使用受检的异常。通过受检的异常,强迫调用者在一个catch中处理该异常或者将它传递出去。
运行时异常大多数是前提违例。例如ArrayIndexOutBoundsExctption表示数组访问的约定(数组的下标必须在0到数组的长度减一之间)被违反了。
实现的所有的未受检的抛出结构都应该是RuntimeException的子类(直接的或者间接的)。
异常也是有完全意义的对象,我们需要懂得如何解析异常。
受检的异常往往指明了可恢复的条件,对于这样的异常,可以通过一些方法帮助调用者恢复信息。例如,用户没有存储够足够的钱而想消费,这时候抛出一个受检的异常,异常中可提供一个访问方法,以便用户查询所缺的费用。
59、避免不必要的使用受检的异常
60、优先使用标准的异常
重用现有的异常有很多好处,最主要的是,它使你的API更易于学习和使用,因为它与程序员已经熟悉的习惯用法是一致的。第二个好处是对于用到这些API的程序而言,他们的可读性会更好,因为不会出现程序员不熟悉的异常。最后,异常类越少,意味着内存印记就越小,装载这些类的时间和开销就越小。
常用的异常:
异常 | 使用场合 |
---|---|
IllegalArgumentException | 非null的参数不正确 |
IllegalStateException | 对于方法调用者而言,对象状态不合适 |
NullPointerException | 在禁止使用Null的情况下参数值为Null |
IndexOutOfBoundsException | 下标参数值越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,检测到对象的并发修改 |
UnsupportOperationException | 对象不支持用户请求的方法 |
61、抛出与抽象相对应的异常
如果方法抛出的一场与它所执行的任务没有明显的联系,这种情况会令人不解。为了避免这种情况,更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常,这种做法成为异常转译。如下所示:
try{
//……
}catch(LowerLevelException e){
throw new HigherLevelException();
}
一个具体的例子:
public E get(int index){
ListInterator<E> i = listerator(index);
try{
return i.next();
}catch(NoSuchElementException){
throw new IndexOutOfBoundsException("Index:"+Index);
}
}
62、每个方法抛出的异常都要有文档
要为你编写的每个方法抛出的每个异常都建立文档,要详细的说明该异常对应的情况和注意事项。
63、在细节消息中包含能捕获失败的信息
为了捕获异常,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。
64、努力使失败保持原子性
作为方法规范的一部分,产生的任何异常都应该让对象保持在该方法调用之前的状态,如果违反了规定,API文档中应该清楚的说明该对象将会处于什么样的状态。
65、不要忽略异常
避免使用空的catch块,至少catch也应该包含一条说明,解释为什么可以忽略这个异常。
PS
这次写到了第九章,后面还有两章,所以还需要再来一篇博客才能完成。