Set集合
java.util.Set
接口和 java.util.List
接口一样,都继承类 Collection
接口,它与 Collection接口的方法基本一致,并没有对Collection集合进行功能的扩展。与 List集合不同,Set集合的特点是 元素无需且不重复。
List集合:元素有序,元素可以重复,可以通过索引值访问集合中的元素。底层维护了一个数组,特点与数组一样。
Set集合:元素无需,且元素不可重复,它的底层使通过 HashMap实现的,通过 hashCode和equals方法达到元素不可重复的功能。
Set的实现类
Set集合有多个子类,我们常用的有 java.util.HashSet
和 java.util.LinkedHashSet这两个集合。
HashSet集合介绍
java.util.HashSet
是Set
接口的一个实现类,它所存储的元素是无序且不可重复的,java.util.HashSet
底层实现是一个java.util.HashMap
。HashSet根据对象的哈希值来确定元素在集合中的位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于它的hashCode和equals方法。
下面我们来看一段代码,通过代码进行原理分析:
Set<String> set = new HashSet<String>();
// 向HashSet集合中添加元素,其中元素"123"被添加了两次
set.add("123"); // true
set.add("231"); // true
set.add("312"); // true
set.add("123"); // false
for (String str : set) {
System.out.println(str); // 输出结果:231,123,312
}
在上面的代码中,我们创建了一个用来存储 String字符串的HashSet集合,我们向集合中添加类两次"123",但是在我们遍历集合中元素时发现,set集合中只存在1个"123",这是因为set集合不允许出现重复的元素,如果出现重复元素,则元素添加失败。
HashSet源码解读
下面我们来看看 HashSet的源码,它的add方法底层是通过 HashMap.put方法实现的,有兴趣的同学可以自行去阅读更深层次的源码。
// HashSet底层维护的是一个 HashMap对象
private transient HashMap<E,Object> map;
// HashSet的构造方法内部创建了一个HashMap对象
public HashSet() {
map = new HashMap<>();
}
/**
* 向HashSet集合中添加元素
*
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set if
* this set contains no element <tt>e2</tt> such that
* <tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* 如果指定的元素不存在,则将其添加到此集合中。更正式地,如果集合中不包含e2元素,则添加指定的
* 元素e,使(e==null ? e2==null e==null e.equals(e2))。如果该集合已经包含该元素,则调用
* 保持该集合不变并返回false。。
*
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
在详细说明add方法的执行原理之前,我们先来了解下什么HashSet的存储结构。
哈希表
哈希表是 HashSet存储数据的存储结构,哈希表分为两个版本:
- JDK8之前,哈希表 = 数组+链表
- JDK8之后,哈希表 = 数组+链表 或 数组+红黑树
哈希冲突:两个或以上的元素拥有相同的哈希值,这就产生了哈希冲突。
在 JDK8之前,哈希表底层采用数组+链表的方式实现,也就是使用链表来解决哈希冲突,同一hash值的元素都存储在同一个链表中,但是当一个链表中的元素较多后,通过 key依次查找的效率会降低。
所以在JDK8中,添加类数组+红黑树的实现方式,当链表的长度超过8时,就会将链表转换成红黑树结构,红黑树的特点就是查询速度快,它是一半一半的查找的;将链表转换成红黑树,这样就大大减少了查找时间。
下面我们来看看 哈希表结构 的存储方式:
HashSet存储原理图(HashMap底层)
HashSet添加元素的实现原理
HashSet集合的add方法存储原理(以JDK8为例):
1、首先创建 HashSet集合时,初始化一个长度16的数组,数组中的每个元素都是一个链表结构。
2、执行 set.add(s1) 这行代码时:
- add方法调用 s1的 hashCode()方法计算字符串"abc"的 hash值,hash值是 96354;
- 然后在集合中查找有没有 96354这个哈希值,发现没有,那么就会直接将s1存储到集合中,并返回true。
3、执行 set.add(s2) 这行代码时:
- add方法调用 s2的 hashCode()方法计算字符串"abc"的 hash值,hash值是 96354;
- 然后在集合中查找有没有 96354这个哈希值,发现集合中已经存在这个hash值,这时就产生 Hash冲突了
- 然后 s2会调用equals方法,与哈希值是 96354的元素进行内容的比较 s2.equals(s1),返回true,HashSet集合就认为这两个元素相同,就不会将s2存储到集合中,并且返回false。
4、执行 set.add("重地") 这行代码时:
- add方法调用 “重地”的 hashCode()方法计算字符串"重地"的 hash值,hash值是1179395;
- 然后在集合中查找有没有 1179395这个哈希值,发现没有,那么就会直接将 “重地”存储到集合中,并返回true。
5、执行 set.add("通话") 这行代码时:
- add方法调用 "通话" 的 hashCode()方法计算字符串"通话"的 hash值,hash值是 1179395;
- 然后在集合中查找有没有1179395这个哈希值,发现集合中已经存在这个hash值,这时就产生 Hash冲突了
- 然后 通话“”会调用equals方法,与哈希值是 1179395的元素"重地"进行内容的比较 "重地".equals("通话"),返回false,HashSet集合就认为这两个元素不相同,就会将"通话"存储到集合中,并且返回true。
HashSet存储内容不重复的元素,前提是这个元素重写了hashCode和equals方法。
LinkedHashSet
HashSet的特点是:存储的元素不重复并且没有顺序。当我们的需求是要保证存储顺序该怎么办呢?下面我们就了解下LinkedHashSet。
java.util.LinkedHashSet
,它是链表和哈希表组合的一个数据存储结构。
LinkedHashSet 存储结构:哈希表(数组+链表/红黑树)+ 链表;它比HashSet集合的存储结构多了一条链表,这条链表是用来记录储存顺序的,这样就保证了集合的有序性。
演示代码如下:
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>();
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
结果:
bbb
aaa
abc
bbc