第九条 覆盖equals时总要覆盖hashCode方法

时间:2021-08-09 16:23:30
上一章主要讲了equals方法,今天讲他的相关方法,可以说是兄弟方法。
我们如果想比较两个同类型对象的值是否一样,可以通过重写equals方法来进行逻辑判断,一些更常见的用法是对象,比如ArrayList和LinkedList等集合,平常用到很多,为了判断集合中是否包含此对象,常用到contains()方法,这两个集合里面调用的方法
public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
ArrayList:
public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
LinkedList:
 public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }
发现,一个是数组结构,一个是链表结构,最终都是遍历集合元素通过equals方法来比较是否有逻辑值相同的元素;remove方法删除元素时,一种是根据索引,一种是根据对象值,第二种也用到了equals方法,因此,equals方法很重要。
但这与hashCode方法有什么关系?上面两个集合貌似没关系,但java中集合太多了,更高效的集合就需要用到hashCode方法了。比如HashMap HashSet Hashtable等。
java中也有规范:
对象的equals方法比较时没有被修改,此对象的hashCode方法返回值必须相同;如果两个对象的equals方法相等,那么这两个对象的hashCode方法返回值也要相等;两个对象equals值不相等,则hashCode方法返回值可以相等,也可以不相等。但有经验的都知道,不相等的话可以提高散列表的性能。
public class Student {

public String name;

public Student(String name) {
super();
this.name = name;
}

@Override
public boolean equals(Object obj) {
if(obj == this){
return true;
}
if(obj instanceof Student){
Student s = (Student)obj;
return s.name.equals(name);
}
return false;
}

}
List<Student> list = new ArrayList<Student>();
Student ss = new Student("jim");
Student ss2 = new Student("jim");
list.add(ss);
System.out.println(list.contains(ss2));
用list集合,返回值为true,可以认为集合中有这个对象,但如果是Hash列表,就
HashSet<Student> set = new  HashSet<>();
Student ss = new Student("jim");
Student ss2 = new Student("jim");
set.add(ss);
System.out.println(set.contains(ss2));
返回值为false,因为hash列表是先比较hashcode值,如果值不相等,那么是两个不用的对象,如果hashcode值相等,再比较equals方法,如果相等,那么就是同一个对象。加入Student里面加入hashCode方法,
@Override
public int hashCode() {

return 0;
}
那么就相等了,但如果都返回同一个hashcode值,那么散列的效率就太低了,就变成了LinkedList链表,所以还要有规则的写hashCode方法,例如
@Override
public int hashCode() {

return name.hashCode();
}
一般而言,hashcode值只要把它散开就行了,不需要太高深的算法,掌握基本的法则就可以解决大部分的问题了。
1、选择一个非0的常数,最好是质数,例如17,31等
2、根据不同的属性选择对应的计算方法
第一步:
如果是boolean类型,则 true 对应 1,false 对应 0 
如果是byte char short int 类型,计算 int值
如果是long类型,则计算 f^(f>>>32)
如果是float类型,则Float.floatToIntBits(f)
如果是double类型,则Double.doubleToLongBis(f),转换为long类型,然后按照long类型计算值
如果是一个对象引用,则对该类递归调用hashCode。
如果是一个数组,则递归计算数组中每一个对象的值

第二步:
用17或31乘以上述的值,然后相加,最终返回这个值,就是hashcode的值如上述对象
@Override
public int hashCode() {
int result = 17;
result = 31*result+name.hashCode();

return result;
}
如果Student中多出了一个属性 int age;则
@Override
public int hashCode() {
int result = 17;
result = 31*result+name.hashCode();
result = 31*result + age;
return result;
}
如果还有别的属性,依次类推。

如果一个对象的值不再修改,为了提高hashcode值得效率,可以对其hashCode方法加一步判断,
@Override
public int hashCode() {
int result = hashCode;
if(result == 0){
result = 17;
result = 31*result+name.hashCode();
result = 31*result + age;
hashCode = result;
}

return result;
}
这样就行了。一般不要为了提高性能而在散列码的计算中排除一个对象的关键部分,同时也不要期望hashCode方法太过于智能。