深入理解 HashSet
什么是 HashSet?HashSet 是存在 java.util 包中的类,是 Java 中常用的一个集合类,是 Set 接口的实现类,该容器中不能存储重复的对象。对于 HashSet 来说,它是基于 HashMap 实现的,底层采用 HashMap 来存储元素,关于 HashMap 的文章请看这里 深入理解 HashMapHashSet 的继承类与实现的接口有哪些?public cla...
什么是 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
更多推荐
所有评论(0)