Effective Java读书笔记-4
try-with-resources优先于try-finally
Java类库中包括许多必须通过调用close方法来手工关闭的资源。例如InputStream、OutputStream和java.sq1.connection。客户端经常会忽略资源的关闭,对性能造成严重影响。try-finally语句是确保资源会被适时关闭的最佳方法,就算发生异常或者返回也一样:
// try-finally-No longer the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
如果再添加第二个资源,就会导致代码很混乱:
// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
} finally{
out.close();
} finally{
in.close();
}
}
}
}
即便用try-finally语句正确地关闭了资源,如前两段代码范例所示,它也存在着些许不足。因为在try块和finally块中的代码,都会抛出异常。例如,在firstLineOfFile方法中,如果底层的物理设备异常,那么调用readLine就会抛出异常,基于同样的原因,调用close也会出现异常。在这种情况下,第二个异常完全抹除了第一个异常。在异常堆栈轨迹中,完全没有关于第一个异常的记录,导致系统调试变得非常复杂,因为通常需要看到第一个异常才能诊断出问题的原因。
Java7引人的try-with-resources语句,使这些问题一下子全部解决了。要使用这个构造的资源,必须先实现AutoCloseable接口,其中包含了单个返回void 的close方法。 Java类库与第三方类库中的许多类和接口,现在都实现或扩展了AutoCloseable接口。如果编写了一个类,它代表的是必须被关闭的资源,那么这个类也应该实现AutoCloseable。
以下就是使用try-with-resources的第一个范例:
// try-with-resources-the the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
以下是使用try-with-resources的第二个范例:
// try-with-resources on multiple resources-short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
}
}
使用try-with-resources不仅使代码变得更简洁易懂,也更容易进行诊断。以firstLineOfFile方法为例,如果调用readLine和(不可见的)close方法都抛出异常,后一个异常就会被禁止,以保留第一个异常。这些被禁止的异常并不是简单地被抛弃了,而是会被打印在堆栈轨迹中,并注明它们是被禁止的异常。通过编程调用getSuppressed方法还可以访问到它们,getsuppressed方法也已经添加在Java7的Throwable中了。
在try-with-resources语句中还可以使用catch子句,就像在平时的try-finally语句中一样。这样既可以处理异常,又不需要再套用一层代码。下面举一个稍费了点心思的范例,这个firstLineOfFile方法没有抛出异常,但是如果它无法打开文件,或者无法从中读取,就会返回一个默认值:
// try-with-resources with a catch clause
static String firstLineOfFile(String path, String defaultVal){
try (BufferedReader br = new BufferedReader(new FileReader(path)) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}
结论很明显:在处理必须关闭的资源时,始终要优先考虑用try-with-resources,而不是用try-finally。 这样得到的代码将更加简洁、清晰,产生的异常也更有价值。
覆盖equals时请遵守通用约定
在覆盖equals方法的时候,必须要遵守它的通用约定。 下面是约定的内容,来自Object的规范。
equals方法实现了等价关系(equivalence relation),其属性如下:
自反性(reflexive):对于任何非null的引用值x,x.equals(x)必须返回true。
对称性(symmetric):对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
传递性(transitive): 对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
一致性(consistent): 对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。
对于任何非null的引用值x,x.equals(null)必须返回false。
一个类的实例通常会被频繁地传递给另一个类的实例。许多类,包括所有的集合类(collection class)在内,都依赖于传递给它们的对象是否遵守了equals约定。
对称性(Symmetry)要求是说,任何两个对象对于“它们是否相等”的问题都必须保持一致。例如下面的类忽视了对称性,它实现了一个区分大小写的字符串。字符串由toString保存,但在equals 操作中被忽略。
//Broken-violatessymmetry!
public class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
// Broken - violates symmetry!
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
// One-way interoperability!
if (o instanceof String) {
return s.equalsIgnoreCase((String) o);
}
return false;
}
// Remainder omitted
}
在这个类中,equals方法图与普通的字符串对象进行互操作。假设我们有一个不区分大小写的字符串和一个普通的字符串:
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s="polish";
cis.equals(s)
会返回true。问题在于,虽然 CaseInsensitiveString 类中的equals方法知道普通的字符串对象,但是,String类中的equals方法却并不知道不区分大小写的字符串。因此,s.equals(cis)
会返回false,违反了对称性。假设把不区分大小写的字符串对象放到一个集合中:
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
此时list.contains(s)
很难确定返回的结果。一旦违反了equals约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎么样。
为了解决这个问题,只需把企图与String互操作的这段代码从equals方法中去掉就可以了。这样做之后,就可以重构该方法,使它变成一条单独的返回语句:
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
}
方法:
1. 使用==操作符检查“参数是否为这个对象的引用”
2. 使用instanceof操作符检查“参数是否为正确的类型”为空。
3. 把参数转换成正确的类型
4. 对于该类中的每个“关键”(significant )域,检查参数中的域是否与该对象中对应的域相匹配。
5. 在编写完equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?
注意:
1. 覆盖equals时总要覆盖hashCode 。
2. 不要企图让equals方法过于智能 。
3. 不要将equals声明中的Object对象替换为其他的类型:这个方法并没有覆盖(override)Object.equals,因为它的参数应该是Object类型,相反,它重载(overload)了Object.equals 。
public class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeChek(areaCode, 999, "area code");
this.prefix = rangeChek(prefix, 999, "pre fix");
this.lineNum = rangeChek(lineNum, 999, "lineNum");
}
private static short rangeChek(int val, int i, String arg) {
if (val < 0 || val > i) {
throw new IllegalArgumentException(arg + ":" + val);
}
return (short) val;
}
/**
* @param o
* @return boolean
* @Description
* 使用==操作符检查“参数是否为这个对象的引用”
* 使用instanceof操作符检查“参数是否为正确的类型”为空。
* 把参数转换成正确的类型
* 对于该类中的每个“关键”(significant )域,检查参数中的域是否与该对象中对应的域相匹配。
**/
@Override
public boolean equals(Object o) {
// 使用==操作符检查“参数是否为这个对象的引用”
if (this == o) {
return true;
}
// 使用instanceof操作符检查“参数是否为正确的类型”为空。测试等同性的同时,测试非空性。
if (!(o instanceof PhoneNumber)) {
return false;
}
// 把参数转换成正确的类型
PhoneNumber that = (PhoneNumber) o;
// 对于该类中的每个“关键”(significant )域,检查参数中的域是否与该对象中对应的域相匹配。
return areaCode == that.areaCode &&
prefix == that.prefix &&
lineNum == that.lineNum;
}
@Override
public int hashCode() {
return Objects.hash(areaCode, prefix, lineNum);
}
}