java面试过程中经常碰到==和equals的问题,而且经常拿String类来做为例子,所以好多人就记住了:==比较的是对象的地址,equals比较的是对象的内容。但是这种说法是不严谨的,请看下面的例子:
public class Test {
public static void main(String[] args) {
AAA a1 = new AAA();
AAA a2 = new AAA();
System.out.println(a1 == a2);
System.out.println(a1.equals(a2));
}
}
class AAA{
public int test = 2;
}
输出结果如下:
false
false
上面的例子中,类AAA的内容很确定,就以int类型的数据2,所以a1和a2的内容是相同的,但是equals的结果却是false,与我们预想的不一样;下面再看一个例子:
String s1 = new String("tuhao");
String s2 = new String("tuhao");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
输出结果如下:
false
true
从上面的例子可以看出,==确实是比较地址,不管是a1还是a1,或者s1还是s2,他们都是不同的对象,内存地址不一样,所以==返回false这很好理解;但是equals为什么返回值不一样呢?要想解决此问题,就必须要研究equals的源码了;equals方法是Object这个类里面的,代码如下:
public boolean equals(Object obj) {
return (this == obj);
}
可以看到,Object的equals方法其实就是==,比较两个对象的地址(假装引用就是对象的地址,实际上引用比地址复杂);在上面的AAA类里面没有重写equals方法,所以a1.equals(a2)比较的是两个对象的地址,所以返回false就不足为奇了。那么为什么s1.equals(s2)就返回true了呢?说明String类重写了equals方法,String的equals方法如下:
public boolean equals(Object anObject) {
//如果两个引用都一样,说明指向同一个对象
//,那么对象的内容也就肯定一样了,没毛病
if (this == anObject) {
return true;
}
//首先判断传进来的引用是不是指指向String类型
//的对象,如果类型都不一样,那么内容肯定不一样了
if (anObject instanceof String) {
//引入中间变量
String anotherString = (String)anObject;
//value是一个char类型的数组,我们知道,String在底层的
//实现其实就是一个char数组,所以value代表了String的内容
int n = value.length;
//如果两个数组长度不一样,也就是String的长度不一样,
//那就没什么好比的了,直接返回false,早死早超生吧
if (n == anotherString.value.length) {
//首先拿到调用equals方法的的String的内容
char v1[] = value;
//这是要比较的String的内容,传进来的
char v2[] = anotherString.value;
int i = 0;
//往死里遍历数组,挨个比较数组里面的char是不是一样的
while (n-- != 0) {
if (v1[i] != v2[i])
//如果有一个char不一样,那么说明两
//个String的内容不一样,返回false
return false;
i++;
}
return true;
}
}
return false;
}
从上面的分析可知,equals默认比的也是对象的地址,但是String类重写了这个方法,重写的方法遍历了char数组,然后挨个比较数组的元素,所以s1.equals(s2)返回了true。
从上面的分析可以看出,要想判断equals的返回结果,就看此类有没有重写equals方法,这些知识都比较基础,有一定经验的开发都知道。下面我们重写类AAA的equals方法:
class AAA{
public int test = 2;
@Override
public boolean equals(Object obj) {
if(obj instanceof AAA && obj != null) {
//向下转型
AAA tmp = (AAA) obj;
//AAA就一个成员变量test,如果两个AAA对象的test
//相同,我们就有理由认为两个AAA对象的内容相同
if(test == tmp.test) {
return true;
}
}
return false;
}
}
测试代码:
AAA a1 = new AAA();
AAA a2 = new AAA();
System.out.println(a1.equals(a2));
System.out.println(key1 == key2);
输出结果如下:
true
false
可以看到,重写equals后,a1和a2的内容就一样了。一切看起来是如此美好,直到出现下面这个例子:
class People{
int age;
String name;
public People(int age, String name) {
super();
this.age = age;
this.name = name;
}
//这里对equals方法进行了重写
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
People other = (People) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
测试代码:
public class Test {
public static void main(String[] args) {
HashMap<People,Integer> map = new HashMap<>();
//创建一个36岁的胡歌对象
People p = new People(36,"huge");
//放入HashMap
map.put(p, 1);
//这里又创建一个36岁的胡歌对象,根
//据这个对象去查找相应的Integer对象
Integer it = map.get(new People(36,"huge"));
System.out.println(it);
}
输出结果如下:
null
妈蛋,竟然没找到!!!我们明明重写了equals方法啊,在本例中,只要两个People的age和name一样,就说明两个对象的内容一样啊,为什么找不到呢?先来看下HashMap的get方法吧:
public V get(Object key) {
Node<K,V> e;
//核心就是这个getNode方法,根据key的hash值找
//Node,找到了就返回Node的value,否则就返回空,没找到
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
继续分析getNode方法(节选):
......
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
......
从代码中可以看出,在HashMap中获取元素时,不仅仅要比较key的值(就是第二次new出来的People对象),还要比较他们的Hash值,也就是说,光重写equals方法是不够的,还得重写hashCode方法。为什么呢?来看下equals的API: 从API中可以看到,重写equals的时候,一般最好也重写hashCode方法,否则在某些集合类(比如HashMap)中使用该类时,会发生意想不到的结果。
可以看到,如果两个对象通过equals比较相等,那么他们的hashCode()方法生成的整形hashCode值必定是相等的。那么这个hashCode怎么生成呢?hashCode()方法是Object的方法,默认的hashCode是在native层生成的:
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
* <p>
* The general contract of {@code hashCode} is:
* <ul>
* <li>Whenever it is invoked on the same object more than once during
* an execution of a Java application, the {@code hashCode} method
* must consistently return the same integer, provided no information
* used in {@code equals} comparisons on the object is modified.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
* <li>If two objects are equal according to the {@code equals(Object)}
* method, then calling the {@code hashCode} method on each of
* the two objects must produce the same integer result.
* <li>It is <em>not</em> required that if two objects are unequal
* according to the {@link java.lang.Object#equals(java.lang.Object)}
* method, then calling the {@code hashCode} method on each of the
* two objects must produce distinct integer results. However, the
* programmer should be aware that producing distinct integer results
* for unequal objects may improve the performance of hash tables.
* </ul>
* <p>
* As much as is reasonably practical, the hashCode method defined by
* class {@code Object} does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the
* Java™ programming language.)
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
public native int hashCode();
很多人理解成对象的内存地址,其实一般都是错误的,hashCode的生成有多种方式,用内存地址的方式来生成只是其中一种,但是各虚拟机厂商都不会选择这种方式,这个问题已经有前辈研究过:https://blog.csdn.net/god8816/article/details/53866182,感兴趣的可以了解下。
在上面的例子中,第一次调用new People(36,"huge")和第二次调用new People(36,"huge")的时候分别生成了两个不同的对象,这两个People的hashCode不一样,HashMap在查找元素时,首先就会判断传进来的People对象的hash和容器里面的People的hash是否有一样,因为他们的hash不一样,所以最终找不到,返回null。既然重写要求equals的时候要求重写hashCode(),那么我们重写吧:
class People{
int age;
String name;
public People(int age, String name) {
super();
this.age = age;
this.name = name;
}
//这里重写hashcode
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
//这里重写equals
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
People other = (People) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
测试代码:
public class Test {
public static void main(String[] args) {
HashMap<People,Integer> map = new HashMap<>();
//创建一个36岁的胡歌对象
People p = new People(36,"huge");
//放入HashMap
map.put(p, 1);
//这里又创建一个36岁的胡歌对象,根
//据这个对象去查找相应的Integer对象
Integer it = map.get(new People(36,"huge"));
System.out.println(it);
}
输出结果如下:
1
可以看到,重写hashCode()方法后,我们就能找到之前存入的数据了。那么hashCode是怎么重写的呢?上面例子中的hashCode是eclipse自动生成的,从代码来看,大体上是把参与equals比较的各个属性按照一定的规则生成一个整形数据。当然系统有一个类也提供了类似的方法,这个类就是Objects(注意,不是所有类的父类的那个Object类),这个类提供了两个方法帮助我们生成hashCode:
public final class Objects {
......
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
}
......
上例中可以将eclipse生成的hashcode改成如下:
//这里重写hashcode
@Override
public int hashCode() {
return Objects.hash(age,name);
}
程序执行的结果同eclipse生成的hashcode。