HashSet
1.关于HashCode()和equals()的重写//原文链接:https://blog.csdn.net/javazejian/article/details/51348320
2.HashSet中元素的添加过程//来源尚硅谷教学
1.关于HashCode()和equals()的重写
equals()的所属以及内部原理(即Object中equals方法的实现原理)
equals方法是类Object中的一个基本方法,用于检测一个对象是否和另外一个对象相等。然而在Object类中这个方法是判断两个对象是否具有相同的引用,如果有他们就一定相等。
public boolean equals(Object obj) { return (this == obj); }
所有的对象都有内存地址和状态同时“==”比较两个对象的的内存地址,所以说 Object 的 equals() 方法是比较两个对象的内存地址是否相等,即若 object1.equals(object2) 为 true,则表示 equals1 和 equals2 实际上是引用同一个对象。
equals()与‘==’的区别
或许这是我们面试时更容易碰到的问题”equals方法与‘’运算符有什么区别?“,并且常常我们都会胸有成竹地回答:“equals比较的是对象的内容,而‘’比较的是对象的地址。”。但是从前面我们可以知道equals方法在Object中的实现也是间接使用了‘==’运算符进行比较的,所以从严格意义上来说,我们前面的回答并不完全正确。我们先来看一段代码并运行再来讨论这个问题。
package com.zejian.test;
public class Car {
private int batch;
public Car(int batch) {
this.batch = batch;
}
public static void main(String[] args) {
Car c1 = new Car(1);
Car c2 = new Car(1);
System.out.println(c1.equals(c2));
System.out.println(c1 == c2);
}
}
运行结果:false false
分析:对于等等 运算符比较两个Car对象,返回了false,这点我们很容易明白,毕竟它们比较的是内存地址,而c1与c2是两个不同的对象,所以c1与c2的内存地址自然也不一样。现在的问题是,我们希望生产的两辆的批次(batch)相同的情况下就认为这两辆车相等,但是运行的结果是尽管c1与c2的批次相同,但equals的结果却反回了false.
当然对于equals返回了false,我们也是心知肚明的,因为equal来自Object超类,访问修饰符为public,而我们并没有重写equal方法,故调用的必然是Object超类的原始方equals方法,根据前面分析我们也知道该原始equal方法内部实现使用的是 "=="运算符,所以返回了false。因此为了达到我们的期望值,我们必须重写Car的equal方法,让其比较的是对象的批次(即对象的内容),而不是比较内存地址,于是修改如下:
@Override
public boolean equals(Object obj) {
if (obj instanceof Car) {
Car c = (Car) obj;
return batch == c.batch;
}
return false;
}
使用instanceof来判断引用obj所指向的对象的类型,如果obj是Car类对象,就可以将其强制转为Car对象,然后比较两辆Car的批次,相等返回true,否则返回false。当然如果obj不是 Car对象,自然也得返回false。我们再次运行:
true false
总结:默认情况下也就是从超类Object继承而来的equals方法与‘==’是完全等价的,比较的都是对象的内存地址,但我们可以重写equals方法,使其按照我们的需求的方式进行比较,如String类重写了equals方法,使其比较的是字符的序列,而不再是内存地址。
这也就是为什么hashset添加元素的时候需要重写.equals()方法。
equals()的重写规则(五条)
为什么重写equals()的同时还得重写hashCode()
在java中,我们可以使用hashCode()来获取对象的哈希码,其值就是对象的存储地址,这个方法在Object类中声明,因此所有的子类都含有该方法。那我们先来认识一下hashCode()这个方法吧。hashCode的意思就是散列码,也就是哈希码,是由对象导出的一个整型值,散列码是没有规律的,如果x与y是两个不同的对象,那么x.hashCode()与y.hashCode()基本是不会相同的,下面通过String类的hashCode()计算一组散列码
package com.zejian.test;
public class HashCodeTest {
public static void main(String[] args) {
int hash=0;
String s="ok";
StringBuilder sb =new StringBuilder(s);
System.out.println(s.hashCode()+" "+sb.hashCode());
String t = new String("ok");
StringBuilder tb =new StringBuilder(s);
System.out.println(t.hashCode()+" "+tb.hashCode());
}
}
运行结果:
3548 1829164700
3548 2018699554
在Java API文档中关于hashCode方法有以下几点规定(原文来自java深入解析一书):
1.在java应用程序执行期间,如果在equals方法比较中所用的信息没有被修改,那么在同一个对象上多次调用hashCode方法时必须一致地返回相同的整数。如果多次执行同一个应用时,不要求该整数必须相同。
2.如果两个对象通过调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。
3.如果两个对象通过调用equals方法是不相等的,不要求这两个对象调用hashCode方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生不同的hash值可以提供哈希表的性能。
通过前面的分析,我们知道在Object类中,hashCode方法是通过Object对象的地址计算出来的,因为Object对象只与自身相等,所以同一个对象的地址总是相等的,计算取得的哈希码也必然相等,对于不同的对象,由于地址不同,所获取的哈希码自然也不会相等。因此到这里我们就明白了,如果一个类重写了equals方法,但没有重写hashCode方法,将会直接违法了第2条规定,这样的话,如果我们通过映射表(Map接口)操作相关对象时,就无法达到我们预期想要的效果。如果大家不相信, 可以看看下面的例子:
package com.zejian.test;
import java.util.HashMap;
import java.util.Map;
public class MapTest {
public static void main(String[] args) {
Map<String,Value> map1 = new HashMap<String,Value>();
String s1 = new String("key");
String s2 = new String("key");
Value value = new Value(2);
map1.put(s1, value);
System.out.println("s1.equals(s2):"+s1.equals(s2));
System.out.println("map1.get(s1):"+map1.get(s1));
System.out.println("map1.get(s2):"+map1.get(s2));
Map<Key,Value> map2 = new HashMap<Key,Value>();
Key k1 = new Key("A");
Key k2 = new Key("A");
map2.put(k1, value);
System.out.println("k1.equals(k2):"+s1.equals(s2));
System.out.println("map2.get(k1):"+map2.get(k1));
System.out.println("map2.get(k2):"+map2.get(k2));
}
/**
* 键
* @author zejian
*
*/
static class Key{
private String k;
public Key(String key){
this.k=key;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Key){
Key key=(Key)obj;
return k.equals(key.k);
}
return false;
}
}
/**
* 值
* @author zejian
*
*/
static class Value{
private int v;
public Value(int v){
this.v=v;
}
@Override
public String toString() {
return "类Value的值-->"+v;
}
}
}
s1.equals(s2):true
map1.get(s1):类Value的值-->2
map1.get(s2):类Value的值-->2
k1.equals(k2):true
map2.get(k1):类Value的值-->2
map2.get(k2):null
对于s1和s2的结果,我们并不惊讶,因为相同的内容的s1和s2获取相同内的value这个很正常,因为String类重写了equals方法和hashCode方法,使其比较的是内容和获取的是内容的哈希码。但是对于k1和k2的结果就不太尽人意了,k1获取到的值是2, k2获取到的是null,这是为什么呢?想必大家已经发现了,Key只重写了equals方法并没有重写hashCode方法,这样的话,equals比较的确实是内容,而hashCode方法呢?没重写,那就肯定调用超类Object的hashCode方法,这样返回的不就是地址了吗?k1与k2属于两个不同的对象,返回的地址肯定不一样,所以现在我们知道调用map2.get(k2)为什么返回null了吧?那么该如何修改呢?很简单,我们要做也重写一下hashCode方法即可(如果参与equals方法比较的成员变量是引用类型的,则可以递归调用hashCode方法来实现):
@Override
public int hashCode() {
return k.hashCode();
}
2.关于HashSet的添加过程
HashSet是set接口的典型实现,按照hash算法来存储集合中的元素,因此具有很好的存储,查找,删除的性能。
HashSet的性能以及特点
特点:HashSet不是线程安全的,集合元素可以是null。其底层还是数组,初始容量是16,如果使用率超过0.75,就会扩大容量为原来的2倍。无序性以及不可重复性
判断两个元素相等的标准:两个对象通过hashcode()方法比较相等,并且两个对象的equals()方法返回值也相等。(需要重写equals()方法以及hashcode方法)
HashSet元素的添加过程
我们向HashSet中添加元素a的过程:
首先调用元素a所在类的hashcode方法,计算元素的hash值——通过hash值计算元素a的存放位置,判断数组此位置上是否已经有其他元素——如果此位置上没有其他元素则元素a添加成功,如果此位置上有其他元素(或者以链表形式存在多个元素),则比较a和此位置元素的hash值如果hash值不同则元素添加成功,如果hash值相同则需要调用元素a的equals方法,如果返回ture则添加失败,如果返回false 则添加成功。