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


    总结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
    }
    
  • 相关阅读:
    [题解]luogu_P4198_楼房重建(线段树logn合并
    [题解]luogu_P3084(单调队列dp
    [题解]luogu_P3084(差分约束/梦想spfa
    [题解/模板]POJ_1201(差分约束
    [题解]luogu_P5059(矩乘
    [题解]luogu_P5004跳房子2(矩乘递推
    CF1042A Benches(二分答案)
    8.24考试总结
    NOIP2017题目
    「LG3045」「USACO12FEB」牛券Cow Coupons
  • 原文地址:https://www.cnblogs.com/wufuqin/p/14988155.html
Copyright © 2020-2023  润新知