分布式Java应用之集合框架篇(下)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/boker_han/article/details/82914815

前言:
分布式Java应用之集合框架篇(上)一文中,从整体上对Java分布式应用中的集合框架进行了介绍,以及对于其中的List家族给出了源码分析;本文将继续介绍集合框架中的Set家族和Map家族,其实Set家族和Map家族之间是有着很深的渊源,在本文的后续内容中,将从两大家族的成员的关键实现进行源码层面的分析!


首先,还是给出集合框架的整体类图关系,通过类图展开下面的介绍;
集合框架类图

对于Collection接口的子接口Set来说,接口的实现类同样是存放一系列有序的元素,且这些元素均为一个个单体对象,相比于List家族中的实现类而言,最大的区别在于List接口的实现类可以存放重复的元素(即元素之间通过==或equals判断为true),而Set接口的实现类不可以存放重复的元素;


我们先看一下,Set家族中常用的实现类HashSet,下面是HashSet类的底层实现源码:

 	//底层元素的存储结构
    private transient HashMap<E,Object> map;
    /**
     * 构造一个新的空集合,实际上就是构造了一个初始容量为16,负载因子为0.75的HashMap对象作为底层存储
     */
    public HashSet() {
        map = new HashMap<>();
    }
    /**
     * 构造一个新的空集合,实际上就是构造了一个初始容量为指定容量,负载因子为指定负载因子的HashMap对象作为底层存储
     * @param      initialCapacity   初始化容量
     * @param      loadFactor       负载因子
     * @throws     IllegalArgumentException 如果初始化容量参数为负数,或负载因子不为正数,那么将会抛出异常
     */
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    /**
     * 构造一个新的空集合,实际上就是构造了一个初始容量为指定容量,负载因子为默认负载因子0.75的HashMap对象作为底层存储
     * @param      initialCapacity   初始化容量
     * @throws     IllegalArgumentException 如果初始化容量为负数,则抛出异常
     */
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

从上述源码可以看出,HashSet底层采用HashMap实现,因此想要理解HashSet的具体实现原理,首先需要搞清楚HashMap的底层实现原理,而HashMap又是Map家族的成员,这就正好印证了本文开头处给出了言论:Set家族与Map家族是有很深的渊源的,底层都是相似的,渊源当然不会小!


那么,我们就将Set家族放下,先看一看Map家族的核心成员及其实现原理;通过集合框架图可以看出,其实研究Map也就是研究HashMap,下面对HashMap类的底层结构以及常用操作的实现进行源码分析;

HashMap底层存储结构:

	/**
     * 采用数组实现的底层存储结构,数组元素类型是内部类Node,数组在有必要的时候,
     * 会进行扩容;当为数组分配内存时,数组的大小始终保持为2的整数次幂(JDK1.8中的HashMap
     * 相关优化的基础)
     */
    transient Node<K,V>[] table;

从这里可以看出,HashMap底层的基本数据结构是数组,而数组的元素类型是Node<K,V>,下面是Node<K,V>的源码:

    /**
     * 基本的hash节点类,HashMap大多数元素都是以这种结构进行包装存放的
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//哈希值
        final K key;//键
        V value;//值
        Node<K,V> next;//关键字段,底层结构的实现基础

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        //final修饰的方法,不可以被重写,保证hashcode求法的一致性
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //核心方法,用于比较节点是否相同
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

从源码可以看出Node<K,V>实现了Map接口中的子接口Entry<K,V>,关键看一下Node<K,V>的字段,可以看到,每一个Node<K,V>对象都有四个字段,分别是hash、key、value以及next;hash、key、value不用说,直接看next,next属性是Node<K,V>类型的,这与链表是相同的,那么可以断定Node<K,V>是一个链表节点类;

小结:
HashMap底层采用的是数组+链表实现的存储结构,每一个数组元素都可以看做是一个链表;


下面看一下HashMap对象的常用操作:

添加键值对:put(K key, V value)

    /**
     * 计算键的哈希值,然后将哈希值的高16位与低16位按位与,得到的结果作为最后参与哈希桶定位的哈希值
     * 这样可以保证当数组长度较小时,高位能够参与哈希桶的定位操作,这是在速度、效率以及质量方面对定位
     * 操作的一种折中处理
     */
    static final int hash(Object key) {
        int h;
        //如果key为null,则默认在数组的第一个桶中进行插入
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    /**
     * 初始化数组或者扩容2倍,如果为null,则按照threshold的值分配数组;
     * 否则,扩容两倍,优化向新数组中移动元素时的操作
     * @return 新的数组
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //判断当前数组是否为null,如果为null,说明还没有初始化数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果当前数组的容量大于0,说明需要进行扩容
        if (oldCap > 0) {
            //如果当前数组的容量已经超过MAXIMUM_CAPACITY,那么不可以进一步扩容,将threshold赋值为最大上限值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果当前数组的容量没有超过MAXIMUM_CAPACITY,那么就扩大两倍,同时将新数组对应的threshold也扩大两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //如果当前数组的容量不大于0,说明需要初始化,如果当前数组的threshold大于0,那么threshold的值就是新的数组容量
        else if (oldThr > 0) 
            // initial capacity was placed in threshold
            newCap = oldThr;
        else {              
            // 否则表示使用默认的初始化容量作为数组的新容量
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果当前threshold为0,那么就会计算新的threshold
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        //更新threshold
        threshold = newThr;
        //创建新数组,并将旧数组中的元素移动到新数组中
        @SuppressWarnings({"rawtypes","unchecked"})
        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 { 
                        // 保持原有顺序对链表中的元素进行迁移
                        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;
                            }
                            //移动oldCap个位置
                            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;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    // 构造一个常规节点(非树节点)
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }
    /**
     * 将以key和value映射的键值对插入HashMap中,如果HashMap中已经存在以key为键的键值对
     * 那么就以value替换已有键值对的值
     * @return 如果已存在以key为键的键值对,那么就返回旧值;否则,返回null
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
     * 实现Map接口中的put方法
     * @param hash 键的哈希值
     * @param key 键
     * @param value 值
     * @param onlyIfAbsent 如果为true,那么不会修改已存在的键值对
     * @param evict 如果为false,那么数组处于创建模式
     * @return 返回旧值或null
     */
    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或者table属性的长度是否为0;如果是则调用resize()对table进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //利用数组长度减一的值与hash进行按位与,来定位键值对应该插入的桶位置
        //判断该位置处的数组元素是否为null;如果为null,则构造一个新的Node对象放到数组的该位置上
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //如果不为null,发生哈希冲突
            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)
                //数组元素为TreeNode类型,调用TreeNode的putTreeVal方法将键值对进行插入处理
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //数组元素为Node类型,则按照链表结构进行查找插入
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //如果到了链表尾部都没有找到相等的键值对,就将键值对插入链表尾部
                        p.next = newNode(hash, key, value, null);
                        //如果当前位置的桶中的节点数超过TREEIFY_THRESHOLD,就将链表结构转换为红黑树结构,结束查找
                        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;
                }
            }
            //如果e不为null,说明查找到相等的键值对,那么替换已有的键值对中的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //结构调整次数加一
        ++modCount;
        //如果插入后的键值对数量超过threshold,则进行扩容操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

小结:
对于键值对的put操作,需要注意的地方有:哈希值的求取、哈希桶定位、链表与红黑树转换、扩容以及扩容之后键值对的移动(保持原有链表的顺序),key为null元素的插入位置、键值对在链表尾部插入等;

put操作的流程梳理:
首先,判断当前数组是否还没有初始化,没有初始化先初始化,如果初始化过了,那么进行哈希值的求取以及哈希桶的定位;定位之后,根据数组元素是否为null,进行分情况处理;如果元素为null,直接插入键值对,如果不为null,则判断是否键相等,相等替换旧值,如果不相等,进入下一步;判断数组元素的类型,如果是红黑树,则按照红黑树的方法进行插入,如果是链表则按照链表的方式进行插入,插入的过程中需要判断是否需要进行链表与红黑树之间的转换;插入之后,判断是否需要进行扩容,如果需要扩容,则进行扩容;否则,方法结束

类似的常用操作还有删除键值对:remove(Object key),获取键值对:get(Object key)等等,但是核心的东西在上面的方法中已经给出,不同的方法关键的东西差不多,所以这里不再详述;


通过分析我们对HashMap的关键实现已经有了一个大概的理解,下面回过头来看一下文章开头提到的HashSet,现在看HashSet就可以知道,底层采用的是HashMap来实现的,放入HashSet中的元素都是作为键值对的key放入底层的HashMap实例中的,而键值对中的value对于HashSet来说是无关紧要的,所以每一个键值对都会共用一个Object对象作为value;

既然是基于HashMap实现HashSet的,那么HashSet中的常用操作也就利用HashMap中对应的方法来实现,因此这里就不给出源码分析,相信理解了HashMap的操作就可以类推出HashSet的操作是如何实现的!


以上就是Set家族和Map家族中的最常用的实现类成员,通过上述的分析,对于这两个家族有了比较深的认识和理解,其实对于HashMap其实还有一个比较关键的东西尚未提及,比如JDK1.8之前,多线程并发操作HashMap实例可能会出现死循环,导致CPU占用率一致飙高等问题,但是由于本文只是对常用实现类的常用操作的实现进行分析,故此处不再赘述其他内容;

总结:
通过本文以及分布式Java应用之集合框架篇(上)这两篇文章,我们对于集合框架中的常用实现类有了一定的认识,但是细细观察可以发现,其实这些常用实现类中大都不是线程安全的,在多线程、高并发大行其道的今天,对于线程安全的问题十分关注,那么,我们就必须掌握如何在并发的场景下使用这些集合类,后面将会对JDK中并发包JUC中的常用类进行分析,由于个人的理解能力有限,因此有不对的地方还希望读者可以指出,不胜感激!

猜你喜欢

转载自blog.csdn.net/boker_han/article/details/82914815