前面我们已经介绍了Set集合和HashSet类的基本特点和常用方法的使用。前面一篇,我们如何HashSet集合中存储的是字符串的元素,我们可以看到元素打印出来是没有重复的元素。我们知道了HashSet是不允许存储重复的元素,但是不知道是由于什么方法或者机制去实现保证元素的唯一性。下面我们通过HashSet来存储我们自定义的一个类对象,来了解保证元素唯一性的过程和原理。
1.在bean包下,写一个Person类,代码如下。
package bean;
public class Person {
private String name;
private int age;
public Person() {
super();
}
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
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;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
//如果这里不重写父类的equals方法,集合中使用contains方法就无法去重
@Override
public boolean equals(Object obj) {
Person p = (Person)obj;
return this.name.equals(p.name) && this.age == p.age;
}
}
2.HashSet存储Person类
package hashset;
import java.util.HashSet;
import bean.Person;
public class Demo2_HashSet {
public static void main(String[] args) {
HashSet<Person> hs = new HashSet<>();
hs.add(new Person("张三",23));
hs.add(new Person("张三",23));
hs.add(new Person("李四",24));
hs.add(new Person("李四",24));
hs.add(new Person("王五",25));
hs.add(new Person("王五",25));
System.out.println(hs);
}
}
运行输出:
[Person [name=李四, age=24], Person [name=王五, age=25], Person [name=王五, age=25], Person [name=张三, age=23],
Person [name=张三, age=23], Person [name=李四, age=24]]
结果一看,这个我们还是看到了重复的Person对象打印出来,这个和我们前面说的Set集合不允许存重复的元素相矛盾。真的是矛盾吗?我们发现我们添加每个Person对象都使用了new关键字,也就是说两个张三其实不是同一个对象,因为内存地址不同,所以HashSet认为这两个元素不是重复的。但是,有时候,两个张三这样的数据,在业务上看来,就是代表同一个人人,应为张三名称相同,年龄也相同,业务上就是认为同一条数据。下面我们就来介绍如何改变自定义类Person来使HashSet保证存储的元素的唯一性。
3.分析一下
我在Person类代码中重写了equals方法,如果不写,使用集合中的contains方法就不能判断两个名称为张三而且年龄一样的人不是同一个业务数据。但是这个重写的equals方法,在HashSet添加元素过程中,并没有执行。所以,我们是不是需要在Person这个自定义类里面需要重写父类Object类的某一方法。联想到HashSet底层是通过哈希算法实现的,刚好Object这个类有一个hashCode()的方法,是不是我们需要在Person类中重写hashCode方法就能实现去重呢?下面我们来试试。
4.在Person类重写hashCode方法
package bean;
public class Person {
private String name;
private int age;
public Person() {
super();
}
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
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;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
//如果这里不重写父类的equals方法,集合中使用contains方法就无法去重
@Override
public boolean equals(Object obj) {
Person p = (Person)obj;
return this.name.equals(p.name) && this.age == p.age;
}
@Override
public int hashCode() {
//return super.hashCode(); //父类的hashCode方法
return 10; //重写,不使用hash算法直接指定hashCode值为10
}
}
注意上面重写的hashCode方法
package hashset;
import java.util.HashSet;
import bean.Person;
public class Demo2_HashSet {
public static void main(String[] args) {
HashSet<Person> hs = new HashSet<>();
hs.add(new Person("张三",23));
hs.add(new Person("张三",23));
hs.add(new Person("李四",24));
hs.add(new Person("李四",24));
hs.add(new Person("王五",25));
hs.add(new Person("王五",25));
System.out.println(hs);
}
}
上面的测试类并没有修改任何代码,执行以下:
[Person [name=张三, age=23], Person [name=李四, age=24], Person [name=王五, age=25]]
结果,发现HashSet打印结果果然进行了元素去重,保证了元素的唯一性。
5.底层实现原理
上面为什么我们重写了hashCode方法就能保证HashSet存储不重复的元素呢?原理是这样的,在HashSet存储中,会自动率先调用hashCode方法,如果两个对象的hashCode值是一样,才会调用equals方法,hashCode值不一样,不会调用equals方法去比较,直接存入HashSet集合。
上面我们制定了每个Person创建的对象的hashCode的值都是10,第一个添加进来的对象是张三这个元素,它的hashCode值是10,发现HashSet集合中暂时没有其他对象的hashCode的值是10,所以第一个张三就直接存储进入HashSet。当执行第二个张三添加进集合的时候,发现已经有一个对象的hashCode是10,所以,会调用equals方法,进行两个张三对象的对比。而且上面我们重写了equals方法,所以比较之后,认为是同一个对象,故第二个重复的张三就不会存储进入HashSet集合。执行添加第三个对象,是李四,虽然李四和张三的hashCode都是10,但是比较之后,名称和年龄不同,认为是两个对象,所以李四存储到HashSet。同理,李四和王五的重复对象没有添加进来就是这个道理。
6.代码优化
上面的原理,我们可以推理出这么一个结论:如果保证了对象的hashCode值不同,那么就不用去调用equals方法,这样效率就更高。上面我们制定了Person实例对象hashCode是10,new了六次,结果对比了6次,如果new了几千上万个,那么调用equals方法的次数也很多,效率可能就不是很好。下面,我们通过IDE编辑器上自动化生成的重写equals和hashCode方法来代替我们上面重写的方法。
package bean;
public class Person {
private String name;
private int age;
public Person() {
super();
}
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
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;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
@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;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) 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;
}
}
所以,以后我们在自定义对象,重写Object的equals和hashCode方法就使用IDE上自动快速生成的代码。