Effective Java学习笔记(三)

[size=large][b][color=blue]第三章:对于所有对象都通用的方法(条目8-12)[/color][/b][/size]
Object类的设计是为了扩展,它的所有非final方法(equals、hashCode,toString,clone和finalize)都具有明确的通用约定(general contract),因为它们被设计成是override的。任何一个类在覆盖这些方法时,都有责任准守这些通用约定,如果不能做到这点,其他依赖于这些约定的类(例如HashMap和HashSet)就无法结合该类一起正常运作。
[size=medium][b][color=red]第8条:覆盖equals时请准守通用约定[/color][/b][/size]
以下4种情况,类的每个实例只和自身相等:
1.类的每个实例本质上都是唯一的。对于代表活动实体而不是值(value)的类来说确实如此,比如Thread。
2.不关心类是否提供了“逻辑相等(logical equality)”的测试功能。
3.超类已经覆盖了equals,从超类继承过来的行为对子类也是适合的。比如多数Set实现都从AbstractSet继承equals实现。
4.类是私有的或者是包级私有的,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals方法的,以防止它被意外调用:

public boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}

那么,什么时候应该覆盖Object.equals呢?如果类具有自己特有的“逻辑相等”概念(不同于对象相等的概念),而且超类的equals也不满足要求的时候,就需要覆盖equals方法了。这通常属于“值类(value class)”的情形。值类仅仅是一个表示值的类,例如Integer或者Date。程序员在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。
实现高质量equals方法的诀窍:
1.使用==操作符检查“参数是否为这个对象的引用”。
2.使用instanceof操作符检查“参数是否为正确的类型“。
3.把参数转化为正确的类型。(转换之前已通过instanceof测试,所以确保会成功)
4.对于类中的每个“关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。
5.编写完equals方法后,应该问自己三个问题:它是否满足对称、传递和一致的特性?建议编写单元测试来检验这些特性(自反和非空特性会自动满足)。请看下面Point类的equals方法:

public class Point {
private final int x;
private final int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Point)) {
return false;
}
Point p = (Point) o;
return x == p.x && y == p.y;
}
}

下面还有编写equals的最后一些告诫:
1.覆盖equals时总要覆盖hashCode;
2.不要企图让equals方法过于智能;
3.不要将equals声明中的Object对象替换为其他类型。

[b][color=red][size=medium]第9条:覆盖equals时总要覆盖hashCode[/size][/color][/b]
在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常工作,这样的集合包括HashMap、HashSet和Hashtable。下面是hashCode实现的例子:

/**
* 电话号码
* @author chasegalaxy
* @since 2012-08-04
*/
public final class PhoneNumber {
// Lazily initialized, cached hashCode
private volatile int hashCode;

private final String areaCode; // 区号
private final short landLineNumber; // 座机号
private final short extensionLineNumber; // 分机号

public PhoneNumber(String areaCode, int landLineNumber, int lineNumber) {
this.areaCode = areaCode;
this.landLineNumber = (short) landLineNumber;
this.extensionLineNumber = (short) lineNumber;
}

@Override
public boolean equals(Object o) {
// 引用相同,即同个对象
if (o == this) {
return true;
}
if (!(o instanceof PhoneNumber)) {
return false;
}
PhoneNumber pn = (PhoneNumber) o;
return pn.areaCode == areaCode
&& pn.landLineNumber == landLineNumber
&& pn.extensionLineNumber == extensionLineNumber;
}

/**
* 散列函数:为不相等的对象产生不相等的散列码
*/
@Override
public int hashCode() {
System.out.println("invoke hashCode...");
int result = hashCode;
if (result == 0) {
System.out.println("calclulate hash code...");
result = 17;
result = 31 * result + areaCode.hashCode();
result = 31 * result + landLineNumber;
result = 31 * result + extensionLineNumber;
hashCode = result;
}
return result;
}
}

这里使用了“延迟初始化(lizily initialize)”散列码的方式,测试代码(两次使用对象,但散列码的计算只进行了一次):

Map<PhoneNumber, String> map1 = new HashMap<PhoneNumber, String>();
Map<PhoneNumber, String> map2 = new HashMap<PhoneNumber, String>();
PhoneNumber pn = new PhoneNumber("0571", 88576543, 8934);
map1.put(pn, "chasegalaxy");
map2.put(pn, "zhangxiaosan");

// 执行结果:
invoke...
calculate...
invoke...

下面是散列码编写的步骤:
1.把某非零常数值,比如17,保存在int变量中;
2.对于对象中的每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
[list]
[*]boolean类型,则计算 f ? 1 : 0
[*]byte/char/short/int类型,则计算 (int)f
[*]long类型,则计算 (int)(f^(f>>>32))
[*]float类型,则计算 Float.floatToIntBits(f)
[*]double类型,则计算 Double.doubleToLongBits(f),然后再按long处理
[*]如果是对象引用,则取其hashCode,如果需更复杂的比较,则可通过为这个域计算一个“范式(canonical representation),然后针对这个范式调用hash。如果这个域的值为null,则返回某个常数(通常是0)
[*]如果是数组,要把每个元素当做单独的域来处理
[/list]
3.按2计算result = 31 * result + c,并返回;
4.问问自己“相等的实例是否都具有相等的散列码”,请编写单元测试来验证。
不要试图从散列码计算中排出掉一个对象的关键部分来提高性能,虽然这样得到的散列函数运行起来可能更快,但是它的效果不见得会好,可能会导致散列表慢到根本无法使用。
Java平台类库中的许多类,比如Integer和Date,它们的散列码计算时取的是它们的确切值:

public int hashCode() {
return value;
}

public int hashCode() {
long ht = this.getTime();
return (int) ht ^ (int) (ht >> 32);
}

上面这种实现并不是个好主意,因为这样做严格限制了在将来的版本中改进散列函数的能力,最好采用上面PhoneNumber类的编写方式。

[size=medium][b][color=red]第10条:始终要覆盖toString[/color][/b][/size]
Object提供了toString的一个实现,但它返回的字符串通常不是用户所期望看到的。它包含类的全名+@+散列码的无符号十六进制。toString的通用约定指出,被返回的字符串是一个“简洁的,但信息丰富,并且易于阅读的表达形式”。
虽然遵守toString的约定并不像遵守equals和hashCode的约定那么重要,但是提供好的toString实现可以使类用起来更加舒适。比如建议在PhoneNumber类中加入下面的方法:

/**
* Returns the string representation of this phone number.
* Thr string consists of 20 characters whose format
* is [XXXX-YYYYYYYY-ZZZZ].
*/
public String toString() {
return String.format("[%s-%08d-%04d]",
areaCode, landLineNumber, extensionLineNumber);
}


[size=medium][color=red][b]第11条:谨慎地覆盖clone[/b][/color][/size]
Cloneable接口的目的是作为对象的一个mixin接口(mixin interface),表明这样的对象允许克隆,遗憾的是,它缺少一个clone方法。既然Cloneable没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,那么Object的clone就返回对该类对象的逐域拷贝,否则就会抛出CloneNotSupportException。
Clone方法的通用约定如下:创建和返回对该对象的一个拷贝,这个约定是非常弱的,拷贝的精确含义取决于该对象的类。下面是对PhoneNumber实现的一个clone方法:

// 必须使PhoneNumber这个类实现Cloneable接口,否则调动clone方法时会抛出AssertionError
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException ex) {
throw new AssertionError(); // Can't happen
}
}

上面代码中返回PhoneNumber,而不是Object体现了一条通用原则:永远不要让客户去做任何类库能够替客户端完成的事情。
简而言之,所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此公有方法首先调用super.clone,然后修正任何需要修正的域。一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用替代原来指向这些对象的引用。虽然,这些内部拷贝操作往往可以通过递归调用clone完成,但这通常不是最佳方法。
真的有必要这么复杂吗?很少有这种必要。但是如果你扩展了一个实现了Cloneable接口的类,那么你除了实现一个行为良好的clone方法外,没有别的选择。否则,最好提供某些其他的途径来代替对象拷贝,或者干脆不提供这样的功能。
另一个实现对象拷贝的好办法是提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)。比如:

// 拷贝构造器
public Yum(Yum yum) {
// ...
}

// 拷贝工厂,是类似于拷贝构造器的静态工厂
public static Yum newInstance(Yum yum) {
// ...
return null;
}

拷贝构造器和静态工厂的变形,都比Cloneable/clone方式具有更多的优势:它们不依赖于某一种很有风险的、语言之外的对象创建机制,不会和final域的正常使用发生冲突,不会抛出不必要的checked异常,不需要进行类型转换。
更进一步,拷贝构造器或拷贝工厂可以带一个参数,参数类型是通过该类实现的接口(这里的拷贝构造器和拷贝工厂可以称之为“转换构造器”和“转换工厂”)。比如:假设你有一个HashSet,并且希望把它拷贝成一个TreeSet,clone方式无法提供这样的功能,但如果用转换构造器很容易实现。
既然Cloneable有上述那么多问题,所以不建议使用Cloneable这个接口。有些专家级的程序员干脆从来不去覆盖clone方法,也从来不去调用它,除非拷贝数组。你必须清楚一点,对于一个专门为了继承而设计的类,如果你未能提供行为良好地受保护的(protected)clone方法,它的子类就不可能实现Cloneable接口。

[color=red][size=medium][b]第12条:考虑实现Comparable接口[/b][/size][/color]
compareTo方法并没有在Object中声明,它是Comparable接口中唯一的方法。compareTo方法不但允许进行简单的同等比较,而且允许执行顺序比较。类实现了Comparable接口,就表明它的实例具有内在的排序关系(natural ordering)。为实现Comparable接口的对象数组排序就这么简单:

Arrays.sort(arr);

Java类库中的值类(value class)都实现了Comparable接口。如果你正在编写一个值类,它具有非常明显的内在排序关系,比如按字母排序、按数值排序或者按年代排序,那你就应该坚决考虑实现这个接口。
就好像违反了hashCode约定的类会破坏其他依赖于散列做法的类一样,违反compareTo的类也会破坏其他依赖于比较关系的类。依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Arrays和Collections,它们内部包含有搜索和排序算法。
compareTo方法的通用约定与equals方法相似。将某对象与指定的对象进行比较,当该对象小于、等于和大于指定对象时,分别返回负整数、零和正整数。如果指定对象无法与该对象进行比较,则抛出ClassCastException。但和equals有所区别,比如BigDecimal的例子:

BigDecimal decimal1 = new BigDecimal("1.0");
BigDecimal decimal2 = new BigDecimal("1.00");
System.out.println(decimal1.equals(decimal2));
System.out.println(decimal1.compareTo(decimal2));
// the result is: false / 0

下面给出PhoneNumber的compareTo方法:

@Override
public int compareTo(PhoneNumber pn) {
// Compare areaCode
int areaCompareToResult = areaCode.compareTo(pn.areaCode);
if (areaCompareToResult != 0) {
return areaCompareToResult;
}
// AreaCode are equal, compare land line number
int landLineNumberDiff = landLineNumber - pn.landLineNumber;
if (landLineNumberDiff != 0) {
return landLineNumberDiff;
}
// AreaCode and LandLineNumber are equal, compare extensionLineNumber
return extensionLineNumber - pn.extensionLineNumber;
}

测试代码及结果:

PhoneNumber pn1 = new PhoneNumber("0571", 88007700, 1234);
PhoneNumber pn2 = new PhoneNumber("0580", 88007700, 1234);
PhoneNumber pn3 = new PhoneNumber("0571", 88007701, 1234);
PhoneNumber pn4 = new PhoneNumber("0571", 88007700, 1235);
System.out.println(pn1.compareTo(pn2) + "," + pn2.compareTo(pn1));
System.out.println(pn1.compareTo(pn3) + "," + pn3.compareTo(pn1));
System.out.println(pn1.compareTo(pn4) + "," + pn4.compareTo(pn1));
// the result is:
// -1,1
// -1,1
// -1,1

上面结果是预期的,正确,所以PhoneNumber可用Collections直接排序(默认升序ASC)。[b][color=blue](第三章完)[/color][/b]

[b][color=red]文章本人原创,转载请注明作者和出处,谢谢。[/color][/b]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值