大家都知道,HashSet这个容器是具有元素唯一性的,那我们就举一个例子验证一下:
public class Test {
public static void main(String args[]) {
HashSet<Person> hs = new HashSet<>();
hs.add(new Person(28, "王长贵"));
hs.add(new Person(35, "谢永强"));
hs.add(new Person(23, "赵玉田"));
hs.add(new Person(23, "赵玉田"));
Iterator<Person> it = hs.iterator();
while(it.hasNext()) {
Person p = it.next();
System.out.println(p.getName() + p.getAge() + "岁");
}
}
}
class Person{
String name;
int age;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
在主方法的下面我们创建了一个类Person并设置了名字和年龄的构造方法还有getter、setter方法,之后在主方法里面创建了一个HashSet容器并在里面添加了几个对象(赵玉田是重复的),最后迭代打印出来,运行结果如下:
诶?为啥容器里面有俩玉田呢?要弄清楚这个问题,还要从HashSet存储流程说起,下面简单的介绍一下哈希表存储的原理:
简单说,哈希表里面还是一个数组,那数组要怎么查找呢?遍历(就是一个一个比对),这种方法是较慢的。为了解决这个问题,哈希表在存储数据的时候,是根据元素自身的特性来计算出一个位置,接着把这个元素存到那个位置里面去。这样做的好处是,当查询一个元素时,我们只需要再次计算出它的位置,之后直接到那个位置去找就可以了(不用一个一个查了),这种计算的方法就是哈希算法。
那么HashSet存储的流程就是:比如说现在往HashSet里面存一个对象,那么首先就会由哈希算法(也就是hashCode方法)计算出一个位置,如果这个位置已经有元素了,再通过对象的equals来判断内容是否相同。相同就不存了,这就是唯一性的原理。否则两个元素就是不同的,不同的元素有通过哈希算法计算出的位置相同,这种情况叫做哈希冲突,怎么解决冲突并不是我们要关心的,人家自有解决的办法。画一张图总结一下:
那么再回到最开始的那个问题,我们自定义的类Person是继承自Object的,Object有自己的hashCode方法,也有自己的equals方法,而Object的equals方法只是简单判断了两个对象的地址是否相同。我们知道,不同对象地址是不同的。暂且不说它们的hashCode是否相同,地址不同的话HashSet就将它们视为不重复了,我们创建了五个Person对象,五个地址都不相同,那么该存还得存,所以取出来的时候还是五个(赵玉田有两个)。
换一个角度去想这个问题,我们判断两个人是否相同的依据是姓名和年龄是否都相同,但是计算机不知道这个依据,它是按照自己的原则去判断的,所以会出现这样的结果。那么能否让计算机也按我们指定的规则去判断呢?
当然是可以的,根据上面讲述的原理,我们只需要重写Person类的hashCode和equals方法即可。
@Override
public int hashCode() {
System.out.println("计算 " + name + " 的位置");
return name.hashCode() + age*39; //尽量保证哈希值唯一所以年龄乘以一个数
}
@Override
public boolean equals(Object arg0) {
System.out.println("比较 " + name + " 的内容");
if(arg0 == this) {
//如果待比较的对象是它自己,直接返回true即可
return true;
}
if(!(arg0 instanceof Person)) {
//增加程序健壮性
throw new ClassCastException("类型错误");
}
Person p = (Person) arg0;
//如果姓名年龄都一样则视为一个人
return p.getAge()==age && p.getName().equals(name);
}
在上面的代码中,hashCode方法里返回的哈希值是根据年龄和姓名特点计算出来的,equals方法中我们自己创建了比较的规则,当然了如果待比较的对象就是自己的话,那就不用比直接返回true即可;此外在这两个方法中都打印了一下来验证方法是否被执行。
再次运行程序,结果如下:
可以看到,每次调用add方法都会调用hashCode方法来计算位置,而当位置相同时(两个23岁的赵玉田hashCode计算结果相同)才会调用equals来比较内容,比较发现内容一样(这是我们自己定的比较规则)所以不存,故最后容器中只有3个元素,这样便实现了自定义的存放规则。
讲完了这个例子之后,我们最后再来看一个小插曲:
HashSet<String> hs = new HashSet<>();
hs.add(new String("刘能"));
hs.add(new String("王大拿"));
hs.add(new String("刘大脑袋"));
hs.add(new String("王天来"));
hs.add(new String("王天来"));
Iterator<String> it = hs.iterator();
while(it.hasNext()) {
String str = it.next();
System.out.println(str);
}
这个例子里面在HashSet里面添加了五个用new关键字创建的字符串,其中有两个字符串都是王天来,运行结果如下:
诶?为啥只有4个呢?刚刚不是说不同对象地址不同吗,那么计算机应该将两个王天来其视为不重复阿,为啥容器里只有四个字符串呢?
别傻了,其实在String的源码中重写了equals方法:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = length();
if (n == anotherString.length()) {
int i = 0;
while (n-- != 0) {
if (charAt(i) != anotherString.charAt(i))
return false;
i++;
}
return true;
}
}
return false;
}
可以看到是一个字符一个字符的比对,所以String的equals方法返回的是两个字符串的值是否相等(也就是String自己定义的规则),这回总算是真相大白了,那么关于HashSet重复性的问题就说到这里了,希望能给大家带来帮助,同时也欢迎大佬们在评论区留言批评指正。