Effective Java——对所有对象通用的方法

时间:2022-11-21 16:01:58

第8条:覆盖equals时请遵守通用约定

如果不对equals进行覆盖,那么类的每个实例都只与它自身相等。如果类具有自己特有的”逻辑相等”的概念,并且超类还没有覆盖equals以实现期望的行为,这时就可以进行覆盖。

高质量equals方法的诀窍

1、使用==操作符检查参数是否为这个对象的引用

2、使用instanceof操作符检查参数是否为正确的类型

3、把参数转换成正确的类型

4、当编写完成了equals方法之后,应该问自己三个问题,它是否是对称的、传递的、一致的。

例如下面实现的equals:

class PhoneNumber {
private final int areaCode;
private final int prefix;
private final int lineNumber;

public PhoneNumber(int areaCode, int prefix, int lineNumber) {
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = lineNumber;
}

@Override
public boolean equals(Object obj) {
// 使用==操作符检查参数是否为这个对象的引用
if (obj == this) {
return true;
}
// 2、使用instanceof操作符检查参数是否为正确的类型
if (!(obj instanceof PhoneNumber)) {
return false;
}
// 3、把参数转换成正确的类型
PhoneNumber pn = (PhoneNumber)obj;
// 4、应该问自己三个问题,它是否是对称的、传递的、一致的。
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
}

其他需要注意的:

1、覆盖equals时总要覆盖hashCode

2、不要企图让equals方法过于智能

3、不要将equals声明中的Object对象替换成其他的类型。

第9条:覆盖equals时总要覆盖hashCode

在每个覆盖了equals方法的类中,必须覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而也导致该类无法结合所有基于散列的结合一块正常运转,这样的结合包括HashMap、HashSet和Hashtable。

对于上面PhoneNumber的例子,它没有实现hashCode如果我们调用下面代码:

Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>();
map.put(new PhoneNumber(408, 867, 5309), "hello");
map.get(new PhoneNumber(408, 867, 5309));

可以发现上面取到的值为null,而不是”hello”。

原因就是没有重写hashCode方法,每次hashCode返回的都是不同的值。

下面来看看HashMap的源码:

@Override public V put(K key, V value) {

// 只展示关键代码
// 插入的位置索引值会对key取hansh
int hash = Collections.secondaryHash(key);

}

public static int secondaryHash(Object key) {
return secondaryHash(key.hashCode());
}

从上面可以看到传入的是key的hashCode,所以即使是相同的key,如果它的hashCode不同对应的index索引也不同。

如果我执行下面代码:

Map<String, String> map = new HashMap<String, String>();
map.put(new String("aaa"), "hello");
map.get(new String("aaa"));

我们可以知道返回的就是hello,所以我们可以来看看String里面是怎么实现hashCode的。

public int hashCode() {
int h = hash;//hash默认为0,用来缓存String里面的hashCode
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

它是根据字符串的内容来确定hashCode的,所以如果字符串的内容是相同的,那么hashCode也是相同的。所以我们可以类似来实现PhoneNumber的hashCode。

所以对应PhoneNumber中的hashCode,也可以有类似的实现:

public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}

这样得到的hashCode值跟里面的属性内容是有关系的,如果属性内容都相同,hashCode也就相同了。

第10条:始终要覆盖toString方法

在实际应用中,toString方法应用返回对象中含有的值得关注的信息。另外可以为toString指定返回的格式。

比较好的toString实现方法:

    public String toString() {  
StringBuffer sb = new StringBuffer();
Class c = this.getClass();
Field[] f = c.getDeclaredFields();
for (int i = 0; i < f.length; i++) {
f[i].setAccessible(true);
String fieldName = f[i].getName();
sb.append(fieldName);
sb.append("=");
try {
sb.append(f[i].get(this));
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
sb.append("\n");
}
return sb.toString();
}

第11条:谨慎地覆盖clone方法

Cloneable接口的目的作为对象的mixin接口,表示该对象可以被克隆。但是它并没有提供clone方法,clone方法在Object中,但是这个方法是protected的,我们可以直接重写这个方法,但是也没办法直接进行调用,所以一般除了实现Cloneable之外,就是对Object中受保护的clone方法提供公有的访问途径。

既然Cloneable并没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法的行为,只有实现了Cloneable,Object的clone方法才能返回拷贝对象,否则会抛出异常。

拷贝对象往往会导致创建它的类的一个新实例,但它同时也会要求拷贝内部的数据结构。这个过程中没有调用构造器。

如果希望拷贝一个对象,超类提供这种功能的唯一途径是,返回一个通过调用super.clone而得到拷贝对象。但是我们一般在使用的时候需要特别注意浅拷贝和深拷贝。

简而言之,所有实现了Cloneable接口的类都必须用一个公有的方法覆盖clone,此公有方法首先调用super.clone,然后修正任何需要修正的域。一般情况下,就意味着要拷贝任何包含内部”深层结构”的可变对象,并用执行新对象的引用代替原来指向的这些对象的引用。

第12条:考虑实现Comparable接口

如果类实现了Comparable接口就表明它的实例具有内在的排序关系。

下面来看看Collections中的sort方法。它要求List中的元素对象继承自Comparable,这样它内部的元素对象就具有内部比较功能了。

public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}

default void sort(Comparator<? super E> c) {
// 传进来的c为null
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
......
}

public static <T> void sort(T[] a, Comparator<? super T> c) {
// 传进来的c为null
if (c == null) {
sort(a);
}
......
}

public static void sort(Object[] a) {
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}

static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
}

private static void binarySort(Object[] a, int lo, int hi, int start) {
......
for ( ; start < hi; start++) {
Comparable pivot = (Comparable) a[start];
......
while (left < right) {
int mid = (left + right) >>> 1;
if (pivot.compareTo(a[mid]) < 0)
right = mid;
else
left = mid + 1;
}

}
}

上面代码都不完整,只把重点代码列举处理了,可以看到对象的比较使用的就是Comparable中的compareTo来进行比较。所以如果希望自己的对象拥有排序的能力,可以考虑实现这个接口。