Effective Java读书笔记十四(Java Tips.Day.14)
来源:互联网 发布:linux 查看硬盘使用率 编辑:程序博客网 时间:2024/05/23 18:42
TIP 38 检查参数的有效性
这绝对是任何程序员都应该注意的地方。因为你编写的程序,可能会随时接收到各种千奇古怪的参数。
如果不做参数检查,可能导致以下后果:
- 崩掉—方法运行失败,异常退出。
- 不崩掉—但这是更坏的结果,因为程序看起来运行正常,但是会产生错误的结果。
- 不崩掉—最糟糕的情况,方法可以正常返回,但可能使某个对象状态异常,然后在后续某个不确定的时刻,爆炸,然后你根本想不到问题会出在那个该死未检查的参数。
对public方法,以及构造器方法,可以使用Javadoc的@throw标签在文档中说明违反参数限制时会抛出的异常:
/** * @return this mod m * @throws new 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 //... }
对未被导出的方法(通常是private方法),通常应该使用断言:
private static void sort(long a[],int offset, int length){ assert a != null; assert offset >= 0 && offset <= a.length; assert length >=0 && length <= a.length - offset; //do the computation ... }
这个方法对数组a中从offset到length-1位置的元素排序,但在此之前,要对数组a以及索引offset和数组长度length这三个参数做必要的检查。
这些断言是在声称这些条件必将为真,无论客户端会怎样调用这个方法。但不同于一般的检查,如果断言失败,将会抛出断言错误(AssertionError)。
如果这些断言没有起到作用,也不会有开销。但如果给Java解释器设置-ea或-enableassertions标记来启用它们,就会产生额外的开销。
有些特殊的情况,可以不对参数做检测。比如有时参数检查工作开销很大,或根本不切实际,而且有效性检查已隐含在计算过程中完成。
例如,考虑一个为对象列表排序的方法,Collections.sort(List),列表中的所有对象都必须是可以互相比较的。如果有对象不能互相比较,排序的某个操作就会抛出ClassCastException。这正式sort方法该做的事情,所以没有必要在排序前检查参数列表中元素是否可以相互比较。
有得必有失,如果不加选择的使用这种方法,将会导致失去失败原子性(failure atomicity)(见TIP 64)。
有时,某些计算会隐式地执行参数检查,但是如果检查不成功,就会抛出错误的异常,换句话说,由于无效的参数值而导致计算抛出的异常,与文档中标明这个方法的异常并不相符。在这种情况下,应当使用异常转译技术,将计算过程中抛出的异常转换为正确的异常。
总之,每当编写方法或者构造器的时候,就应当考虑它的参数有哪些限制,而且应当把这些限制写到文档中,并且在方法体的开头就通过显示的检查来实施这些限制。
TIP 39 必要时进行保护性拷贝
Java被认为是安全的语言,因为它对缓冲区溢出、数组越界、以及其它的内存破坏错误都自动免疫,而这些问题常常出现在诸如C/C++这样的语言中。
在一门安全语言中,在设计类的时候,程序员应当明确的知道任何导致对象状态变化的方法。例如一个类的私有field,如果不设计set方法,那么程序员就可以非常确定,在这个类之外的任何地方,field的值都不会发生变化。(反射机制暂且不理,它不属于常规编程的范畴)
然而,即使在安全语言中,程序员仍然要采取一些措施,来保证类的实例状态不会发生意外的改变。
换言之,编写一些面对客户端的不良行为时仍能保持健壮的类,是非常值得的。
看起来下面这个类是不可变的。它用于表示一个固定的时间段:
public class Period { private final Date start; private final Date end; /** * * @param start 开始时间 * @param end 结束时间 * @throws IllegalArgumentException 如果开始时间在结束时间之后 * @throws NullPointerException 如果start或end参数为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; }}
而且这个构造器方法来增加了参数检查:开始日期不应该在结束日期之后。
然而,由于Date类本身是可变的,因此很容易违反这个条件:
Date start = new Date(); Date end = new Date(); Period p = new Period(start,end); end.setYear(88); //p的内部状态被修改了
那么,怎么才能避免这种情况发生呢?
且看修改后的构造器方法:
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); } }
如果有人还想用之前的方法来改变Period实例的状态,是一定会失败的。因为p.end的实例,与传入构造器的end实例不是同一个了。所以无论怎样改变end的状态,与p.end的状态半毛钱关系都没有。
这就是保护性拷贝的作用。
请注意参数的有效性检查移到了保护性拷贝操作的后面,这是非常有必要的。在多线程并发的环境中,如果参数检查仍然放在保护性拷贝操作之前,有可能会在另一个线程中改变构造方法的参数,如果这个改变发生在危险阶段,即检查参数开始到拷贝参数之间的时间段,那么很可能导致参数检查失败,甚至更严重的、难以捕捉的错误。
同时也要注意,我们没有用到Date的clone方法来进行保护性拷贝。因为Date是非final类,不能保证clone方法一定会返回Date的对象,有可能返回一个Date子类的对象。如果这个子类将每个创建的实例加入到一个私有的静态列表中,然后允许攻击者访问这个列表,那么攻击者就可以自由地控制所有的实例。
(看到这里,豆爷才明白以往写的代码都是漏洞百出,丝毫没有考虑到安全性的问题,甚至连安全的意识都没有。)
为了阻止这种攻击,对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法来进行保护性拷贝。
然而,由于start()和end()方法直接提供了对其可改变内部成员的访问能力:
Date start = new Date(); Date end = new Date(); Period p = new Period(start,end); p.end.setYear(88);//p的内部状态仍然被改变了...
所以,start()和end()不能直接返回start和end:
public Date start(){ return new Date(start.getTime()); } public Date end(){ return new Date(end.getTime()); }
这样做就对了,Period真正是不可变的了。无论程序员怎么折腾,都绝对不会违反”开始时间在结束时间之后”这个约束条件了。
不过有一点,访问方法与构造方法不同,可以使用clone来做保护性拷贝。原因很简单,在访问方法中,我们清楚地知道,Period内部的start和end的类型是Date,而不可能是其他某个不可信子类。请参考TIP 11,一般情况下,如果无法确定实例的来源类型,就使用构造器或静态工厂来做保护性拷贝。
不仅仅是针对不可变类。
每当编写方法或构造器时,如果允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否是可变的。
如果是可变的,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。如果你不允许对象进入之后还会发生变化,就必须对该对象进行保护性拷贝。
当然,如果对象是不可变的,就没有必要做这些事情了。
同理,如果要将一个内部对象返回给客户,那么对它们进行保护性拷贝也是有必要的。不管类是否为不可变的,在把一个指向内部可变组件的引用返回给客户端之前,也应该加倍认真考虑。
解决方案就是保护性拷贝。或者,如果要返回一个数组或容器,可以提供一个不可变的版本来返回。比如需要返回一个List时,可以返回一个不可变的版本:Collections.unmodifiableList(list)
。
总之,上述的真正启示在于,只要有可能,都应该使用不可变的对象作为对象内部的组件,这样就不必要为保护性拷贝操心。
否则,如果必须要使用可变的对象作为内部的组件,类就必须保护性拷贝这些组件,在构造方法、get和set方法中都要做这件工作。
除非,拷贝的成本/开销受到限制,并且类信任它的客户端不会瞎搞胡来,就可以在文档中指明客户端不得修改收到影响的组件,以此来代替保护性拷贝。
- Effective Java读书笔记十四(Java Tips.Day.14)
- Effective Java读书笔记一(Java Tips.Day.1)
- Effective Java读书笔记二(Java Tips.Day.2)
- Effective Java读书笔记五(Java Tips.Day.5)
- Effective Java读书笔记六(Java Tips.Day.6)
- Effective Java读书笔记八(Java Tips.Day.8)
- Effective Java读书笔记九(Java Tips.Day.9)
- Effective Java读书笔记十(Java Tips.Day.10)
- Effective Java读书笔记十一(Java Tips.Day.11)
- Effective Java读书笔记十二(Java Tips.Day.12)
- Effective Java读书笔记十三(Java Tips.Day.13)
- Effective Java读书笔记十五(Java Tips.Day.15)
- Effective Java读书笔记十六(Java Tips.Day.16)
- Effective Java读书笔记十七(Java Tips.Day.17)
- Effective Java读书笔记十九(Java Tips.Day.19)
- Effective Java读书笔记二十(Java Tips.Day.20)
- Effective Java读书笔记二一(Java Tips.Day.21)
- Effective Java读书笔记二二(Java Tips.Day.22)
- Awesome SAR
- gitlab集成ldap配置
- Select查询语句详解(MySQL)
- java的方法锁、对象锁以及类锁的区别
- 计算机中的存储单位,bit ,Byte, KB, MB, GB,TB...
- Effective Java读书笔记十四(Java Tips.Day.14)
- android基于zxing实现扫描中对扫描线条的更改
- SpringMVC-框架介绍
- 初探java虚拟机
- Color POJ
- 关于开机出现“安装程序正在为首次使用计算机做准备”的解决方案及微软OOBE与SYSPREP的实用技巧
- UART遇到两个与 stty 相关问题
- 单链表自动生成函数
- Geode多节点集群实验