HashSet 源码分析
本文将从以下几个方面介绍
前言
HashSet 的特点
类图
源码分析
HashSet 如何保证元素的不重复
总结
前言
在工作中,经常有这样的需求,需要判断某个ID是否在某个组的管理之下等,就需要查询该组下的ID放到一个集合中,且集合中元素不能有重复,之后判断该集合是否包含我们的目标ID;这时,我们可以使用 HashSet 来存放我们的ID,HashSet可以自动的帮助我们去重,比如HashSet<String> set = new HashSet<>(list) 等。接下来看下 HashSet 的内部是怎么实现的。
HashSet的特点
从 HashSet 的 Javadoc 的说明中,可以得到以下信息:
1. HashSet 底层是使用 HashMap 来保存元素的
2.它不保证集合中存放元素的顺序,即是无序的,且这种顺序可能会随着时间的推移还会改变
3.允许 null 值,且只有一个
4.HashSet 不是线程安全的,底层的 HashMap 不是线程安全的,它自然就不是啦,可以使用 Collections.synchronizedSet(new HashSet()) 来创建线程安全的 HashSet;synchronizedSet 方法底层会根据 Set 来创建 SynchronizedSet 对象,而 SynchronizedSet 类继承了 SynchronizedCollection,在 SynchronizedCollection 中的所有方法都加上了 synchronized 关键字,所以它是线程安全的,可以被多线程并发访问。
5.集合中的元素不会重复
类图
先来看看 HashSet 的一个类图
从类图中,可以看到, HashSet 继承了 AbstractSet 抽象类, 而 AbstractSet 又继承了 AbstractCollection 抽象类,此外,HashSet 还实现了 Set 接口等。
AbstractSet 抽象类主要实现了两个方法 equals 和 hashcode 方法,因为 HashSet 中没有重复元素,就是根据这两个方法来进行判断的:
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection<?> c = (Collection<?>) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
public int hashCode() {
int h = 0;
Iterator<E> i = iterator();
while (i.hasNext()) {
E obj = i.next();
if (obj != null)
h += obj.hashCode();
}
return h;
}
Set 接口,它是一个顶层接口,主要定义了一些公共的方法,如 add, isEmpty, size, remove, contains 等一些方法;HashSet, SortedSet,TreeSet 都实现了该接口。
源码分析
接下来看下它的内部实现,它内部使用 HashMap 来存放元素,它的所有方法基本上都是调用 HashMap 的方法来实现的,相等于对HashMap包装了一层,关于 HashMap 的实现,可以参考 HashMap源码分析-jdk1.6和jdk1.8的区别。
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
{
// 用来存放元素的 HashMap
private transient HashMap<E,Object> map;
// 因为 HashMap 是存放键值对的,而 HashSet 只会存放其中的key,即当做 HashMap 的key,
// 而value 就是这个 Object 对象了,HashMap 中所有元素的 value 都是它
private static final Object PRESENT = new Object();
// 无参构造,创建 HashMap 对象,初始大小为 16
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
// 根据初始大小和加载因子创建 HashSet
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
// 根据初始大小创建 HashSet
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 迭代器
public Iterator<E> iterator() {
return map.keySet().iterator();
}
........................
}
从上面声明可看到,HashSet 底层是使用 HashMap 来存放元素的,且 HashMap 中所有元素的 value 都是同一个 Object 对象,且它被 final 修饰。
接下来看下它的方法实现:
// 返回集合中元素的个数
public int size() {
return map.size();
}
// 判断集合是否为空
public boolean isEmpty() {
return map.isEmpty();
}
// 集合中是否包含某个值,即判断 HashMap 中是否包含该key
public boolean contains(Object o) {
return map.containsKey(o);
}
// 添加元素,在这里可以看到,添加元素的时候,会向 HashMap 中添加,且 HashMap中的value都是同一个 Object对象
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// 删除元素
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
// 清楚集合
public void clear() {
map.clear();
}
以上就是 HashSet 源码的全部实现了,看着很简单,但是要知道 HashMap 的实现过程才会清楚。
HashSet 如何保证元素的不重复
接下来,看下 HashSet 的 add 方法,看下它是如何保证添加的元素不重复的
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
之后来看下 HashMap 的 put 方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put 方法会调用 putVal 方法进行添加元素,来看下 putVal 方法的实现:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果 key 的 hashcode 在 Node 数组中不存在,即 集合中没有改元素,则创建 Node 节点,加入到 Node 数组中,添加元素成功
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 如果在 Node 数组中该 key ,则在 Node 数组该索引上的链表进行查找
Node<K,V> e; K k;
// 链表上已经存在该元素
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 在该链表上找不到该key,则创建,插入到链表上
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果 key 已存在,则返回旧的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
// 如果元素不存在,则返回 null
return null;
}
所以,在向 HashSet 添加元素的时候,如果要添加元素的 hashcode 已存在,且 equals 相等,则会替换掉旧的值。
关于 HashMap 的其他方法实现,可以参考 HashMap源码分析-jdk1.6和jdk1.8的区别
以上就是 HashSet 的实现。看起来很简单,但是前提是得知道 HashMap 的实现。
总结
HashSet的特点
1. HashSet 底层是使用 HashMap 来保存元素的
2.它不保证集合中存放元素的顺序,即是无序的,且这种顺序可能会随着时间的推移还会改变
3.允许 null 值,且只有一个
4.HashSet 不是线程安全的,底层的 HashMap 不是线程安全的,它自然就不是啦,可以使用 Collections.synchronizedSet(new HashSet()) 来创建线程安全的 HashSet
5.集合中的元素不会重复

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
vue.js响应式原理解析与实现—实现v-model与{{}}指令
上次我们已经分析了vue.js是通过Object.defineProperty以及发布订阅模式来进行数据劫持和监听,并且实现了一个简单的demo。今天,我们就基于上一节的代码,来实现一个MVVM类,将其与html结合在一起,并且实现v-model以及{{}}语法。 tips:本节新增代码(去除注释)在一百行左右。使用的Observer和Watcher都是延用上一节的代码,没有修改。 接下来,让我们一步步来,实现一个MVVM类。 构造函数 首先,一个MVVM的构造函数如下(和vue.js的构造函数一样): class MVVM { constructor({ data, el }) { this.data = data; this.el = el; this.init(); this.initDom(); } } 和vue.js一样,有它的data属性以及el元素。 初始化操作 vue.js可以通过this.xxx的方法来直接访问this.data.xxx的属性,这一点是怎么做到的呢?其实答案很简单,它是通过Object.defineProperty来做手脚,当你访问this.xxx的时...
-
下一篇
以太坊区块链 Asp.Net Core的安全API设计 (上)
去中心化应用程序(DApp)的常见设计不仅依赖于以太坊区块链,还依赖于API层。在这种情况下,DApp通过用户的以太坊帐户与智能合约进行交互,并通过交换用户凭据而发布的JWT token与API层进行交互。 目标是使用以太坊帐户作为用户凭据来请求JWT Token。 最简单的方法可能是请求用户使用其他随机生成的数据在以太坊上进行交易,然后在发出JWT之前检查交易和随机数据。这种方法有几个副作用: 1.用户必须进行交易并支付gas以进行简单的身份验证。 2.用户必须等待12-120秒(基于耗费的gas)才能完成身份验证过程。 3.每个用户的所有登录操作在以太坊区块链上变得不可公开。 这种方式不实用,并且有一些用户体验限制,我们需要一种方法让用户证明他拥有与他想要用来登录的帐户相关的私钥,而不是只(当然)要求私钥,而不管他是否进行交易。 解决方案 Metamask团队成员Dan Finlay的这篇文章向我启发了本教程。基本上,你的DApp可以提示用户使用他的私钥对短信进行签名。此签名操作不会生成交易,并且它由Metamask附加组件透明地处理(顺便说一句,你的帐户需要解锁)。签名后,帐户,...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL8.0.19开启GTID主从同步CentOS8
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- MySQL数据库在高并发下的优化方案
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Dcoker安装(在线仓库),最新的服务器搭配容器使用