简单的聊聊常用的HashSet

前面写完了 HashMap 的源码简单理解,不趁机把 HashSet 的源码顺带写了属实可惜,毕竟 HashSet 的实现就是靠 HashMap 来做的,源码内容很少,下面就来介绍一下。

一、简单介绍

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. 常用方法

说在前面: HashSet 是基于 HashMap 所构建的,所以基本上绝大部分操作都是直接调用的 HashMap 有关的方法,并赋予一些限制条件,仅此而已。

比较特殊的方法是 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 这个方法的:

Associates the specified value with the specified key in this map. If the map previously contained a mapping for the key, the old value is replaced.
Params:
key – key with which the specified value is to be associated
value – value to be associated with the specified key
Returns:
the previous value associated with key, or null if there was no mapping for key. (A null return can also indicate that the map previously associated null with key.)

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

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

Implements Map.put and related methods.
Params:
hash – hash for key
key – the key
value – the value to put
onlyIfAbsent – if true, don’t change existing value
evict – if false, the table is in creation mode.
Returns:
previous value, or null if none

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


到这里整理一下 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 的源码与解释如下。

Removes the mapping for the specified key from this map if present.
Params:
key – key whose mapping is to be removed from the map
Returns:
the previous value associated with key, or null if there was no mapping for key. (A null return can also indicate that the map previously associated null with key.)

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

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

Implements Map.remove and related methods.
Params:
hash – hash for key
key – the key
value – the value to match if matchValue, else ignored
matchValue – if true only remove if value is equal
movable – if false do not move other nodes while removing
Returns:
the node, or null if none

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 方法的返回值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值