前言
Set接口的实现类HashSet的特点是无序不可重复。
底层实现:
JDK 8.0 之前,数组 + 链表
JDK 8.0 及之后,数组 + 链表 + 红黑树
那么向HashSet集合中添加元素时,它是如何判断该元素是否重复呢?
下面用代码举例来演示其原理。
代码实现
首先,创建一个实体类User:
public class User {
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUserName() {
return username;
}
public void setUserName(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((username == null) ? 0 : username.hashCode());
result = prime * result + ((password == null) ? 0 : password.hashCode());
// 验证调用了hashCode并获取哈希值
System.out.println("------" + result);
return result;
}
@Override
public boolean equals(Object obj) {
// 验证是否调用了equals方法
System.out.println("------ equals方法");
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
return true;
}
@Override
public String toString() {
return "User [username=" + username + ", password=" + password + "]";
}
}
在上面的 hashCode() 和 equals() 方法中,分别做了标记,如果调用了这两个方法,就会在控制台打印出对应的标记,这样我们就可以知道 HashSet 在添加用户时判断重复的原理。
下面,创建HashSet集合,并添加元素(包括重复元素):
public class HashSetDemo {
public static void main(String[] args) {
Set<User> users = new HashSet<>();
User u1 = new User("张三", "123456");
User u2 = new User("李四", "654321");
User u3 = new User("张三", "123456");
users.add(u1);
users.add(u2);
users.add(u3);
users.forEach(System.out::println);
}
}
运行结果如下:
------1474597979
------1622625361
------1474597979
------ equals方法
User [username=李四, password=654321]
User [username=张三, password=123456]
从打印结果可以看出,前三行的三串数字分别是添加的三个元素对应的哈希值(hashCode值),说明 Set 集合通过调用 hashCode() 方法获取哈希值来初步判断当前元素是否与之前元素重复(可能产生误差,但一般几率非常非常小)。然后发现第三个元素与第一个元素的哈希值相等,在调用 equals() 方法来进一步精确判断两个元素是否相同,相同则判定为重复,所以最终结果没有重复元素。
总结
通过以上的过程发现 HashSet 集合不可重复的原理:
当第一个元素添加到 Set 集合中时,会优先调用该对象的 hashCode() 方法得到哈希值,根据这个哈希值来决定该对象存放在集合的哪个位置。
当第二个元素添加到 Set 集合中时,会继续调用该对象的 hashCode() 方法得到哈希值,此时会有两种情况:
- 哈希值不同,说明是不同的对象,则根据这个哈希值来决定新元素存放的位置。
- 哈希值相同,此时还不能说明是相同的对象,所以会继续调用 equals() 方法。
如果 equals() 方法返回 true,说明是相同对象,则该元素将不被添加到集合中;如果 equals() 方法返回 false,说明这两个对象虽然哈希值一样,但是是不同的对象,则在该哈希值对应的位置产生链表。
补充
JDK 8.0 之前,只要产生碰撞,就在链表上加一个元素,随着元素增多,HashSet 的性能降低。
JDK 8.0 及之后,当碰撞阈值达到 8 时,自动把链表转换成红黑树,目的是为了提高 HashSet 的性能。