关于 hashcode 和 equals
首先需要明白 hashCode() 和equals是Object类中已经被定义好的,所以在java中定义的任何类都有这两个方法。其中原始的equals()方法是用来比较两个对象的地址值,而原始的hashCode()方法返回其对象所在的物理地址。看下面一个例子:
public static void main(String[] args) { Person person1 = new Person(10,"zhangsan"); Person person2 = new Person(10,"zhangsan"); System.out.println("pserson1 hasCode:" + person1.hashCode()); System.out.println("pserson2 hasCode:" + person2.hashCode()); System.out.println(person1.equals(person2)); } } class Person{ int age; String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Person(int age, String name) { this.age = age; this.name = name; }
运行结果如下:
需要注意的一点是,equals()相等的两个对象,hashCode()一定相等,hashCode()相等的话,并一定是相等的两个对象,即equals并一定相等(我的理解是equals比较的是两个对象,而hasCode()是对象的属性,对象相等那么其属性一定相等,相反其属性相等但是并不一定是同一个对象)。这就要求我们在重写自定义类的时候如果重写了equals()方法的话,一定也要重写hascode()方法。
同时我们知道Set集合是不允许有重复的内容的。具体判断set集合中是否有已经有该对象的步骤如下:
1)、判断两个对象的hashCode是否相等 。
如果不相等,认为两个对象也不相等,完毕
如果相等,转入2)
(这一点只是为了提高存储效率而要求的,其实理论上没有也可以,但如果没有,实际使用时效率会大大降低,所以我们这里将其做为必需的。)
2)、判断两个对象用equals运算是否相等 。
如果不相等,认为两个对象也不相等
如果相等,认为两个对象相等(equals()是判断两个对象是否相等的关键)
如果想查找一个集合中是否包含有某个对象,大概的程序代码怎样写呢?
你通常是逐一取出每个元素与要查找的对象进行比较,当发现某个元素与要查找的对象进行equals方法比较的结果相等时,则停止继续查找并返回肯定的信息,否则,返回否定的信息,如果一个集合中有很多个元素,比如有一万个元素,并且没有包含要查找的对象时,则意味着你的程序需要从集合中取出一万个元素进行逐一比较才能得到结论。
有人发明了一种哈希算法来提高从集合中查找元素的效率,这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组(使用不同的hash函数来计算的),每组分别对应某个存储区域,根据一个对象的哈希吗就可以确定该对象应该存储在哪个区域HashSet就是采用哈希算法存取对象的集合,它内部采用对某个数字n进行取余(这种的hash函数是最简单的)的方式对哈希码进行分组和划分对象的存储区域;Object类中定义了一个hashCode()方法来返回每个Java对象的哈希码,当从HashSet集合中查找某个对象时,Java系统首先调用对象的hashCode()方法获得该对象的哈希码表,然后根据哈希吗找到相应的存储区域,最后取得该存储区域内的每个元素与该对象进行equals方法比较;这样就不用遍历集合中的所有元素就可以得到结论,可见,HashSet集合具有很好的对象检索性能,但是,HashSet集合存储对象的效率相对要低些,因为向HashSet集合中添加一个对象时,要先计算出对象的哈希码和根据这个哈希码确定对象在集合中的存放位置为了保证一个类的实例对象能在HashSet正常存储,要求这个类的两个实例对象用equals()方法比较的结果相等时,他们的哈希码也必须相等;也就是说,如果obj1.equals(obj2)的结果为true,那么以下表达式的结果也要为true:
obj1.hashCode() == obj2.hashCode()
换句话说:当我们重写一个对象的equals方法,就必须重写他的hashCode方法,不过不重写他的hashCode方法的话,Object对象中的hashCode方法始终返回的是一个对象的hash地址,而这个地址是永远不相等的。所以这时候即使是重写了equals方法,也不会有特定的效果的,因为hashCode方法如果都不想等的话,就不会调用equals方法进行比较了,所以没有意义了。
如果一个类的hashCode()方法没有遵循上述要求,那么,当这个类的两个实例对象用equals()方法比较的结果相等时,他们本来应该无法被同时存储进set集合中,但是,如果将他们存储进HashSet集合中时,由于他们的hashCode()方法的返回值不同(Object中的hashCode方法返回值是永远不同的),就是说可能会被set保存两次的。
看下面这个例子:
public static void main(String[] args) { Person person1 = new Person(10,"zhangsan"); Person person2 = new Person(10,"zhangsan"); System.out.println("pserson1 hasCode:" + person1.hashCode()); System.out.println("pserson2 hasCode:" + person2.hashCode()); System.out.println(person1.equals(person2)); Set<Person> set = new HashSet<Person>(); set.add(person1); set.add(person2); System.out.println(set.size()); } } class Person{ int age; String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Person(int age, String name) { this.age = age; this.name = name; } @Override public boolean equals(Object o){ Person a = (Person) o; return a.name.equals(name) ; } //@Override //public int hashCode(){ // return 1; //}
这里面我们只是重写了equals方法,调用 person1.equals(person2) 也是返回true的,但是最终set.size()是2。这是运行结果
我们再修改一下例子,重写一下 hascode方法
@Override public int hashCode(){ return age + name.hashCode(); }
再看一下结果
我们再来看一个有意思的事情,如果我们把equals直接返回false,那么再调用set.add(person1),那么按照我们以上的分析,先检查hashCode() 是否相等,再调用equals()方法,那么这时候应该返回 set.size() 为3,然而事情的真相真如我们分析的那样么,先看运行结果;
public static void main(String[] args) { Person person1 = new Person(10,"zhangsan"); Person person2 = new Person(10,"zhangsan"); System.out.println("pserson1 hasCode:" + person1.hashCode()); System.out.println("pserson2 hasCode:" + person2.hashCode()); System.out.println(person1.equals(person2)); Set<Person> set = new HashSet<Person>(); set.add(person1); set.add(person2); set.add(person1); // System.out.println(person1 == person1); System.out.println(set.size()); } } class Person{ int age; String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Person(int age, String name) { this.age = age; this.name = name; } @Override public boolean equals(Object o){ //Person a = (Person) o; //return a.name.equals(name) ; return false; } @Override public int hashCode(){ return age + name.hashCode(); }
hashCode是基于hashMap实现的,我们查看一下hashMap的源码查找一下原因
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> 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; }
分析一下 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 如果 hash一样的话,会去判断 key == key 和 key.equals(k),只要两个有一个符合要求就插入不成功,而对于同一个对象来说 person1 == person1 永远是true,所以set里面还是2,当然这种情况很少会遇到,一般重写 equals()方法的时候不会直接返回 false~。如果非要让set变成3个话也行,重写一下hashCode就可以了,比如我们可以产生一个随机数
@Override public int hashCode(){ return (int)(Math.random()*100); }
最后在看一个删除时候应该注意的地方:
public static void main(String[] args) { Person person1 = new Person(10, "zhangsan"); Person person2 = new Person(10, "zhangsan"); //System.out.println("pserson1 hasCode:" + person1.hashCode()); //System.out.println("pserson2 hasCode:" + person2.hashCode()); //System.out.println(person1.equals(person2)); Set<Person> set = new HashSet<Person>(); set.add(person1); set.add(person2); set.add(person1); System.out.println("删除前set.size():" + set.size()); System.out.println("更改属性前person1.hashCode():" + person1.hashCode()); person1.setAge(6); System.out.println("更改属性后person1.hashCode():" + person1.hashCode()); set.remove(person1); // System.out.println(person1 == person1); System.out.println("删除后set.size():" + set.size()); }
运行结果:
事情又出乎我们的意料之外,我们明明已经删除了person1,但是事实上并没有删除成功,我在程序中已经把原因打印出出来了,更改了属性以后,因为我们的hashCode()是根据属性值来生成的,因此属性更改以后hashCode()也被修改了但是他的存储位置不会有变化,当删除的时候会按照新的hashCode()去找person1,肯定找不到,所以并没有删除成功,会导致失败。
这告诉我们一个事情:如果对象的属性值参与了hashCode的运算中,在进行删除的时候,就不能对其属性值进行修改,否则会出现严重的问题。