什么是 HashSet?
HashSet 是存在 java.util 包中的类,是 Java 中常用的一个集合类,是 Set 接口的实现类,该容器中不能存储重复的对象。对于 HashSet 来说,它是基于 HashMap 实现的,底层采用 HashMap 来存储元素,关于 HashMap 的文章请看这里 深入理解 HashMap
HashSet 的继承类与实现的接口有哪些?
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
}
从上面的代码中可以知道 HashSet 继承了AbstractSet 类并且实现了 Set、Cloneable、Serializable 接口
HashSet 中的主要成员变量
// 序列ID
static final long serialVersionUID = -5024744406713321676L;
// HashSet 内部使用 HashMap 存储
private transient HashMap<E,Object> map;
// 虚拟对象,用来作为 value 存储到 map 中
private static final Object PRESENT = new Object();
HashSet中的构造函数
一、HashSet()
无参的 HashSet()
// 当调用该构造函数的时候,其实就是实例化了 HashMap 对象
public HashSet() {
// 实例化
map = new HashMap<>();
}
二、HashSet(Collection<? extends E> c)
// 把另一个集合的元素全部添加到当前的Set中
public HashSet(Collection<? extends E> c) {
// 这里初始化的 Map 计算了它的初始容量
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
三、HashSet(int initialCapacity, float loadFactor)
相信看过 深入理解 HashMap 朋友看到这里是不是有种似曾相识的感觉?没错,就是 HashMap 中的 initialCapacity 和 loadFactor
// 指定初始容量和加载因子
public HashSet(int initialCapacity, float loadFactor) {
// initialCapacity(对应HashMap中的初始容量), loadFactor(对应HashMap中的加载因子)
map = new HashMap<>(initialCapacity, loadFactor);
}
四、HashSet(int initialCapacity)
// 指定初始容量构造一个 HashSet
public HashSet(int initialCapacity) {
// initialCapacity 对应 HashMap中的初始容量
map = new HashMap<>(initialCapacity);
}
五、HashSet(int initialCapacity, float loadFactor, boolean dummy)
// 以指定的参数构造一个新的空链接哈希集合
// initialCapacity:初始容量
// loadFactor:加载因子
// dummy:标记
// 非public修饰,主要是给 LinkedHashSet 使用的
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
// 底层以指定的参数化构造一个 LinkedHashMap 实例来实现
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
HashSet 中常用的方法有哪些?
isEmpty()
如果该集合不包含任何元素,则返回 true
public boolean isEmpty() {
// 实际调用的是 HashMap 的 isEmpty 方法判断 HashSet 是否为空
return map.isEmpty();
}
contains(Object o)
contains 判断 HashSet 中是否包含指定元素,有的话则返回 true
public boolean contains(Object o) {
// 底层实际调用的是 HashMap 的containsKey 来判断是否包含指定的 key,如果包含指定元素则返回 true
return map.containsKey(o);
}
add(E e)
如果向 HashSet 中添加一个已存在的元素时,新添加的集合元素将不会被存放到 HashMap 中 ,原来的元素也不会有任何改变,也就说明了 HashSet 不能有重复元素的特性
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
add() 方法中的 put 操作实际上是 HashMap的 put 操作,而 put 操作返回的是 putVal
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
关于 hash() 和 putVal() 方法的详细内容请点击这里 深入理解 HashMap ,这里不再讲述
说明:大家可以发现 HashSet 每次放入的值都会被当做key来处理,然后通过 HashMap 中的 put() 方法采用遍历单向链表的形式,对于使用相同的 key 和 相同的 key 生成的 hash值 ,那么就使用指定的值替换旧的值
remove(Object o)
如果指定的元素存在 set 中,则移除
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
iterator()
// 获取 HashSet 的迭代器,返回元素的顺序不是特定的
public Iterator<E> iterator() {
// 底层调用 HashMap 的 keySet 来返回所有的 key
return map.keySet().iterator();
}
通过上面的代码可以知道 HashSet 中的元素是存放在底层的 HashMap 的 key 上的,可以发现它遍历的是 keySet,HashMap 中的 key 键重复的话就更新该值,可以保证 key 是不重复的,也就是说 HashSet 添加的值是不会重复的
keySet()
public Set<K> keySet() {
// AbstractMap 中的 keySet;
Set<K> ks = keySet;
// 如果key的集合为空
if (ks == null) {
// 调用 keySet 方法进行初始化
ks = new KeySet();
// 赋值
keySet = ks;
}
return ks;
}
size()
返回集合中的元素的数量
public int size() {
return map.size();
}
clear()
调用 HashMap 的 clear() 方法清空 Node 中的所有节点
public void clear() {
map.clear();
}
总结
为什么HashSet不允许出现重复值?
set 是线性结构,set 中的值不能重复,hashset 是 set 的 hash 实现,因为 HashSet 的内部是使用 HashMap 的 key来存储元素的
public class HashSetDemo {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("hello");
set.add("hello");
System.out.println("set ------- " + set);
List<String> list = new ArrayList<>();
list.add("hello");
list.add("hello");
System.out.println("list ------ " + list);
}
}
输出:
set ------ [hello]
list ------ [hello, hello]
再举个例子
public class HashSetDemo {
public static void main(String[] args) {
Random random = new Random();
Set<Integer> set = new HashSet<>();
// 生成 100 个20以内的数字
for (int i = 0; i < 100; i++) {
// 添加到set中
set.add(random.nextInt(20));
}
System.out.println(set);
}
}
输出:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
根据输出内容我们可以知道100 个 20 以内的数字中没有重复的
HashSet 可以存储为 null 元素吗?
可以,因为 HashSet 底层是由 HashMap实现的,HashMap 允许 key 为 null,所以 HashSet 固然也可以
public class HashSetDemo {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add(null);
System.out.println(set);
}
}
输出:
null
HashSet 到底是有序的还是无序的?
HashSet 是无序的,因为 HashMap 的 key 是无序的,而 HashSet 的底层实现是 HashMap,所以 HashSet 是无序的
有序跟无序怎么理解?
答:如果插入内容的顺序与输出内容的顺序一致表示是有序的,否则是无序的
拿上面的例子继续探讨
public class HashSetDemo {
public static void main(String[] args) {
Random random = new Random();
Set<Integer> set = new HashSet<>();
for (int i = 0; i < 100; i++) {
set.add(random.nextInt(20));
}
System.out.println(set);
}
}
输出:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
谁?谁说是无序的?这不是很明显的是有序的嘛,执着的你们肯定也觉得很奇怪,metoo,OK,继续探讨
再看下另外一个例子
public class HashSetDemo {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);
set.add(4);
set.add(5);
for (Integer integer : set) {
System.out.print(integer + " ");
}
}
}
输出:
1 2 3 4 5
还是有序的呀,到底怎么回事?
故意换一下数字的位置再试试
public class HashSetDemo {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(3);
set.add(2);
set.add(4);
set.add(5);
for (Integer integer : set) {
System.out.print(integer + " ");
}
}
}
输出:
1 2 3 4 5
OK,这回是无序的了,再试一个
public class HashSetDemo {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
set.add(5);
set.add(4);
set.add(3);
set.add(2);
set.add(1);
for (Integer integer : set) {
System.out.print(integer + " ");
}
}
}
输出:
1 2 3 4 5
OK,你们要的无序来了
那么第一个例子那个是怎么回事?说明如下:
说明: 由于我们所创建的 HashSet 是 Integer 类型的,这也是最巧的一点,Integer 类型 hashCode() 的返回值就是其 int 值本身,而存储的时候元素通过一些运算后会得出自己在数组中所处的位置。由于在这一步,其本身即下标(只考虑这一步),其实已经实现了排序功能,由于int类型范围太广,内存放不下,所以对其进行取模运算,为了减少哈希冲突,又在取模前进行了,扰动函数的计算,得到的数作为元素下标,按照 JDK8 下的 hash 算法,以及 loadFactor 及扩容机制,这就导致数据在经过 HashMap 的 hash() 运算后仍然是自己本身的值,且没有发生哈希冲突
总结: 所以HashSet 只是不保证有序, 并不是保证无序
HashSet 是线程安全的吗?
不是,HashSet 不是线程安全的
如何从 HashSet 中获取指定的元素?
通过将 Set 集合转换为 List 列表即可使用 get 操作获取指定元素
public class HashSetDemo {
// 自定义一个获取变量的类型方法
public static String getType(Object obj) {
return obj.getClass().getName().toString();
}
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);
set.add(4);
set.add(5);
List<Integer> list = new ArrayList<>(set);
System.out.println(list.get(1)); // 获取指定元素
System.out.println(getType(list)); // 打印变量类型
}
}
输出:
2
java.util.ArrayList