文章目录
- 1. Set 集合概述
- 2. Set 集合常用子类
- 3. HashSet
- 3.1 HashSet 的属性
- 3.2 HashSet 的构造器
- 3.3 add 方法
- 3.4 remove 方法
- 3.5 遍历
- 3.6 是否包含
- 3.7 总结
- 4. LinkedHashSet
- 5. TreeSet
- 6. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
1. Set 集合概述
Set 集合的特点就是:无序,元素不可重复,至多只有一个null值
2. Set 集合常用子类
- HashSet
- (无序,唯一)基于
HashMap
实现的,底层采用HashMap
来保存元素
- LinkedHashSet
-
LinkedHashSet
是HashSet
的子类,并且其内部是通过LinkedHashMap
来实现的。(有点类似于LinkedHashMap
其内部是基于HashMap
实现,不过还是有一点点区别的 )
- TreeSet
- (有序,唯一)红黑树(自平衡的排序二叉树)
以上三个类都是非线程同步。
3. HashSet
类图:
- 实现了
Cloneable
接口,可以克隆- 实现了
Serializable
接口,可以序列化、反序列化- 实现了
Set
接口,是Set
的实现类之一- 实现了
Collection
接口,是Java Collections Framework
成员之一- 实现了
Iterable
接口,可以使用for-each
迭代(但不建议,因为无序)
HashSet 类注释:
从类注释来看,可以归纳 HashSet 的特点:
- 实现 Set 接口
- 不保证迭代顺序
- 允许元素为 null
- 底层实际上是一个HashMap实例(基于哈希表实现)
- 非线程同步
- 初始容量非常影响迭代性能
HashSet 是对 HashMap 的简单包装,对 HashSet 的函数调用都会转换成合适的 HashMap 方法,因此 HashSet 的实现非常简单,只有不到300行代码。
3.1 HashSet 的属性
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;//序列化版本UID
private transient HashMap<E,Object> map;//底层用来存储数据的真实容器map
// Dummy value to associate with an Object in the backing Map
//由于Set只使用到了HashMap的key,所以此处定义一个静态的常量Object类,来充当HashMap的value
private static final Object PRESENT = new Object();
......
3.2 HashSet 的构造器
构造器详解:
private transient HashMap<E,Object> map;
//默认构造器,使用HashMap的默认容量大小16和默认加载因子0.75初始化map,构造一个HashSet
public HashSet() {
map = new HashMap<>();
}
//构造一个指定Collection参数的HashSet,这里不仅仅是Set,只要实现Collection接口的容器都可以
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
//明确初始容量和装载因子的构造器
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
//仅明确初始容量的构造器(装载因子默认0.75)
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//不对外公开的一个构造方法(默认default修饰),底层构造的是LinkedHashMap,dummy只是一个标示参数,无具体意义
HashSet( int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}
通过上面的源码,我们发现了HashSet就像是一些公司,它就对外接活儿,活儿接到了就直接扔给外包 HashMap 处理了。因为底层是通过 HashMap 实现的,这里简单提一嘴:
HashMap的数据存储是通过数组+链表/红黑树实现的,存储大概流程是通过hash函数计算在数组中存储的位置,如果该位置已经有值了,判断key是否相同,相同则覆盖,不相同则放到元素对应的链表中,如果链表长度大于8,就转化为红黑树,如果容量不够,则需扩容(注:这只是大致流程)。
只有最后一个构造方法有写区别,这里构造的是 LinkedHashMap,该方法不对外公开,实际上是提供给 LinkedHashSet 使用的,而第三个参数 dummy 是无意义的,只是为了区分其他构造方法。
3.3 add 方法
HashSet 的 add 方法时通过 HashMap 的 put 方法实现的,不过 HashMap 是 key-value 键值对,而 HashSet 是集合,那么是怎么存储的呢,我们看一下源码 :
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
看源码知道,HashSet 添加的元素是存放在 HashMap 的 key 位置上,而 value 取了默认常量 PRESENT,是一个Object 对象,至于 map 的 put 方法,下面是其大致工作流程:
3.4 remove 方法
HashSet 的 remove 方法通过 HashMap 的 remove 方法来实现
//HashSet的remove方法,形参其实是key,也就是我们存入Set的值
public boolean remove(Object o) {
return map.remove(o)==PRESENT;//如果删除key会返回其对应的value,只要存在这个key,它的value绝对是PRESENT,则结果为真,Set中不存在这个key的话自然为假
}
//map的remove方法
public V remove(Object key) {
Node<K,V> e;
//通过hash(key)找到元素在数组中的位置,再调用removeNode方法删除
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
//真正进行删除元素的逻辑
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//步骤1.需要先找到key所对应Node的准确位置,首先通过(n - 1) & hash找到数组对应位置上的第一个node
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//1.1 如果这个node刚好key值相同,运气好,找到了
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
/**
* 1.2 运气不好,在数组中找到的Node虽然hash相同了,但key值不同,很明显不对,我们需要遍历继续
* 往下找;
*/
else if ((e = p.next) != null) {
//1.2.1 如果是TreeNode类型,说明HashMap当前是通过数组+红黑树来实现存储的,遍历红黑树找到对应node
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//1.2.2 如果是链表,遍历链表找到对应node
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//通过前面的步骤1找到了对应的Node,现在我们就需要删除它了
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
/**
* 如果是TreeNode类型,删除方法是通过红黑树节点删除实现的
*/
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
/**
* 如果是链表的情况,当找到的节点就是数组hash位置的第一个元素,那么该元素删除后,直接将数组
* 第一个位置的引用指向链表的下一个即可
*/
else if (node == p)
tab[index] = node.next;
/**
* 如果找到的本来就是链表上的节点,也简单,将待删除节点的上一个节点的next指向待删除节点的
* next,隔离开待删除节点即可
*/
else
p.next = node.next;
++modCount;
--size;
//删除后可能存在存储结构的调整,可参考【LinkedHashMap如何保证顺序性】中remove方法
afterNodeRemoval(node);
return node;
}
}
return null;
}
3.5 遍历
HashSet作为集合,有多种遍历方法,如普通for循环,增强for循环,迭代器,我们通过迭代器遍历来看一下 :
public static void main(String[] args) {
HashSet<String> setString = new HashSet<> ();
setString.add("星期一");
setString.add("星期二");
setString.add("星期三");
setString.add("星期四");
setString.add("星期五");
Iterator it = setString.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
打印结果:
星期二
星期三
星期四
星期五
星期一
HashSet 是通过 HashMap 来实现的,HashMap 通过 hash(key) 来确定存储的位置,是不具备存储顺序性的,因此 HashSet 遍历出的元素也并非按照插入的顺序。
通过 for-each 遍历 HashSet
第一步: 根据 toArray() 获取 HashSet 的元素集合对应的数组。
第二步: 遍历数组,获取各个元素。
//假设set是HashSet对象,并且set中元素是String类型
String[] arr = (String[])set.toArray(new String[0]);
for (String str:arr)
System.out.printf("for each : %s\n", str);
3.6 是否包含
利用 HashMap 的 containsKey 方法实现 contains 方法
//检查是否包含元素o
public boolean contains(Object o) {
return map.containsKey(o);
}
/**
* 检查是否包含指定集合中所有元素,该方法在AbstractCollection中
*/
public boolean containsAll(Collection<?> c) {
// 取得集合c的迭代器Iterator
Iterator<?> e = c.iterator();
// 遍历迭代器,只要集合c中有一个元素不属于当前HashSet,则返回false
while (e.hasNext())
if (!contains(e.next()))
return false;
return true;
}
由HashMap 基于 hash 表实现,hash 表实现的容器最重要的一点就是可以快速存取,那么 HashSet 对于 contains 方法,利用 HashMap 的 containsKey 方法,效率是非常之快的。
3.7 总结
- HashSet 中元素不可重复
- 允许元素为 null
- 不保证迭代顺序
- 底层实际上是一个 HashMap 实例(基于哈希表实现)
- 非线程同步
- 初始容量非常影响迭代性能
4. LinkedHashSet
类图:
LinkedHashSet 类注释:
总结:
- 迭代是有序的,元素插入和取出顺序满足 FIFO
- 允许为 null
- 底层实际上是一个 HashMap + 双向链表实例(其实就是 LinkedHashMap)
- 非同步
- 性能比 HashSet 稍差,因为要维护一个双向链表
- 初始容量与迭代无关,LinkedHashSet 迭代的是双向链表
5. TreeSet
类图:
TreeSet 的类注释:
总结:
- 实现 NavigableSet 接口
- 可以实现排序功能
- 底层实际上是一个 TreeMap 实例(红黑树)
- 非同步
属性和构造器:
实际上底层是一个 TreeMap 实例,其中的 map 的每个 value 放的是 Object 对象,实际上是用 key 来存储 TreeSet 的值。
6. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
-
HashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,并且都不是线程安全的。 -
HashSet
、LinkedHashSet
和TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是哈希表(基于HashMap
实现)。LinkedHashSet
的底层数据结构是双向链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。 - 底层数据结构不同又导致这三者的应用场景不同。
HashSet
用于不需要保证元素插入和取出顺序的场景,LinkHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。