首先之所以会将hashCode()与equals()放到一起是因为它们具备一个相同的作用:用来比较某个东西。其中hashCode()主要是用在hash表中提高 查找效率,而equals()则相对而言使用更广泛,用于比较两个对象的值是否相同,在Java集合框架中它们共同出现用来比较某元素是否相等。
一hashCode()
hashCode()位于Object类中,其定义如下:
public native int hashCode();
从上述的定义可以看到hashCode()属于本地方法。
1为何需要hashCode()或者说hashCode()的作用:
我们知道当判断两个对象是否相同时我们时我们可以使用equals()方法,那么为何需要hashCode()呢?其实从名字上就可以看出hashCode的作用是为hahs表准备的,是为了提高查找效率而存在的,如在java集合框架中的HashSet,HashMap。
那么当我们往集合中添加一个元素的时候如何判断该元素是否存在呢?(注意java集合框架中除了ArrayList与LinkedList外都不允许存在重复元素,对于Map集合重复指的是K/V都相同)如果不用hashCode(),那么你可能会想到用equals()方法将集合中已存在的元素与待插入元素一个一个比较,但是这样做显然效率低下,因此hashCode()应运而生,hashCode()就是为了解决元素是否重复而存在的,它采用一定的规则将和对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。它用来标识一个对象,如果hash值不同则表示这两个对象一定不同,如果hash值相同,则这两个对象可能相同也可能不同,此时需要使用equals()来判断这两个对象是否相同注意:equals()才是用来判断两个对象是否相同的核心,hashCode()只是为了提高在集合中的查找效率而存在的,只要hashCode值不同则这两个对象一定不同,如在HashMap的put函数中,通过hash的值来判断是否存在该元素,如果hash值不存在(tab[i]==null),则一定不存在该元素,若hash值存在,则可能存在该元素,需要通过equals方法来确定,如果hash值存在且key.equals.(k)则表明存在该元素,直接更新其值,否则表明不存在,则采用链表或红黑树的方式将元素添加到tab[i]对应的链表或红黑树中,这样的话就能大大减少使用equals的次数,从而提高效率。HashMap的put函数源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//1
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//2首先判断tab[(n - 1) & hash]处是否为空,如果是代表该数组下标为[(n - 1) & hash]的位置无元素,可直接put
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> 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<K,V>)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))))//如果Hash值相同,则调用equals方法来确定是否存在该元素,则执行break语句
break;//跳出for循环,执行下面的if语句,即<span style="font-family: Arial, Helvetica, sans-serif;">existing mapping for key,则更新value的值,e.value=value。</span>
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;
}
可以看到先通过hash得到插入的数组索引i,如果tab[i]==null,表示此下标处无元素存在,可直接添加元素,否则出现冲突,扫描链表或红黑树,在此过程中通过equals方法来确定是否存在该元素,如果存在,则直接更新,否则采用链表或红黑树的方式将元素添加到tab[i]对应的链表或红黑树中。
即通过hash的值来判断是否存在该元素,如果hash值不存在(tab[i]==null),则一定不存在该元素,若hash值存在,则可能存在该元素,需要通过equals方法来确定,如果hash值存在且key.equals.(k)则表明存在该元素,直接更新其值,否则表明不存在,则采用链表或红黑树的方式将元素添加到tab[i]对应的链表或红黑树中
那么我们能否仅仅根据hashCode()的值来判断两个对象是否相等呢?答案是不能,因为不同的对象可能会生成相同的hashcode值。虽然不能根据hashcode值判断两个对象是否相等,但是可以直接根据hashcode值判断两个对象不等,如果两个对象的hashcode值不等,则必定是两个不同的对象。如果要判断两个对象是否真正相等,必须通过equals方法。
也就是说对于两个对象,如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等;
如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同;
如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;
如果两个对象的hashcode值相等,则equals方法得到的结果未知。
二equals()
equals()同样也是位于Object类中的一个方法,我们来看一下其源码:
public boolean equals(Object obj) {
return (this == obj);
}
可以看到,Object类中的equals方法是用来判断两个对象的地址是否相等(this==obj),但是我们知道在使用String类的时候equals比较的是两个字符串的内容是怎么回事呢?这时因为String重写了Object类的equals(),因为对于字符串而言我们更关心其内容,而不是其对象地址,,我们来看一下String类中的equals()源码:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
我们可以看到,在String的equals()中首先判断这两个对象是否相同,若相同则直接返回true(因为同一个地址的String其存储的内容一定相同),若这两个对象不同则首先获取这两个字符串的长度,如果长度相同则逐个比较每个字符是否相同,若相同则返回true,如果长度不同则直接返回false。所以String类中的equals()比较的是两个字符串对象的内容。
其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。
1)对于==,如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;
如果作用于引用类型的变量,则比较的是所指向的对象的地址
2)对于equals方法,注意:equals方法不能作用于基本数据类型的变量
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
三重写equals()方法与hashCode()方法
我们知道java中的String,Date,Integer等类重写了equals()方法,那么什么时候需要重写equals方法呢?当一个类需要定义属于自己的“逻辑相等”的概念而不仅仅是对象引用是否相等时则需要重写该方法,那么什么时候需要重写hashCode()方法呢?其实hashCode()方法的重写不是强制的,它是为了在将你自己定义的类存入java集合框架然后get出来时确保是同一个对象所做的一种规范性要求,以满足java规范中的相等的对象必须具有相等的散列码,如下面的例子:
class People{
private String name;
private int age;
public People(String name,int age) {
this.name = name;
this.age = age;
}
public void setAge(int age){
this.age = age;
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
return this.name.equals(((People)obj).name) && this.age== ((People)obj).age;
}
}
public class Main {
public static void main(String[] args) {
People p1 = new People("Jack", 12);
System.out.println(p1.hashCode());
HashMap<People, Integer> hashMap = new HashMap<People, Integer>();
hashMap.put(p1, 1);
System.out.println(hashMap.get(new People("Jack", 12)));
}
}
在这里我只重写了equals方法,也就说如果两个People对象,如果它的姓名和年龄相等,则认为是同一个人。
这段代码本来的意愿是想这段代码输出结果为“1”,但是事实上它输出的是“null”。为什么呢?原因就在于重写equals方法的同时忘记重写hashCode方法。
虽然通过重写equals方法使得逻辑上姓名和年龄相同的两个对象被判定为相等的对象(跟String类类似),但是要知道默认情况下,hashCode方法是将对象的存储地址进行映射。而java集合get方法时首先会通过hashCode()得到的hash值来判断是否存在该元素,如果hash值不存在,则直接返回null,因此上述代码输出结果为null
HashMap中get(k)源码如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) { //通过该hash值与table的长度n-1相与得到数组的索引first
if (first.hash == hash && // always check first node//如果hash值不同下面的代码将不会执行
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)//代表该HashMap为数组+红黑树结构
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {//否则代表是数组+链表结构
if (e.hash == hash && <span style="font-family: Arial, Helvetica, sans-serif;">//如果hash值不同下面的代码将不会执行</span>
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;//<span style="font-family: Arial, Helvetica, sans-serif;">如果上述红黑树与链表结构中都不存在该hash值则表示该HashMap中不存在该元素,返回null</span>
}
因此如果想上述代码输出结果为“1”,很简单,只需要重写hashCode方法, 让equals方法和hashCode方法始终在逻辑上保持一致性。
class People{
private String name;
private int age;
public People(String name,int age) {
this.name = name;
this.age = age;
}
public void setAge(int age){
this.age = age;
}
@Override
public int hashCode() {
// TODO Auto-generated method stub
return name.hashCode()*37+age;
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
return this.name.equals(((People)obj).name) && this.age== ((People)obj).age;
}
}
public class Main {
public static void main(String[] args) {
People p1 = new People("Jack", 12);
System.out.println(p1.hashCode());
HashMap<People, Integer> hashMap = new HashMap<People, Integer>();
hashMap.put(p1, 1);
System.out.println(hashMap.get(new People("Jack", 12)));
}
}
这样输出结果即为1与预期结果相同。
四总结:
1hashCode()与equals()都属于Object类中的方法,因此java中的所有的类中都默认存在这两种方法。
2为满足java规范中的相等的对象必须具有相等的hash值,通常在重写equals()时一定要重写hashCode()方法。
3如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;对于==,如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;
如果作用于引用类型的变量,则比较的是所指向的对象的地址。