一、equals()
我们都知道==是用来判断两者是否相等。像基础数据类型的话比较当然没有问题,比如:
int a = 1;
int b = 1;
System.out.println(a == b);//true
结果肯定是true,但如果是引用数据类型呢?
Student s1 = new Student("小李");
Student s2 = new Student("小李");
System.out.println(s1 == s2);//false
比如这个代码,我么创建了两个Student对象,我们的需求是同名字的Student需要判断是相同的;但 == 的结果并不是我们需要的,这是因为在引用类型使用 == 时,比较的是两个引用指向的内存地址是否相同,但显然很多时候我们只是需要逻辑上的相等。
这时候就需要引入equals来解决。
equals 是 Object类就有的方法,所有的java类都可以重写他,来自定义对象相等的条件。
public boolean equals(Object obj) {
return (this == obj);
}
- Object类的方法实现,只是用 == 来实现
- 我们知道我们定义的所有类都自动继承自Object类。
因此,我们可以将自定义类重写equals方法。我们来分析idea自动生成的equals重写方法是怎样的
@Override
public boolean equals(Object o) {
if (this == o) return true;//如果是指向一个对象,肯定相同。
if (o == null || getClass() != o.getClass()) return false;//如果为空,或者不是本类类型的对象,肯定不相同
Student student = (Student) o;//将引用强转为本类类型
return Objects.equals(name, student.name);//自定义判断相等,这里是使用student的name属性来判断。
}
接下来解答一下可能疑问的点:
- o.getClass() 就是得到o这个对象的类类型,比如Student类型的对象的类类型就是Class
- 可以看到最终还是是调用了Objects.equals(name, student.name),相当于比较两个String类型的变量是否相同。实际上这里最终会调用String类里的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;
}
实现比较简单,可以看到equals重写并不复杂,但不可或缺。
二、hashcode()
hashcode()的出现有他特定的作用,首先得先了解一下hash表。
我们知道,数组的链表都有他们各自的缺点。
- 数组存放,删除元素很麻烦。
- 链表存放,查找元素很麻烦。
有没有一个容器可以鱼与熊掌兼得呢?就是Hash表。
他的内部结构是数组+链表,类似这样,圆形表示存放的元素。
如何工作呢?
首先给存放的每一个元素都发一个属于自己的一个hash值。然后根据这个hash值经过计算得到一个下标,然后将元素存放到这个下标下的链表中。
- 数组的缺点是删除元素,而这里删除元素后并不需要移动其他元素,只需要把对应的结点置空即可。
- 链表的缺点是查找,hash表将链表分成了很多个很短的链表,所以也不存在这个缺陷。
完美的解决的这两个初始容器的缺陷。
好了。现在我们来看hashcode,他工作在哪一环呢?
我们以HashMap为例,他是java中hash表的具体实现之一,以键-值对存放(key-value)。
如果我们调用put方法存放元素,源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
可以看到形参调用了一个hash(),首选我们去putVal()看看这个值有什么用,这里只分析部分源码,因为主要目的是认识hash的作用。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
这个操作可以看到 将 (n - 1) & hash 赋给了i,这个hash就是put方法传过来的第一个int形参。下一行可以看出 i 是下标,并且这个下标存放的如果是空,就存放一个元素。看到这已经够了,
- 我们可以看到hash的一个作用就是配合计算存放元素的下标
- 并且如果两个元素的hash值是相同的,那么存放的将是同一个下标。
- 但存放在同一个下标的元素hash值并不一定相同。这就涉及到具体是如何实现的计算。比如这里的(n - 1) & hash,n是容量,当n=16时,hash值为15和31计算出来的下标是一样的。因为 15的二进制1111,31的二进制11111,可以看到计算下标只与低位为准,多余的高位在位运算中并不重要。
现在我们去看看hash()是怎么实现的。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看到用到了key的hashCode,这句的意思是将hashCode()计算出的值无符号右移16位,再和自己异或,其实就是将这个数的高16位和第16位进行异或。
现在我们来分析一下hashcode()的作用。hash表中需要一个hash值来计算存放的下标,需要逻辑上相等的元素的hash值相等。这里涉及到一个面试题:重写equals()为什么还要重写hashcode()?
- hash表查找元素时首先通过hash值来算一个下标,然后在同一个下标里的链表中用equals来判断是否元素,主要是为了hash表查找的效率,如果说没有hashcode,怎么判断该存到哪个下标下呢?这就容易出现某一个链表特别长,某些链表特别短,不够均匀,所以hashcode的出现是为了让hash表性能更好。
- 可以看出重要的是相同的元素的hash值一定要相同。如果相同的元素进入了不同的下标下,那么就会出问题,这就是依靠具体的算法,使得equals()为true的两个元素的hashcode相等。
接下来看看自动生成的hashcode怎么重写的。
@Override
public int hashCode() {
return Objects.hash(name);
}
调用了Objects类下的hash,注意是Objects,并不是Object。继续跟踪的话,最终调用了Object类下的public native int hashCode();这里就不论述了。涉及到底层的hash算法究竟是怎么实现。
总结:我们需要知道的
- equals和==的区别?
逻辑相等 物理相等 - hashcode和equals的关系?
equals为true的对象,hashcode相等;
hashcode相等,equals不一定为true。 - 他们在hash表中如何互相影响和工作,理解原理过程。