HashSet集合
概述:你真的了解HashSet集合么?用了怎么久的集合你真的了解集合么?
**注意:该文章是基于JDK8讲解的**
那不妨先看几个问题,如果你都能知道那么就说明你真的了解,如果感觉似曾相识,但是又说不上来,那么就是认真的往下看吧?
想要看懂集合源码需要具备哪些知识?
数据结构和算法,javaSE的面向对象,进制转换,以及java的一些运算符,比如位移,异或等等。
理解源码的方式就是打断点一步一步的跟踪,看源代码是如何执行的,遇到看不懂的代码就多看几次。
这个文章是我在没有学习数据结构和算法的前提下理解的,所以呢?也行解释的不够透彻,请多多谅解,后期肯定会发布关于数据结构和算法的一些文章。
1.带着问题学习?
基础部分问题?
1.HashSet底层是什么数据结构?
2.HashSet允许有空值么?
3.HashSet允许有重复值么?
4.如果new两个值一样的字符串,往HashSet集合中添加,是否能添加进去?
5.HashSet是如何保证元素的唯一性的?
6.HashSetadd方法其实是调用的那个方法?
7.HashSet是否是线程安全的呢?
上面的几乎都没有什么难度,使用过集合的大多数人都了解。
那我们来看看哪些硬核的难点吧!
我们都知道HashSet底层其实上是new了一个HashMap集合,那我们就来看看,HashSet调用add方法的时候的一些问题。
1.HashMap的value部分值是否相同?
2.HashMap的初始化容量是多大?是在什么时候进行初化容量?
3.在计算HashMap的key的HashCode值的时候是单纯的时候hashCode方法计算出来的么?
4.HashMap什么时候进行扩容?
5.HashMap数组转红黑树需要满足那些条件?
7.HashSet在添加重复元素的时候,具体是怎么进行判断该元素已经存在的?
8.使用HashSet集合的时候,需要重写HashCode和equlas方法么?
2.问题答案:
基础部分问题?
1.HashSet底层是什么数据结构?
答案:HashSet底层采用的是数组加链表加红黑树,在new HashSet的时候实际底层是new了一个HashMap,把HashMap的key部分,作为HashSet的Value部分。
2.HashSet允许有空值么?
答案:准确的来说是允许的(也就是代码不会出现异常),但是只能有一个空值,如果有第二个空值,那么第二个空值将加不进HashSet集合。
3.HashSet允许有重复值么?
答案:肯定是不允许的,因为HashSet的value部分是HashMap的key部分,因为HashMap的key本身就是无序不可重复的,所以HashSet也就不可能重复。
4.如果new两个值一样的字符串,往HashSet集合中添加,是否能添加进去?
答案:是不可以加入进去的,因为在进行添加元素的时候会进行判断,通过hashCode方法和equals方法进行比对,String这个类,重写了这两个方法,比较的是字符串的值,而不是使用继承自Object的equlash和hashCode方法去进行比较。
5.HashSet是如何保证元素的唯一性的?
答案:依赖于hashCode()和equals()这两个方法,所有在我们比较两个我们自定义的对象的时候,需要我们重写这两个方法,自定义比较规则,否则就是使用继承自Object的进行比对,比对的是对象的内存地址。
6.HashSetadd方法其实是调用的那个方法?
答案:其实调用的是HashMap的map.put方法。
7.HashSet是否是线程安全的呢?
答案:HashSet是线程不安全的,所以呢?他的执行效率比较高,因为HashSet和HashMap的源代码中的方法都有没有加synchronized关键字。
那我们来看看哪些硬核的难点吧!
1.HashMap的value部分值是否相同?
答案:都是相同的,因为value部分是使用了一个静态的Object对象进行占位,这个对象只是用于占位操作,并没有多大的实际意义。
2.HashMap的初始化容量是多大?是在什么时候进行初化容量?
答案:初始化容量是16,是在第一次调用resize()方法的时候进行扩容的,并不是new HashMap方法的时候就进行扩容。
3.在计算HashMap的key的HashCode值的时候是单纯的时候hashCode方法计算出来的么?
答案: 不是,而是通过一个表达式进行计算后的结果((key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
),并不是单纯的hashCode值。
4.HashMap什么时候进行扩容?
答案:底层数组超过临界值12的时候就会进行扩容,那么为什么不是到16才进行扩容呢?试下一下,他是一个线程不按的集合,万一此时突突来了很多对象,要加入到这个集合,那么这个集合不就炸了么?扩容的机制就是:当前数组容量乘以2再乘以加载因子0.75
也就是说,只要这个集合中添加了12个元素后,底层数组就会扩容到32,因为每次添加元素的时候都会++Size,并不是说,这个数组中满了12个单向链表的时候进行扩容。
扩容判断的核心源代码:
if (++size > threshold)
resize();
afterNodeInsertion(evict);
5.HashMap数组转红黑树需要满足那些条件?
答案:首先判断该链表是否已经到达8个节点,如果满足该条件,再次进行判断这个数组链表的值是否大于64,如果小于64,还不会转化为红黑树,而是进行数组的扩容,大于64再转红黑树。
7.HashSet在添加重复元素的时候,具体是怎么进行判断该元素已经存在的?
答案:进行equlas方法和HashCode方法进行比对,如果比对不出来再进行判断该链表是不是一颗红黑树,是的话进行红黑树的方式进行判断,如果不是,那么就遍历该链表,依次进行比对,如果比对到匹配的值,那么添加失败,如果没有比对到相等的值,那就把该元素添加到该链表的末尾。
8.使用HashSet集合的时候,为什么要重写HashCode和equlas方法?
答案:因为底层添加元素的时候会调用这两个方法进行比对,而这个两个方法就是需要我们自定义比对规则,不然默认继承Object的。
3.源码分析,证明答案
首先看下HashSet的继承结构图:
1.Iterable接口: 顶级父接口,只要实现这个父接口,那就说明该集合是可遍历的,而且有迭代器对象。
2.Collection接口:说明实现这个接口的集合都是单value的形式存在。
3.AbstractCollection抽象类:这个抽象类实现了一些基本的方法,比如toArray()还有toString()等等,定义了一些抽象方法等待子类去实现。
4.Set接口:说明实现该接口的集合都是无序不可重复的。
5.Serializable接口:实现这个接口,就标志着该类是支持可序列化的,也就是把对象保存的磁盘。
6.Cloneable接口:一看这个单词就知道,实现该接口的类都是支持可克隆的。
7.AbstractSet抽象类: 该抽象了实现了equals方法和hashCode,removeAll方法。
了解完HashSet的基本机构图后,我们对HashSet具备的功能就有了一定的了解。
那么我们简单的来看一下数组+链表+红黑树的这这种数据接口的特点:
数组:数组的特点是有序的,查询比较快,但是呢?随机增删就比较慢了,因为随机增删元素涉及到元素的位移操作。
链表:链表是随机增删比较快,但是呢?查询比较慢,因为查询的时候会从头节点开始查找。
来,有图有真相:
红黑树:留到下次将HashMap的时候再讲,因为这里主要是介绍hashSet。因为红黑树极其复杂,不是一两句话可以说明白的。
我们知道,程序其实就是数据结构加算法组成的,所以,程序的精妙之处就是数据结构和算法,不同的业务可能选择不太的数据结构去存储数据,既然他们各有各的优点,那么就可以尝试着把他们组合起来,让他们发挥各自的特点。
在没有树化之前的数据结构图:大概是这样的:
new HashSet的源码:
//执行构造器
public HashSet() {
map = new HashMap<>();
}
1.第一次调用add方法的源码分析:
// 第一次add方法的执行过程:
// 2.add方法 :调用map的put方法 PRESENT:静态的一个Object对象 用于占位,每一个map的value都是用一个对象
* public boolean add(E e) {
* return map.put(e, PRESENT)==null; //如果return null的时候就代表执行成功了
* }
* // 调用hash方法获取到key的hash值
* 3. public V put(K key, V value) {
* return putVal(hash(key), key, value, false, true);
* }
* // 通过hash算法获取的key的hash值 此hash值并不等于key原本的hash值
* static final int hash(Object key) {
* int h;
* return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
* }
*
* 4.得出hash值后 然后去putValue方法判断是否应该把这个值添加进去
* 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成立,调用resize()
* if ((tab = table) == null || (n = tab.length) == 0) //table 其实就是HashMap里面的那个Node数组[] 存放链表的那个数组
* n = (tab = resize()).length; //resize())执行完后,返回一个初始化容量为16的table[]数组
*
* // 通过key的hash值计算出元素应该存放到table数组的那个索引位置
* //并把这个位置的对象赋值给临时变量p,判断p是否为null
* //如果p为空,代表这个位置还没有存放过元素,就创建一个node对象,key和value都放进去,next为null,留给第后来添加的元素存放Node对象
* if ((p = tab[i = (n - 1) & hash]) == null)
* tab[i] = newNode(hash, key, value, null);
* else {
* 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) {
* 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;
* }
* }
* 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) //判断当前这个table数组是否超过了12这个最大容量值,如果超过进行扩容
* resize();
* // 这个方法其实是一个空方法,是留给子类去实现的
* afterNodeInsertion(evict);
* return null; //程序走到这儿,就代表我们第一次添加的元素已经成功了
* }
*
* 5. resize()方法: 初始化数组链表的初始容量10,并且判断该数组是否要进行扩容
* final Node<K,V>[] resize() {
* Node<K,V>[] oldTab = table; //此时的table=0
* int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap=0
* int oldThr = threshold;
* int newCap, newThr = 0;
* if (oldCap > 0) { //table不大于0 false
* if (oldCap >= MAXIMUM_CAPACITY) {
* threshold = Integer.MAX_VALUE;
* return oldTab;
* }
* else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
* oldCap >= DEFAULT_INITIAL_CAPACITY)
* newThr = oldThr << 1; // double threshold
* }
* else if (oldThr > 0) // oldThr=0不成立
* newCap = oldThr;
* else { //
* newCap = DEFAULT_INITIAL_CAPACITY; //初始化HashMap的容量为16
* //最大容量值为 0.75X16=12,当数组中的元素超过12个的时候,数组链表开始扩容
* newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
* }
* if (newThr == 0) {
* float ft = (float)newCap * loadFactor;
* newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
* (int)ft : Integer.MAX_VALUE);
* }
* threshold = newThr; // 最大容量值
* // 创建一个初始化容量为16的table[]
* Node<K, V>[] newTab = (Node<K,V>[])new Node[newCap];
* table = newTab;
* if (oldTab != null) {
* for (int j = 0; j < oldCap; ++j) {
* Node<K,V> e;
* if ((e = oldTab[j]) != null) {
* oldTab[j] = null;
* if (e.next == null)
* newTab[e.hash & (newCap - 1)] = e;
* else if (e instanceof TreeNode)
* ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
* else { // preserve order
* Node<K,V> loHead = null, loTail = null;
* Node<K,V> hiHead = null, hiTail = null;
* Node<K,V> next;
* do {
* next = e.next;
* if ((e.hash & oldCap) == 0) {
* if (loTail == null)
* loHead = e;
* else
* loTail.next = e;
* loTail = e;
* }
* else {
* if (hiTail == null)
* hiHead = e;
* else
* hiTail.next = e;
* hiTail = e;
* }
* } while ((e = next) != null);
* if (loTail != null) {
* loTail.next = null;
* newTab[j] = loHead;
* }
* if (hiTail != null) {
* hiTail.next = null;
* newTab[j + oldCap] = hiHead;
* }
* }
* }
* }
* }
* 最终返回一个 初始化容量为16的table[]数组
* return newTab;
* }
*
2.第二次调用add方法的源码分析
* final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
* boolean evict) {
* Node<K,V>[] tab; Node<K,V> p; int n, i;
* // 显然已经添加过一次元素,此时的table是不等于null的
* if ((tab = table) == null || (n = tab.length) == 0)
* n = (tab = resize()).length;
* // 通过计算后的key的hash值,算出元素存放在数组中的那个位置
* if ((p = tab[i = (n - 1) & hash]) == null)
* // 创建一个 Node对象放到Node数组中(table[])
* tab[i] = newNode(hash, key, value, null);
* else {
* 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) {
* 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;
* }
* }
* if (e != null) { // existing mapping for key
* V oldValue = e.value;
* if (!onlyIfAbsent || oldValue == null)
* e.value = value;
* afterNodeAccess(e);
* return oldValue;
* }
* }
* // 记录修改次数
* ++modCount;
* // 此时为2 不大于12 所以不进行扩容
* if (++size > threshold)
* resize();
* afterNodeInsertion(evict);
* //返回null说明添加成功
* return null;
* }
3.x向集合中添加相同元素的分析:
*
* final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
* boolean evict) {
* Node<K,V>[] tab; Node<K,V> p; int n, i;
* //只有第一次add的时候才会执行这个 if
* if ((tab = table) == null || (n = tab.length) == 0)
* n = (tab = resize()).length;
* // 此时这个 方法为false 因为这次添加的元素是我们上次已经添加过的元素,所以算出来的下标1肯定是和上一次算出的下标一致
* // 判断这个数组的下标位置中是否已经有链表元素存在
* if ((p = tab[i = (n - 1) & hash]) == null)
* tab[i] = newNode(hash, key, value, null);
* else {
* //添加重复值的时候执行:
* Node<K,V> e; K k;
* // 此时的这个p就是指向的上面算出来的数组下标里的那个Node对象
* //如果当前索引位置对应的链表的第一个元素和准备添加的这个key的hash值hash值相同
* if (p.hash == hash &&
* //如果hash值相同的情况下 当前准备要加入的key和刚刚计算出来的数组下标对应的那个Node对象的key是同一个对象 或者
* // 当前的这个key不为null然后在和计算出来的那个数组下标对应的那个Node对象里的key进行equals比较,
* //如果没有重写那么调用的就是继承自Object的equals方法,如果重写过,那么就调用重写后的,hashcode方法也是一样,所以建议两个方法都重写
*
* ((k = p.key) == key || (key != null && key.equals(k))))
* e = p;
* // 如果上面一个条件为假 再判断 这个p是不是一颗红黑树,如果是红黑树的话再按照红黑树的方式进行比较
* // 如果是红黑树 调用:putTreeVal(); 方法进行添加
* else if (p instanceof TreeNode)
* e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
* else {
* // 假如不是红黑树,那就是第三种情况:按照上面第一个情况的方式依次和整个链表进行比较,如果找到条件满足的那就直接break(此元素已经存在);
* // 结束遍历,return oldValue 那么就代表着添加失败,如果说,比较完后都没有满足条件的(该元素不存在),那就挂载到这个链表的末尾
*
* // 在把元素添加到最后,立即判断 该链表是否已经到达8个节点,如果到达,调用treeifyBin(tab, hash);方法把当前这个链表转化为红黑树
* 判断条件如下: if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();
* // 上面条件不成立才进行树化 再进行转红黑树时还进行判断这个数组链表的值是否大于64,如果小于64,还不会转化为红黑树,而是进行数组的扩容,大于64再转红黑树
* for (int binCount = 0; ; ++binCount) {
* // 遍历整链表,都没有找到值一直的,直接添加到链表的末尾
* if ((e = p.next) == null) {
* p.next = newNode(hash, key, value, null);
* if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
* treeifyBin(tab, hash);
* break;
* }
* // 如果 比的过程中找到一个值与准备添加的元素的值一致,那么就直接break,添加失败
* if (e.hash == hash &&
* ((k = e.key) == key || (key != null && key.equals(k))))
* break;
* p = e;
* }
* }
* 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);
* return null;
*