0
点赞
收藏
分享

微信扫一扫

简单的聊聊常用的HashSet

拾杨梅记 2022-04-29 阅读 80
java

一、简单介绍

HashSet 是一个没有重复元素的集合,实现了 Set 的接口。与 HashMap 一样,HashSet 也不能保证元素的顺序,也可以插入 null 值。HashSet 是基于 HashMap 实现的,只是我们使用 HashMap 的时候会传入 keyvalue ,而使用 HashSet 只会人工传入 keyvalue 是由系统自动附加的,这一点本文中会有所提及。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

二、源码浅析

1. 成员变量

和看 HashMap 一样,我们先看看 HashSet 的成员变量都有哪些,实际上只有3个 —— UID、HashMap、PRESENT常量。

static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();

UID 属性我们不在此阐述,只介绍 map 和 PRESENT。

HashMap 的构造是存在 key = E, value = object 的情况的,是非常常见的 HashMap 的构造方法。随后 final 声明了一个 object 类型的常量 PRESENT ,用来代替人工传入的 value

2. 构造方法

HashSet 的构造方法有 5 个。


构建一个新的 HashSet ,其所使用的 HashMap 示例的初始容量为 16 ,负载因子为 0.75 。

public HashSet() {
    map = new HashMap<>();
}

构造包含指定集合中元素的新集合。HashMap是使用 0.75 的负载因子和初始容量创建的,且初始容量足以包含指定集合中的元素。

public HashSet(Collection<? extends E> c) {
    // 利用 Math.max 函数确保最小创建的初始容量大小为 16
    // 使用 c.size 和 0.75f 负载因子大小计算合理的初始容量
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

这里需要复习一下 HashMap 的有关知识,代码中有这么一句话 (int) (c.size()/.75f) + 1 ,倘若集合 c 的元素数量为14。那么新的 HashMap 的容量是 19.666 还是 20 还是 32 呢?答案是 32 ,如果忘记了原因,可以去查看 《简单的理解一下 HashMap 的源码》 第二模块第七节,复习一下 HashMap 的有参构造以及 tableSizeFor()方法 的实现。


构建一个新的 HashSet ;其所使用的 HashMap 示例具有指定的初始容量和指定的负载因子。

public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}

本质上还是要去看 HashMap 的构造方法,不在过多阐述。


构建一个新的 HashSet ;其所使用的 HashMap 示例具有指定的初始容量和 0.75 的负载因子。

public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}

一个比较特别的构造方法。这个构造方法只有 LinkedHashSet 会使用。其所使用的 LinkedHashMap 示例具有指定的初始容量和指定的负载因子。

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

3. 常用方法

比较特殊的方法是 add() 和 remove() ,其他方法也写在下面了,想理解更清楚的话还是再去看一遍 HashMap 的源码更合适。

add()

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

add 方法其实并不难,就是调用了 HashMap 的 put 方法,传入的 key 是我们传入的 key ,value 就是前面提到的常量 PRESENT 。add 方法是一个 boolean 返回值的方法,返回的判断条件是 put 的返回值null 做比较。这是一个在 HashMap 的源码中没有注意到的细节,什么时候 put 方法会返回一个 null 呢?我们不难会想起 put 本质上是调用了 putVal ,也就是说 putVal 方法在某个情况下会返回 null 。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) 

如上是 putVal 的函数名与参数,返回值为 V 类型,也就是 value 对应的类型。而函数体中与 return 有关的代码有两段:

一个是 return oldValue

if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}

一个是 return null

++modCount;
if (++size > threshold)
    resize();
afterNodeInsertion(evict);
return null;

这样看起来感觉本来很清晰的 HashMap 的 put 过程突然又复杂了起来。不妨看一下源码的作者是怎么解释 put 这个方法的:

从上我们不难理解, put 方法的返回值,应当是传入的key对应的上一个value,如果在 HashMap 中没有传入的这个 key ,那么就会返回 null

再去看 putValu 源码作者的解释:

还是一样的含义,返回值是旧的数据值,如果没有旧的数据值则返回空


到这里整理一下 HashMap 的 put 方法或者说 putVal 方法什么时候返回 null ,也就是 HashSet 的 add 方法的判断条件合适成立?

传入的 key 在当前持有的 HashMap 集合里不存在时,put 方法以及 putVal 方法会向 HashMap 插入这个新的 key ,并返回 null 。

再去看 add 方法的判断条件: map.put(e, PRESENT)==null ,是不是就非常的简单明了。add 方法本质上还是对 HashMap 进行了“插入”的操作,只是根据“插入”的返回值的差异,来反馈给我们是否“无重复的插入”成功。“插入”操作总是被执行,只有“插入”不存在的值时才会使得表达式成立,向我们返回 true 的结果。

remove()

public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

与 add 方法一样。HashSet 的 remove 方法会直接调用 HashMap 的 remove 方法。在 HashMap 中,remove 的源码与解释如下。

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

包括实际调用的 removeNode 方法也在这里贴出

final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)

与 HashMap 的 put 方法和 putVal 方法一样,他们都会 return 旧的数据值或者说旧的数据节点node,但是如果不存在的话就会返回 null

remove 的本意是删除,判断条件自然是要求数据存在才可以删除:map.remove(o)==PRESENT ,使用 add 方法传入的 value 均为常量 PRESENT,所以此处应当获得返回值也是 PRESENT ,成功获得则删除成功,返回 true 即可。反之就是获得了 null ,判断条件不成立,删除失败,因为目标数据不存在。

iterator()

返回的是 HashMap 的 iterator

public Iterator<E> iterator() {
    return map.keySet().iterator();
}

size()

返回的是 HashMap 的 size

public int size() {
    return map.size();
}

isEmpty()

调用的是 HashMap 的 isEmpty()

public boolean isEmpty() {
    return map.isEmpty();
}

containsKey()

调用的是 HashMap 的 containsKey()

public boolean contains(Object o) {
    return map.containsKey(o);
}

clear()

调用的是 HashMap 的 clear()

public void clear() {
    map.clear();
}

4. 常见问题解释

4.1 HashSet如何保证元素不重复?

其实还是要回到 HashMap 的 put 函数上。

put 函数的执行流程就是:计算 hash 值,判断桶位,判断 key 的 hash 是否相同进而进行 equals 判断。然后再进行插入操作或者修改value的操作。如果判断结果不相同,则插入新的元素。

HashSet 为 value 统一赋值为常量 PRESENT ,所以所谓的“修改”操作其实也没有发生真正意义的修改。

如果向 HashSet 中添加一个重复的元素,那么就会根据 hash 判断到桶位发现 hash 和 equals 的结果都相同,根据 put 返回的值是“旧的数据值”再和 null 进行判断,就可以得知是不是添加了重复的元素。

在这里有一个小小的问题,HashMap 又是如何判断两个 key 是否相同的呢?希望大家在对自定义的对象类型操作时,不要忘记了重写 hashcode方法 和 equals方法。

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

4.2 HashSet和HashMap的区别

HashSetHashMap
HashSet实现了Set接口HashMap实现了Map接口
HashSet实际上只存储了对象HashMap存储的是键值对
HashSet的添加操作为add()HashMap的添加操作是put()
HashSet使用成员对象计算hashcode值HashMap重写了hashcode方法,实际值是键值对的hashcode异或的结果
基于HashMap实现的HashSet操作要更慢HashMap的操作更快

在之前的 HashMap 的文章中只提到了 hash 方法的实现,是高低16位异或的结果,极大程度的避免了hash碰撞。这里附以 HashMap 的 hashcode 方法,还请将两个方法区分开来,理解两个方法的实际作用。

public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}

4.3 其他问题

目前查阅网络论坛,了解到的有关 HashSet 的问题都是围绕着它的成员常量 PRESENT 来考察的,还请各位了解 HashSet 的 add 与 remove 方法的操作,以及理解他们的根本 HashMap 的 put 与 remove 方法的返回值。

举报

相关推荐

0 条评论