前言
大家都知道如果要判断一个对象是否相同,都要在对象实体中重写equals和hashcode方法,那你知道为什么重写这两个方法就能根据自己定义的规则实现相等比较了吗?
今天带大家来了解一下equals和hashcode重写的实现。
set是如何去重的?
Set只是一个接口,我们平时使用最多的是HashSet,那么HashSet是如何去重的呢?
来看下是如何往set中添加一个对象的:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
额,原来Set里其实是维护了一个map,通过map的key不可重复来实现我们想要的set功能。具体的map.put是怎么实现的呢?
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
我们都知道hashMap的基本原理是,通过hash去找数组的index位置,如果hash相等,那么相等的对象放到该hash槽的list中。如图所示:
再来看下putVal的源码是怎么实现的:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
看到这里,我们就清楚了,key可以认为就是HashMap中的槽位,也就是整个tables数组,如果这个key在数组中不存在(通过hash来判断),如果不存在,那就将key放到这个槽位。如果存在(通过hash判断相等,这个时候已经鸠占鹊巢了,需要通过equals来判断了),key相等,就会被新key替换,然后对应的值会放到这个槽位后的list中。
自定义对象如何去重
看到刚才的分析,基本就比较清晰了,先通过hashcode判断,然后通过equals判断。默认这两个函数都是Object类中实现的
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
Object中的hashCode是个native的方法,如果没有重写父类Object的hashCode方法,每次运行的结果都是不同整数,称为哈希值,没有特别意义;
而equals,默认的实现只是判断对象是否是同一个对象,这个明显也不是我们希望的。我们希望,对象的属性完全一样,就认为是相等的,无论这两个对象是否是同一个对象。
所以我们需要重写这两个方法,那怎么重写呢?
重写hashCode()
- 只要对象equals方法涉及到的关键域内容不改变,那么这个对象的hashCode总是返回相同的整数。(如果关键域内容改变,则hashCode返回的整数就可以改变)。
- 如果两个对象的equals(Object obj)方法是相等的,那么调用这两个对象中的任意一个对象的hashCode方法必须产生相同的整数结果。如果两个对象equals方法不同,那么必定返回不同的hashCode整数结果。(简而言之:相等的对象必须有相等的散列码即hashCode);
重写equals约定
- 自反性:x.equals(x) = true;
- 对称性:如果有x.equals(y) = true,那么一定有y.equals(x) = true;
- 传递性:对任意的x,y,z。如果有x.equals(y) = y.equals(z) = true,那么一定有x.equals(z)= true;
- 一致性:无论多少次调用,x.equals(y)总会返回相同的结果。
示例
假设我们现在有一个对象,里面包含两个属性,List和String:
public class User {
private List places = new ArrayList<>();
private String name;
}
重写及测试的方法如下:
public class User {
private List places = new ArrayList<>();
private String name;
public List getPlaces() {
return places;
}
public void setPlaces(List places) {
this.places = places;
}
public List addToPlaces(String place) {
this.places.add(place);
return this.places;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object object) {
if (object == this) {
return true;
}
if (object instanceof User) {
User other = (User)object;
if (!this.name.equals(other.name)) {
return false;
}
if (CollectionUtils.isEmpty(this.places) && CollectionUtils.isEmpty(other.places)) {
return true;
}
if (CollectionUtils.isEmpty(this.places) || CollectionUtils.isEmpty(other.places)) {
return false;
}
Collections.sort(other.places);
Collections.sort(this.places);
String origin = StringUtils.join(this.places.toArray(), ",");
String another = StringUtils.join(((User)object).places.toArray(), ",");
return origin.equals(another);
} else {
return false;
}
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (name == null ? 0 : name.hashCode());
result = 31 * result + (places == null ? 0 : getListHashCode(places));
return result;
}
@Override
public String toString() {
return "name:" + this.name + " list:" + places;
}
private int getListHashCode(List list) {
if (list.size() == 0) {
return 0;
}
int hash = 0;
for (String s : list) {
hash += s == null ? 0 : s.hashCode();
}
return hash;
}
public static void main(String[] args) {
List userList = new ArrayList<>();
User user1 = new User();
user1.setName("li");
user1.addToPlaces("beijing").add("tianjin");
User user2 = new User();
user2.setName("li");
user2.addToPlaces("beijing").add("tianjin");
User user3 = new User();
user3.setName("zhang");
User user4 = new User();
user4.setName("zhang");
if (!userList.contains(user1)) {
userList.add(user1);
}
if (!userList.contains(user2)) {
userList.add(user2);
}
if (!userList.contains(user3)) {
userList.add(user3);
}
if (!userList.contains(user4)) {
userList.add(user4);
}
System.out.println(userList);
Set set = new HashSet<>();
set.add(user1);
set.add(user2);
set.add(user3);
set.add(user4);
System.out.println(set);
}
}
输出结果
[name:li list:[beijing, tianjin], name:zhang list:[]]
[name:zhang list:[], name:li list:[beijing, tianjin]]
总结
- equals方法用于比较对象的内容是否相等(覆盖以后)
- hashcode方法只有在集合中用到
- 当覆盖了equals方法时,比较对象是否相等将通过覆盖后的equals方法进行比较(判断对象的内容是否相等)。
- 将对象放入到集合中时,首先判断要放入对象的hashcode值与集合中的任意一个元素的hashcode值是否相等,如果不相等直接将该对象放入集合中。如果hashcode值相等,然后再通过equals方法判断要放入对象与集合中的任意一个对象是否相等,如果equals判断不相等,直接将该元素放入到集合中,否则不放入。
- 将元素放入集合的流程图:
- HashSet中add方法源代码:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
- map.put源代码:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}