HashSet类之唯一特性
一、Set集合概述
一个不包含重复元素的 collection。更确切地讲,set 不包含满足 e1.equals(e2)
的元素对 e1
和 e2
,并且最多包含一个 null 元素。正如其名称所暗示的,此接口模仿了数学上的 set 抽象。
Set集合实现了Collection接口,但没有更多新的方法,只是在所有构造方法以及 add、equals 和 hashCode 方法的协定上,Set 接口还加入了其他规定,这些规定超出了从 Collection 接口所继承的内容。这些规定是为了实现让Set集合不包含重复元素,所以我们值需要研究Set集合是如何保持元素唯一特性即可。
二、Set集合特点
无索引,不可以重复,无序(存取不一致)
public static void demo1() {
HashSet<String> hs = new HashSet<>(); //创建HashSet对象
boolean b1 = hs.add("a");
boolean b2 = hs.add("a"); //当向set集合中存储重复元素的时候返回为false
hs.add("b");
hs.add("c");
hs.add("d");
System.out.println(hs); //HashSet的继承体系中有重写toString方法
System.out.println(b1); //true
System.out.println(b2); //false
for (String string : hs) { //只要能用迭代器迭代的,就可以使用增强for循环遍历
System.out.println(string);
}
}
三、HashSet集合概述
此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。
private transient HashMap<E,Object> map;
注意,此实现不是同步的。
四、HashSet集合保持唯一特性
由于Set集合是不包含重复值的,如果我们在存储每一个元素时都是用equals()方法进行比较,则效率较低。HashSet集合采用Hash算法提高了去重复的效率,降低了equals()方法的使用频率,具体比较如下:
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该值确定对象在HashSet中的存储位置。
在Hash集合中,不能同时存放两个相等的元素,而判断两个元素相等的方法如下:
- 如果hashCode值不相同,说明是一个新元素,则存入集合中。
- 如果hashCode码值相同,但是equles()方法判断不相等,说明是一个新元素,,则存入集合中。
- 如果hashCode码值相同,且equles()方法判断相等,说明元素已经存在,则不存入集合;
对于hashCode()和equles()方法,我在(https://blog.csdn.net/qq_40298054/article/details/83622837)一文中作了详细介绍。
例如:对象相等,hashCode返回值相等;对象不相等,hashCode返回值不一定不相等,如果有兴趣的话可以进行阅读。
所有由上可见,类中的hashCode()和equles()方法十分重要,如果需要提高性能的话,那么优化hashCode()算法以减少equles()进行比较的次数对于优化性能是十分重要的(即在创建的Hash表中减少冲突)。
案例:
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;
}
@Override
public boolean equals(Object obj) {
Person p = (Person)obj;
return this.name.equals(p.name) && this.age == p.age;
}
@Override
public int hashCode() {
final int NUM = 38;
//String类重写了hashCode()方法,我们可以直接使用
//定义一个参数是为了尽量减少hashCode值重复的几率
return name.hashCode() * NUM + age;
}
}
其实在开发中,我们不需要费力重写这两个方法,编译器已经为我们重写完成,我们只需要直接选择生成即可。
/*
* 为什么是31?
* 1,31是一个质数,质数是能被1和自己本身整除的数
* 2,31这个数既不大也不小
* 3,31这个数好算,2的五次方-1,2向左移动5位
*/
@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; //直接返回true
if (obj == null) //传入的对象为null
return false; //返回false
if (getClass() != obj.getClass()) //判断两个对象对应的字节码文件是否是同一个字节码
return false; //如果不是直接返回false
Person other = (Person) obj; //向下转型
if (age != other.age) //调用对象的年龄不等于传入对象的年龄
return false; //返回false
if (name == null) { //调用对象的姓名为null
if (other.name != null) //传入对象的姓名不为null
return false; //返回false
} else if (!name.equals(other.name)) //调用对象的姓名不等于传入对象的姓名
return false; //返回false
return true; //返回true
}
我们可以看出,系统为我们生成的两个方法十分复杂,这是因为为了程序的健壮性考虑。程序中包含的判断越多,则出现故障的概率越小。我们知道印度的计算机行业比较发达,就是因为他们编写程序时考虑的比较周全,出现的错误较少。所以我们在以后编写代码时尽量注意这一点。
五、LinkedHashSet
具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。
此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目的双重链接列表(底层链表实现)。此链接列表定义了迭代顺序,即按照将元素插入到 set 中的顺序(插入顺序)进行迭代(是set集合中唯一一个能保证怎么存就怎么取的集合对象)。注意,插入顺序不 受在 set 中重新插入的 元素的影响。
因为该类是HashSet的子类,所以也是保证元素唯一的,与HashSet的原理一样。