文章目录
1、Object类的equals和hashCode方法
==比较两个对象的地址。只有a,b指向同一个对象,才返回true。非常严格,不能被重写。
equals默认和==效果相同。
但是可以重写equals方法。比如String类重写了,即使a,b指向的对象不是同一个,但是两个字符串长度字符都相同就返回true。
hashCode()默认比较地址的哈希码。一般ab指向不同对象时(地址一定不同),哈希码也不同。
因此:
a==b 必定有 a、b的hashCode相同
a、b的hashCode相同 不一定有 a==b
2、equals和hashCode方法的作用
自己用
自己使用的时候,只需要重写和使用equals方法就可以了。比如Point类{x,y}。重写equals方法只需要比较两个对象的x坐标和y坐标。
事实上,自己使用不一定要重写equals方法,新定义一个isEqual方法、same方法都能用。
当作HashSet、HashMap的键
HashSet
内部也是HashMap
,HashMap
怎么判断两个键相等?
目标是判断已有元素是否和当前元素equals
,但是直接调用equals
太费时间,因此先比较hashCode
,这也是hashcode存在的原因。
如何快速比较两个对象是否equals:
若hashCode不同则必定不equals(这是由hashCode方法的特点保证,后面介绍),若hashCode相同,再调用equals
但是比较所有元素的hashCode需要o(n)时间,也很慢,所以hashmap内部是一个链表组成的数组。判断是否contains时,先把hashcode映射为数组的index,接着只需要比较index号的链表内是否存在具有相同hashCode的元素。
这样做也有一点点问题,看后文第六部分。
3、重写equals和HashCode方法
重写equals要求,这些要求是很显然的:
- 自反性 a.equals(a)
- 对称性 x.equals(y) y.equals(x)
- 一致性 x.equals(y) 多次调用结果一致
- 对于任意非空引用x,x.equals(null) 应该返回false
重写HashCode方法要求:
equals
则hashCode
必须相同。.hashCode
不同,则必须不equals
- 尽量保证hashCode相同时equals,尽量保证不equals的时候,hashCode不同。
也就是说重写后的equals和hashcode基本有一致性,因为hashCode的目的就是快捷版的equals
由于第1条的规定,重写了equals方法就必须重写hashCode方法。
4、数组的equals和hashCode
数组没有重写equals方法和hashcode方法,直接arr1.equals(arr2)
比较的是地址。
所以比较两个数组是否长度相等且元素相等,使用Arrays.equals(arr1,arr2)
方法,而不是arr1.equals(arr2)
。
因为没有重写equals方法和hashCode方法,数组不建议放进HashSet和HashMap的key。
5、list的equals和HashCode
list的equals比较的是list长度和每个元素是否equals。
list的元素和长度都相同,则equals,且hashCode相同。
list的元素和长度不都相同,则不equals,且大概率hashCode不相同。
因此,list执行完add前后肯定不equals,hashCode也大概率不同。
6、 对象做key后不要修改
以List为例,看看list做key后再add会发生什么。
HashSet<List<Integer>> set = new HashSet<List<Integer>>();
ArrayList<Integer> list = new ArrayList<>();
set.add(list);
System.out.println(list.hashCode());//1
System.out.println(set.iterator().next().hashCode());//1
list.add(8);
System.out.println(list.hashCode()); // 39
System.out.println(set.iterator().next().hashCode());// 39
System.out.println(set.iterator().next().equals(list));//true
System.out.println(set.contains(list)); //false ???
set.add(list);
System.out.println(set.size());//2 ???
set.add(new ArrayList<Integer>());
System.out.println(set.size());//3
前面没有什么问题,但是set中已有的list和即将加入的listhashcode
都是39,且 set.iterator().next().equals(list)
都返回true
了,为何set.contains(list)
还是返回false
原因:
哈希表的内部是数组加链表(不考虑红黑树),即是链表组成的数组。添加元素时,先对元素计算哈希值hashcode
,然后根据哈希值选择放入哪个链表(多个哈希值对应单个链表)。
第一次set.add
的时候,list的hashcode
是1,因此被放入某个table数组中的某一列,假设在第1列,即1号链表。
接着list调用list.add
方法,改变了自己的hashcode
为39。
第二次调用set.add
方法,list的hashcode
是39,尝试放入某一列,假设是7,即7号链表。和7号链表的所有元素比较(先比较hashcode
,相同就再比较equals
),没有发现冲突(因为7号链表没有其他元素),所以可以放入。
最后,set中拥有了两个指向同一个list的“指针”,虽然这两个list指向同一地址,但是没有冲突。
所以:list放入set之后,不要再调用list.add
list.set
或lilst.remove
方法了。
同样的道理,一般的对象放入set
后,也不要让它的hashcode
改变。例如
HashSet<Student> set = new HashSet<Student>();
Student s = new Student(2);
set.add(s);
s.age=1;
set.add(s);
System.out.println(set.size());//2
class Student{
int age;
public Student(int age){
this.age = age;
}
@Override
public boolean equals(Object o){
if(o==this) return true;
if(o instanceof Student2){
return ((Student2)o).age == this.age;
}
return false;
}
@Override
public int hashCode(){
return this.age;
}
}
因为改变了对象s
的age
,第二次add又成功了。此时打印set的两个元素,age都是1。