HashSet底层解析

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、哈希表(散列表)介绍

       在看HashSet集合前,先来了解一下哈希表。散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

        那么在Java中,哈希表是如何构建的的呢?

1.Java中哈希表的结构

在这里插入图片描述

在JDK1.8版本前:数组 + 链表

在JDK1.8版本后:数组 + 链表 +红黑树

如图:
在这里插入图片描述

2.哈希表添加一个节点元素的步骤(方法)

①、计算添加元素(新元素)的哈希值。计算哈希值的过程称为哈希算法。

Java中是如何计算哈希值的?是通过hashCode()方法来计算哈希值。
    a、若未重写hashCode方法,那么哈希值等效于对象的地址值
    b、若重写了hashCode方法,那么哈希值可以是千变万化

②、计算在数组中的位置:通过计算出来的哈希值与数组的长度进行取模运算,得出来的值就是该节点元素在数组中的位置(索引值)。

③、如果该索引所对应的位置上的值未null,则直接添加新的节点元素。否则就与该位置上的链表中每一个节点元素进行比较(新元素.equals(旧元素))。
        若有两个节点元素相同,则去重新元素,也就是添加失败。
        若都不相同,则在链表后面添加新的元素。

3.Java中哈希表的底层原理

①、初始化时,内存默认创建一个长度为16,加载因子为0.75的数组。

加载因子:就是数组扩容的时机。
数组扩容的时机:哈希表中元素节点的个数 > 数组的长度 × 加载因子 

②、每次扩容新数组的长度时旧数组的两倍,并且所有的元素节点都重新计算在数组中的位置。

③、在JDK1.8版本之后引入了一个红黑树的结构,用来提升查找的效率。

​ 在单个链表中节点元素的个数大于等于8且底层数组长度达到64时,链表就会转换成红黑树。

④、在JDK1.8之前,如果新添加元素的位置上的值为null,那么无论哈希表中有多少元素,

​ 哈希表都不会扩容。在JDK1.8之后就取消了该机制。

二、HashSet集合概述

        此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。 此类为基本操作提供了稳定性能,这些基本操作包括 addremovecontainssize,假定哈希函数将这些元素正确地分布在桶中。对此 set 进行迭代所需的时间与 HashSet 实例的大小(元素的数量)和底层 HashMap 实例(桶的数量)的“容量”的和成比例。因此,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。

三、HashSet类的实现

1.属性

static final long serialVersionUID = -5024744406713321676L;
// 底层使用 HashMap 来保存 HashSet 中所有元素。
private transient HashMap<E,Object> map;

// 定义一个虚拟的 Object 对象作为 HashMap 的 value,将此对象定义为 static final。
private static final Object PRESENT = new Object();

2.构造方法

/**
 * 默认的无参构造器,构造一个空的 HashSet。
 *
 * 实际底层会初始化一个空的 HashMap,并使用默认初始容量为 16 和加载因子 0.75。
 */
public HashSet() {
	map = new HashMap<E,Object>();
}
/**
 * 构造一个包含指定 collection 中的元素的新 set。
 *
 * 实际底层使用默认的加载因子 0.75 和足以包含指定
 * collection 中所有元素的初始容量来创建一个 HashMap。
 * @param c 其中的元素将存放在此 set 中的 collection。
 */
 public HashSet(Collection<? extends E> c) {
 	map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
 	addAll(c);
 }

 /**
 * 以指定的 initialCapacity 和 loadFactor 构造一个空的 HashSet。
 *
 * 实际底层以相应的参数构造一个空的 HashMap。
 * @param initialCapacity 初始容量。
 * @param loadFactor 加载因子。
 */
public HashSet(int initialCapacity, float loadFactor) {
	map = new HashMap<E,Object>(initialCapacity, loadFactor);
}

/**
 * 以指定的 initialCapacity 构造一个空的 HashSet。
 *
 * 实际底层以相应的参数及加载因子 loadFactor 为 0.75 构造一个空的 HashMap。
 * @param initialCapacity 初始容量。
 */
public HashSet(int initialCapacity) {
	map = new HashMap<E,Object>(initialCapacity);
}

3.add() : 向集合中添加一个元素

        在添加的过程中,其核心的思想就是元素的去重处理。所以我们将会围绕这元素的去重思想重点分析HashSet集合中添加方法是如何去重的。由于HashMap和HashSet集合共用一套Hash表结构且HashMap双列集合是由2根单列集合组成 : 键的集合是 HashSet 值的集合,是 元素可重复的集合,所以这里会借鉴一部分HashMap的源码。1.7版本JDK中 HashMap中的put方法源码: 1.7版本的哈希表结构没有添加红黑树结构。

核心代码语句:

e.hash == hash && ((k = e.key) == key || key.equals(k))
老元素.hashCode() == 新元素.hashCode() && (老元素 == 新元素 || 新元素.equals(老元素))

可以将该过程分为6个段进行分析,分析如下:

	/**
     * 假设我要添加一个对象元素stu
     * Student{
     *     name;
     *     age;
     * }
     */

    // 第一段调用add(stu)方法。重点关注形参E e的去向。
    public boolean add(E e) { // = stu4;
        /**
         *  HashMap集合是一个双列集合,也称键值对Key-Value集合
         *  他的key集合其特点无序性、元素唯一性,其底层也是哈希表构成。
         *  所以在Java中HashMap集合中的键于HashSet集合共用一套哈希表结构。
         *
         *  在这里调用map中的put方法是将对象e传入HashMap的键中进行存储。
         *  本质是判断put方法返回值是否为null
         *  若返回值为null,add方法返回值为true,说明元素添加成功
         *  若返回值不等于null,add方法的返回值为false,说明元素添加失败。
         *  PRESENT:传入一个空的集合
         */
        return this.map.put(e, PRESENT)==null;
    }

    // HashMap集合中的put方法
    // 此时key = e
    public V put(K key, V value) {

        /**
         * 第二段:判断当前哈希表是否为空
         * table:此时底层的那个哈希表
         * EMPTY_TABLE:空表
         */
        if (table == EMPTY_TABLE) {
            // 如果为空表,第一次添加就不需要判读是否去重
            // 直接添加元素,该判断作用是提高程序运行的效率
            inflateTable(threshold);
        }


        /**
         * 第三段:非空校验,判断key是否为空值
         */
        if (key == null){
            // 如果第一次添加就直接将元素放入哈希表中
            // 如果不是,旧剔除后续的null值
            return putForNullKey(value);
        }


        /**
         * 第四段:计算哈希值,通过哈希值计算key在数组中的位置(索引)
         */
        int hash = hash(key); // 计算哈希值
        int i = indexFor(hash, table.length); // 进行在数组中的索引值


        /**
         * 第五段:重中之重,判断元素是否重复,若重复该元素就不添加(去重操作的核心)
         *循环组成部分:
         * 初始化变量:Entry<K,V> e = table[i] 获取出底层数组中的i索引所对应的值
         * 循环判断条件:e != null 当节点元素为null结束循环
         * 循环自增语句:e = e.next 当一次循环体执行完,就将e指向下一个节点元素
         * 循环体语句:重复循环的代码
         *
         * 该循环就是拿新添加的对象于索引所对应的链表中的元素进行一一比对
         * 若有相同的元素,就将新添加的元素出去不要,达到去重的目的
         */
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;

            /**
             * 判断条件一:e.hash == hash
             *        判断新元素的哈希值hash与哈希表中的老元素元素的哈希值e.hash是否相同
             * 判断条件二:(k = e.key) == key
             *      a、若传入的是一个基本数据类型的封装类如Integer的化就是比较其值是否相等
             *      b、若传入的是一个引用类的对象,则比较的是地址值
             *      好处是:提高了代码的执行效率
             * 判断条件三:key.equals(k)
             *      若重写了equals方法,比较的是对象的内容
             *      若未重写equals方法,比较的是对象的地址值
             *  条件二和三存在的意义是:防止条件一中两个对象的哈希值相同
             *
             *  去重的核心思想:
             *  	if(老元素.hashCode() == 新元素.hashCode() && (老元素 == 新元素 || 新元素.equals(老元素)))
             */
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {


                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);

                return oldValue;
            }
        }

        /**
         * 第六段:向集合中成功添加元素
         */
        modCount++; // 记录集合更改的次数,在多线程中能够规避某些问题
        addEntry(hash, key, value, i); // 添加元素

        // 成功添加元素,返回空值
        return null;
    }

4.其它方法

/**
* 返回对此 set 中元素进行迭代的迭代器。返回元素的顺序并不是特定的。
*
* 底层实际调用底层 HashMap 的 keySet 来返回所有的 key。
* 可见 HashSet 中的元素,只是存放在了底层 HashMap 的 key 上,
* value 使用一个 static final 的 Object 对象标识。
* @return 对此 set 中元素进行迭代的 Iterator。
*/
public Iterator<E> iterator() {
	return map.keySet().iterator();
}

/**
* 返回此 set 中的元素的数量(set 的容量)。
*
* 底层实际调用 HashMap 的 size()方法返回 Entry 的数量,就得到该 Set 中元素的个数。
* @return 此 set 中的元素的数量(set 的容量)。
*/
public int size() {
	return map.size();
}
 
/**
* 如果此 set 不包含任何元素,则返回 true。
*
* 底层实际调用 HashMap 的 isEmpty()判断该 HashSet 是否为空。
* @return 如果此 set 不包含任何元素,则返回 true。
*/
public boolean isEmpty() {
	return map.isEmpty();
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值