本栏是博主根据如题教材进行Java进阶时所记的笔记,包括对原著的概括、理解,教材代码的报错和运行情况。十分建议看过原著遇到费解地方再来参考或与博主讨论。致敬作者Joshua Bloch和各路翻译者们,以及为我提供可参考博文的博主们。
重写equals()方法的同时也要重写hashCode()方法
必须在每个重写equals()的类中重写Object.hashCode(),否则将违反hashCode()的通用规约,然后在比如HashMap,HashSet这种类里面使用时,出现各种奇怪的异常。hashCode的使用规约大致如下:
hashCode通用规约
- 不改变在equals()中比较所涉及到的相关参数前提下,对某一对象多次执行hashCode, 它必须返回相同值,而该应用本次执行结束后,下一次执行跟本次执行时的返回值是否相同就没啥要求了。
- 如果x.equals(y) == true,那么x和y的hashCode()返回值也应相同。
- 如果x.equals(y) == false,x和y的hashCode()返回值不要求绝对不同。但程序员应清楚,存在不同对象有相同hashCode的情况。可以考虑改用更好的映射函数尽量减少这种情况,以提供更好性能。
问题案例
最关键的是第二条。根据equals方法,先后创建的两个实例在逻辑上相等是有可能的,但对于hashCode方法来讲,两者只是两个不同的对象,于是返回值也会不同。比如这个例子PhoneBookTest:
/**
* 电话簿,用于说明为什么重写equals一定要同时重写hashCode
*/
public class PhoneBookTest {
private static Map<PhoneNumber , String> phoneBook= new HashMap<>();
public static void main(String[] args) {
phoneBook.put(new PhoneNumber("403,404,8080") , "lightDance");
String targetName = phoneBook.get(new PhoneNumber("403,404,8080"));
System.out.println(targetName);
}
}
/**
* 跟电话簿配合使用的电话号码
*/
public class PhoneNumber {
private String number;
public String get() {
return number;
}
public void set(String number) {
this.number = number;
}
public PhoneNumber(String number) {
this.number = number;
}
}
我们希望只要把相同的电话号码当作参数传进去,就可以得到相同的联系人姓名,但是实际上,由于没有重写equals方法,两个new出来的PhoneNumber是两个不同的对象,于是get方法当然查不到对应的联系人姓名
就算两对象哈希返回值恰好一样也不会将两实体弄混,因为HashMap在存储的时候,会同时存储key的哈希值和key的实例,而根据key去取对应value时HashMap.get(Object),也会在算出hashCode值之后,在具有相同hashCode的元素链表中逐一对比,看key是否相同(x.equals(y) || x == y),只有这些条件全部满足才会确认是该元素。
修正措施
要想修正这个问题,应该重写hashCode()方法的逻辑。首先如果想让其满足预期要求,那么有很简单很暴力的方式,比如这个{@link BadPhoneNumber},直接返回个常数24出来。这样的确能令相同对象拥有相同hashCode,但如此一来,HashMap中所有的元素就全部被放到了同一根链表里面,复杂度平了个方。
优秀的hashCode方法倾向于为不同的对象提供尽可能不同的hashCode值,而且各个相同hashCode值相同的链表中,元素个数也尽可能相同。不过这只是理想状态。下面给出一种较为理想的修改方式BestPhoneNumber:
/**较为理想的hashCode重写方式及一般步骤,这里不妨将所存的数据搞多一些,以方便说明问题。*/
public class BestPhoneNumber {
/**
* 性别
*/
private boolean sex;
/**
* 亲密度
*/
private float intimacy;
/**
* 电话号码
*/
private String number;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof BestPhoneNumber)) {
return false;
}
BestPhoneNumber phoneNumber = (BestPhoneNumber) obj;
return phoneNumber.number.equals(this.number) &&
phoneNumber.sex == this.sex &&
phoneNumber.intimacy == this.intimacy;
}
@Override
public int hashCode() {
int result = 23;
result = 31 * result + number.hashCode();
result = 31 * result + Float.hashCode(intimacy);
result = 31 * result + Boolean.hashCode(sex);
return result;
}
}
hashCode计算方案设计
1.随便搞个非零的常数,作为hashCode的乘数,这里用了23,放在hashCode()方法里面
2.对equals方法所设计需要比较的每一个参数f,按如下方式计算其局部哈希值c:
- 对boolean, byte, char, short, int, long, float, double这些基本数据类型,使用Type.hashCode(f),其中Type是对应基本类型的装箱类型
- 对引用类型并且其equals也使用递归比较字段的,那么也递归调用其hashCode()以计算c;如果要实现更复杂的比较,那么就为它设计一个“范式”,然后在范式中调用hashCode.另外,若该字段为null则c = 0(其实也可以是其他常数,但一般都用0)
- 对于数组字段,将其中所有有效字段单独计算局部哈希值然后求和;如果没有有效字段,则使用常量,但这时最好就不要使用0了。另外,如果数组中全部元素都有效,考虑使用Array.hashCode()
3.result = 31result + c,并将这个值返回
*.选择31这个数字是因为它是奇素数,素数可以有效减少哈希值相同的情景,而奇数可以防止因位移而丢失信息(偶数为2的倍数,*2等价于向左移位)
**.result有一个非0的初始值,这样第二步中的0默认值就不会对它产生影响导致冲突增加
***.有时需要基于参数顺序调整计算哈希值的参数,例如当计算String类型hashCode时,"add"与"dda"有不同返回值更有利于减少冲突。
****.另外,数字31有个比较好的特性,就是可以通过位移和减法完成乘法操作(31i = i << 5 -i),这种优化很多虚拟机可以自动完成
将它们运用在hashCode中得到这样的代码:
@Override
public int hashCode() {
int result = 23;
result = 31 * result + number.hashCode();
result = 31 * result + Float.hashCode(intimacy);
result = 31 * result + Boolean.hashCode(sex);
return result;
}
它是hashCode近乎完美的一个实现,媲美Java类库中的其他hashCode实现。但是这并不是最先进的,如果的确需要减少hashCode值的冲突,可以考虑使用Guava的com.google.common.hash.Hashing
重写hashCode后,需编码验证相等的实例是否有相同hashCode值(除非使用了AutoValue这样的工具,能保证无误),如果不能则找出问题并改正它。
Objects.hash()
其实,还有一种更偷懒的方式创建hashCode,即使用Objects.hash(Object...),但可惜由于用到了varargs新特性,以及基本类型的自动装箱和拆箱,速度较慢,只建议在对性能不作要求的条件下使用。比如:
@Override
public int hashCode() {
return Objects.hash(arg1 , arg2);
}
注意事项
对于不可变且hashCode计算成本高的类,考虑在对象中缓存hashCode,仅仅当参数发生改变时才重新计算。如果能确认hashCode会作为key用在HashMap等类中,那么最好在实例创建的时候就把hashCode计算出来,否则就使用延迟加载。
/**延迟加载的hashCode代码*/
private int hashCode;
@Override
public int hashCode() {
int result = hashCode;
if(result == 0){
result = Integer.hashCode(arg1);
result = 31 * result + Double.hashCode(arg2);
result = 31 * result + Short.hashCode(arg3);
hashCode = result;
}
return result;
}
不可以为了加快hashCode运行的速度就随随便便把什么参数(equals中涉及的)排除在计算范围之外,否则很可能影响HashMap的性能。举个例子,Java1.2中的String.hashCode()仅仅取前16个字符进行hash计算,于是它遇到了URL...
不要对返回值做些什么规定,这样会降低其灵活性而且不利于以后的扩展和修改,虽然Java类库中有不少类已经这么干了,但是小朋友们千万不要模仿他们哦
全代码git地址:点我点我