总结HashSet以及分析部分底层源码

1. HashSet继承的抽象类和实现的接口

  • 继承的抽象类:AbstractSet
  • 实现了Set接口
  • 实现了Cloneable接口
  • 实现了Serializable序列化接口:该接口标记此类支持序列化操作

2. HashSet底层数据结构

HashSet底层是基于HashMap实现的,HashMap底层数据结构是基于数组+链表实现的。

1. 特点

  • 既保存了数组查询和修改元素效率快的优点,也保存了链表在添加和删除元素时效率快的特点。
  • 存储的元素是无序的,不允许重复的,存储的元素最多只能有一个为null值,这是因为HashSet底层存储元素时只是利用了HashMap的key来存储元素,而HashMap的value都是存储的 一个new Object() 对象。所以说HashSet只是利用了HashMap的key,并没有利用HashMap的value。

2. HashSet的底层结构图

因为HashSet底层是使用的HashMap,所以下图实际上是HashMap的底层数据结构。当存储一个元素时,首先会给这个元素计算一个hash值。然后根据计算出来的hash值决定将元素存储到哈希表中的那个位置。

3. 优点

  • 存取效率高,可以动态扩容

4. 缺点

  • 每次存储新的元素都需要计算一次hashCode值,如果计算hash值的算法设计的不好,哈希碰撞产生过多,就可能造成一个节点小存储了多个元素,而哈希表中相邻的元素的位置没有存储任何元素。
  • HashSet线程不安全,在多线程情况下会出现线程安全问题。

3. HashSet适用的场景

  • 需要存储不重复的值,要求存取效率高,适合在单线程情况下使用。

  • 如果需要在多线程情况下使用,需要使用Collections集合工具类,创建一个线程安全的HashSet集合

    Set<Integer> hashSet = Collections.synchronizedSet(new HashSet<Integer>());

4. HashSet底层源码分析

1. 构造函数

1. 默认无参构造函数

/**
* 默认无参构造函数
*/
public HashSet() {
map = new HashMap<>();
}

2. 传递一个集合的构造函数

/**
* 可以将集合中的数据全部添加到新创建的HashSet集合中,会去除掉重复的值。
* @param c
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}

2. 添加一个元素的流程

1. 将数据包装

在每次添加数据时,如果数据是基本数据类型,会先将基本数据类型进行装箱操作,把基本数据类型转换成对应的包装类型(引用数据类型)

// 例如:集合中存放Integer数据类型,在进行add操作时,会先进行装箱操作

/**
* 将基本数据类转换为引用数据类型
* @param i 传入的参数为一个基本型数据类型
* @return 返回的参数是一个基本数据类型的包装类(引用数据类型)
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

2. 调用add() 方法

/**
* HashSet的添加方法
* @param i 传入需要添加的元素
* @return 添加成功返回true,失败返回false
*/
public boolean add(E e) {
// 直接调用已经创建好的HashMap集合,调用HashMap中的put()方法进行添加,key为元素值,value为常量对象
return map.put(e, PRESENT)==null;
}
  • 常量说明

    // 该常量对象将作为HashSet集合的value
    private static final Object PRESENT = new Object();

3. HashMap中的put()方法

/**
* HashMap的put添加方法
* @param key 对应的是HashSet要添加的元素
* @param value 对应的是一个常量对象 new Object()
* @return 添加成功返回null,添加失败返回value值
*/
public V put(K key, V value) {
// 调用putVal()方法,对元素进行添加
return putVal(hash(key), key, value, false, true);
}

4. HashMap中的hash()方法

/**
* HashMap的hash方法,用于计算每个key的hash值,这个hash值将决定key在哈希表中的具体位置
* @param key 对应的是HashSet要添加的元素
* @return 返回根据key计算出来的hash值
*/
static final int hash(Object key) {
// 用于接收计算好的hash值
int h;
// 返回根据key计算出来的hash值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

5. HashMap中putVal()方法

/**
* HashMap的hash方法,用于计算每个key的hash值,这个hash值将决定key在哈希表中的具体位置
* @param hash 计算好的hash值
* @param value 需要存储的key值
* @param onlyIfAbsent 需要存储的value值
* @param onlyIfAbsent 如果返回true说明添加的key是首次添加,false说明是修改了对应key的value
* @param evict 目前HashMap并没有使用改变了,留给了实现HashMap的子类
* @return
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 创建一个类型为Node的数组,其实就是哈希表
Node<K,V>[] tab;
//
Node<K,V> p;
// 辅助n,记录tab的长度。辅助变量i,存储经过计算得到的tab表的下标值
int n, i; // 判读tab表是否为空,或者长度为0,满足则说明是第一次创建tab表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // 为tab表创建初始大小16,赋给辅助变量n // 将tab表长度减一在和hash进行按位与运算,得到一个tab表的下标值,赋给i,
// 再将当前下标所指向的tab表的对象赋给p,判断当前位置上是否存储对象,即是否为null
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 如果当前位置为null,直接添加一个新节点 // 如果当前位置已经存储过节点
else {
// 创建一个节点对象e
Node<K,V> e;
// 创建一个与key相同类型的变量k
K k; /*
如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样,
并且满足下面两个条件之一:
(1)准备加入的key和p指向的Node结点的key是同一个对象
(2)p指向的Node结点的key的equals()和准备加入的key比较后相同
*/
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 判断p是不是红黑树的一个节点对象
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 作为节点添加到红黑树 // 如果table对应索引位置,已经是一个链表,就使用for循环比较
else {
/*
1. 依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后
注意在把元素添加到链表后,立即判断该链表是否已经达到8个结点,
达到8个节点数就调用treeifyBin()对当前这个链表进行树化(转成红黑树)
注意:
if(tab==null||(n=tab.length)<MIN_TREEIFY_CAPACITY(64))
resize();
如果上面条件成立,先table扩容,只有上面条件不成立时,才进行转成红黑树
*/
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
} // 2. 依次和该链表的每一个元素比较过程中,如果有key相同情况,就直接break
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
// 将对应位置上的节点
p = e;
}
} if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
} // 记录集合被修改的次数
++modCount;
// 判断当前哈希表中实际存储的元素个数是否得到扩容条件,threshold的大小为哈希表长度的0.75(默认值)
if (++size > threshold)
resize(); // 调用扩容方法
afterNodeInsertion(evict); // 该方法在HashMap中没有实际作用,是留给HashMap的子类的
return null; // 添加节点元素成功,返回null
}

总结HashSet以及分析部分底层源码的更多相关文章

  1. List-LinkedList、set集合基础增强底层源码分析

    List-LinkedList 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 继上一章继续讲解,上章内容: List-ArreyLlist集合基础增强底层源码分析:https:// ...

  2. Java泛型底层源码解析-ArrayList,LinkedList,HashSet和HashMap

    声明:以下源代码使用的都是基于JDK1.8_112版本 1. ArrayList源码解析 <1. 集合中存放的依然是对象的引用而不是对象本身,且无法放置原生数据类型,我们需要使用原生数据类型的包 ...

  3. List-ArrayList集合基础增强底层源码分析

    List集合基础增强底层源码分析 作者:Stanley 罗昊 [转载请注明出处和署名,谢谢!] 集合分为三个系列,分别为:List.set.map List系列 特点:元素有序可重复 有序指的是元素的 ...

  4. 2018.11.20 Struts2中对结果处理方式分析&struts2内置的方式底层源码剖析

    介绍一下struts2内置帮我们封装好的处理结果方式也就是底层源码分析 这是我们的jar包里面找的位置目录 打开往下拉看到result-type节点 name那一列就是我们的type类型取值 上一篇博 ...

  5. BAT资深工程师 由浅入深分析 Tp5&Tp6底层源码 - 分享

    BAT资深工程师由浅入深分析Tp5&Tp6底层源码 第1章 课程简介 本章主要让大家知道本套课程的主线, 导学内容,如何学习源码等,看完本章要让小伙伴觉得这个是必须要掌握的,并且对加薪有很大的 ...

  6. BAT资深工程师由浅入深分析Tp5&Tp6底层源码☆

    第1章 课程简介 本章主要让大家知道本套课程的主线, 导学内容,如何学习源码等,看完本章要让小伙伴觉得这个是必须要掌握的,并且对加薪有很大的帮助. 第2章 [TP5灵魂]自动加载Loader 深度分析 ...

  7. LInkedList总结及部分底层源码分析

    LInkedList总结及部分底层源码分析 1. LinkedList的实现与继承关系 继承:AbstractSequentialList 抽象类 实现:List 接口 实现:Deque 接口 实现: ...

  8. Vector总结及部分底层源码分析

    Vector总结及部分底层源码分析 1. Vector继承的抽象类和实现的接口 Vector类实现的接口 List接口:里面定义了List集合的基本接口,Vector进行了实现 RandomAcces ...

  9. 从底层源码浅析Mybatis的SqlSessionFactory初始化过程

    目录 搭建源码环境 POM依赖 测试SQL Mybatis全局配置文件 UserMapper接口 UserMapper配置 User实体 Main方法 快速进入Debug跟踪 源码分析准备 源码分析 ...

随机推荐

  1. Pandas核心用法

    目录 Numpy和Pandas Numpy科学计算 Pandas数据分析 安装jupyter notebook Numpy语法 创建和基本使用 切片索引 布尔索引 对位运算 矩阵的乘除 其他方法 Pa ...

  2. HITS算法简介

    1.算法名称 超文本敏感标题搜索 (Hyperlink-Induced Topic Search) 2.算法背景 HITS 算法是由康奈尔大学的Jon Kleinberg 博士于1997 年首先提出的 ...

  3. 深度解析HashMap集合底层原理

    目录 前置知识 ==和equals的区别 为什么要重写equals和HashCode 时间复杂度 (不带符号右移) >>> ^异或运算 &(与运算) 位移操作:1<&l ...

  4. AtCoder Beginner Contest 213 F题 题解

    F - Common Prefixes 该题也是囤了好久的题目了,看题目公共前缀,再扫一眼题目,嗯求每个后缀与其他后缀的公共前缀的和,那不就是后缀数组吗?对于这类问题后缀数组可是相当在行的. 我们用后 ...

  5. hdu 1158 Employment Planning(DP)

    题意: 有一个工程需要N个月才能完成.(n<=12) 给出雇佣一个工人的费用.每个工人每个月的工资.解雇一个工人的费用. 然后给出N个月所需的最少工人人数. 问完成这个项目最少需要花多少钱. 思 ...

  6. mysql查询表名和列名字

    -- 登录yellowcong 数据库 mysql -uroot -proot yellowcong -- 查看当前数据库 select database() -- 查看数据库里面的表 --table ...

  7. Java测试开发--sts安装Lombok(七)

    1.sts安装Lombok的步骤: 下载最新的lombok.jar包,进入cmd窗口,切到Lombok下载的目录,运行命令: java -jar lombok.jar,会出现如下界面: 已经默认选好了 ...

  8. [后端及服务器][WSL2(Ubuntu)+Docker]从零开始在WSL中安装Docker

    目录 简介 WSL 安装 开启虚拟化(BIOS) 检查系统版本 安装WSL 老版本安装详情 简介 想花三篇文章写下从Windows(WSL)上开启Docker部署php/node/vue/html等项 ...

  9. 动手写一个简单的Web框架(Werkzeug路由问题)

    动手写一个简单的Web框架(Werkzeug路由问题) 继承上一篇博客,实现了HelloWorld,但是这并不是一个Web框架,只是自己手写的一个程序,别人是无法通过自己定义路由和返回文本,来使用的, ...

  10. 站长管理服务器必读:Ftp、Ftps与Sftp三兄弟的不同与区别以及部署全指引

    文章标题: 站长管理服务器必读:Ftp.Ftps与Sftp三兄弟的不同与区别以及部署全指引 关键字 : ftp,sftp,freesshd,ftps 文章分类: 教程 创建时间: 2020年3月23日 ...