Java集合框架——Map

前言

下图为Map接口以及其相关子类实现类的简易结构图:
在这里插入图片描述
接下来对上图中的实现类的原理及使用方面进行一个简单的介绍。

1、HashMap类

    Hash的底层实现为数组+链表/红黑树的结构形式,在JDK1.8之前HashMap底层实现为数组+链表的形式,JDK1.8以后添加了红黑树,至于何时进行链表和红黑树的转换,待下面分析源码的时候进行讲解;先来展示下HashMap类的继承实现关系:

public class HashMap<K,V> 
	extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
    .........}

下面是一个典型的HashMap的结构图:

图片来源于网络

1.1、源码分析

接下来看下实现类中的静态变量及成员变量:

    /**
     * 设置table初始化时的容量默认值为16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 设置数组最大的容量值为2的30次方
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认负载因子为0.75
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 以下三个静态常量是链表和红黑树相互转换的阈值因子;
     * 链表和红黑树相互转换的条件:
     * 	1)链表中的元素个数大于8个,同时数组table的元素个数大于64时才会转换为红黑树
     * 	2)如果红黑树中的节点个数小于UNTREEIFY_THRESHOLD,则由红黑树转化为链表
     */
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64;
	
	/********************成员变量**************************/
	/**
     * table变量是一个Node数组,负责存储键值对
     */
    transient Node<K,V>[] table;

    /**
     * 存放具体元素的集合
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * 记录map中键值对的个数
     */
    transient int size;

    /**
     * 计数器,记录hashMap结构变化的次数
     */
    transient int modCount;

    /**
     * 设置扩容的阈值,大小为数组table容量*负载因子
     * 例如:默认容量为16*默认负载因子0.75=12,当key-value的个数大于12时进行扩容操作
     * 
     * 扩容后hashmap的容量为之前的2倍
     */
    int threshold;

    /**
     * 真实装载因子,不设置默认为0.75;可以通过构造函数来修改
     */
    final float loadFactor;

HashMap的构造函数:

/**
 * 指定初始化容量和负载因子的构造函数
 */
public HashMap(int initialCapacity, float loadFactor) {
    
    
		// 判断初始化容量initialCapacity是否小于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // 判断初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY->2的30次幂                                   
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        // tableSizeFor(initialCapacity) 判断指定的初始化容量是否是2的n次幂,
        // 如果不是那么会变为比指定初始化容量大的最小的2的n次幂。
        this.threshold = tableSizeFor(initialCapacity);
    }
	// tableSizeFor() 实现,可以自己测试,返回的数据刚好如上边说的一样:如果不是2的n次幂那么会变为比指定初始化容量大的最小的2的n次幂
	static final int tableSizeFor(int cap) {
    
    
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

    /**
     * 使用默认的负载因子,指定初始化容量
     */
    public HashMap(int initialCapacity) {
    
    
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 使用默认参数的无参构造函数
     */
    public HashMap() {
    
    
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }

    /**
     * 根据输入Map构造一个新的HashMap对象
     */
    public HashMap(Map<? extends K, ? extends V> m) {
    
    
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    
    
    	// 获取参数集合的长度
        int s = m.size();
        // 判断参数集合的长度是否大于0
        if (s > 0) {
    
    
        	// 判断table数组是否初始化
            if (table == null) {
    
     // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                // 如果t大于阈值,则初始化阈值
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            // 已经初始化且s大于阈值,则进行扩容操作
            else if (s > threshold)
                resize();
            // 遍历执行插入的操作
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
    
    
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

注意:在上述中我们讲到tableSizeFor()会将设定非2的n次幂的容量值转化为大于该值的最小2的n次幂的值;threshold 代表的是数组的阈值,等于数组容量*负载因子;但是上面介绍的 threshold = tableSizeFor(容量),这是因为这里计算的threshold只是一个初始化的值,而在进行put的时候会对threshold重新计算。

添加数据的方法:

	//先来介绍下hashMap中对key进行hash的方法
	 static final int hash(Object key) {
    
    
        int h;
        /**
         * 1)当key为null时,hash的值为0
         * 2)当不为null时,使用key的hashCode值h与h右移16位以后的值进行或非(^)的操作
         */
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

	// 注意:这个hash()计算出来的并不是元素在数组table中的位置,如果要计算在数组中的位置还要进行下一步的操作:
		// 使用hash()计算出来的值与数组table的容量值n减去1进行求与(&)的操作
		// int index=(n - 1) & hash;
	

	/**
	 *	Put方法的实现步骤大致如下:
	 *	 1)先通过hash值计算出key映射到哪个bucket,即计算出table中的索引;
	 *	 2)如果bucket上没有碰撞冲突,则直接插入;
	 *	 3)如果出现碰撞冲突了,则需要处理冲突:
	 *		a、如果该bucket使用红黑树处理冲突,则调用红黑树的方法插入数据;
	 *		b、否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
	 *   4)如果bucket中存在重复的键,则为该键替换新值value;
	 * 	 5)如果size大于阈值threshold,则进行扩容;
	 */
    public V put(K key, V value) {
    
    
        return putVal(hash(key), key, value, false, true);
    }

	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    
    
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        /*
	    	1)transient Node<K,V>[] table; 表示存储Map集合中元素的数组。
	    	2)(tab = table) == null 表示将空的table赋值给tab,然后判断tab是否等于null,第一次肯定是			null
	    	3)(n = tab.length) == 0 表示将数组的长度0赋值给n,然后判断n是否等于0,n等于0
		    	由于if判断使用双或,满足一个即可,则执行代码 n = (tab = resize()).length; 进行数组初始化。
		    	并将初始化好的数组长度赋值给n.
	    	4)执行完n = (tab = resize()).length,数组tab每个空间都是null
   	 */
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
       /*
	    	1)i = (n - 1) & hash 表示计算数组的索引赋值给i;
	    	2)p = tab[i = (n - 1) & hash]表示获取计算出的位置的数据赋值给节点p
	    	3) (p = tab[i = (n - 1) & hash]) == null 判断节点位置是否等于null,如果为null,则执行代码:tab[i] = newNode(hash, key, value, null);根据键值对创建新的节点放入该位置的bucket中
	        小结:如果当前bucket没有哈希碰撞冲突,则直接把键值对插入空间位置
   		*/ 
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
    
    
        	// 当前位置已经有值时
            Node<K,V> e; K k;
            // 判断新插入节点的key是否与当前节点原有的数据的key相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 相同的话则将p赋给e
                e = p;
            // 判断当前节点是否为树节点
            else if (p instanceof TreeNode)
            	// 如果为树节点则使用红黑树的添加方式
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
    
    
            	// 原来的结构为链表格式的结构
            	// 采用循环遍历的方式,判断链表中是否有重复的key,如果有则覆盖value;如果没有则添加到链表的最后
                for (int binCount = 0; ; ++binCount) {
    
    
                    if ((e = p.next) == null) {
    
    
                        p.next = newNode(hash, key, value, null);
                        // 判断链表中的节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树
                        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,说明原来的map中没有与插入的节点相重复的key
            if (e != null) {
    
     // existing mapping for key
                V oldValue = e.value;
                // onlyIfAbsent 如果true代表不更改现有的值
                // onlyIfAbsent=false说明原来的值要被覆盖
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 执行回调
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        // 记录修改的次数
        ++modCount;
        // 修改size,如果map中键值对的个数size大于阈值则进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

将链表转化为红黑树的操作:

  /**
     替换指定哈希表的索引处桶中的所有链接节点
     Node<K,V>[] tab = tab 数组名
     int hash = hash表示哈希值
  */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
    
    
        int n, index; Node<K,V> e;
        /*
        	如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64),就去扩容。而不是将节点变为红黑树。
        	目的:如果数组很小,那么转换红黑树,然后遍历效率要低一些。这时进行扩容,那么重新计算哈希值,链表长度有可能就变短了,数据会放到数组中,这样相对来说效率高一些。
        */
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
    
    
            /*
            	1)执行到这里说明哈希表中的数组长度大于阈值64,开始进行树形化
            	2)e = tab[index = (n - 1) & hash]表示将数组中的元素取出赋值给e,e是哈希表中指定位置桶里的链表节点,从第一个开始
            */
            //hd:红黑树的头结点   tl :红黑树的尾结点
            TreeNode<K,V> hd = null, tl = null;
            do {
    
    
                //新创建一个树的节点,内容和当前链表节点e一致
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    //将新创键的p节点赋值给红黑树的头结点
                    hd = p;
                else {
    
    
                    /*
                    	 p.prev = tl:将上一个节点p赋值给现在的p的前一个节点
                    	 tl.next = p;将现在节点p作为树的尾结点的下一个节点
                    */
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
                /*
                	e = e.next 将当前节点的下一个节点赋值给e,如果下一个节点不等于null
                	则回到上面继续取出链表中节点转换为红黑树
                */
            } while ((e = e.next) != null);
            /*
            	让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,
            	以后这个桶里的元素就是红黑树而不是链表数据结构了
            */
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

扩容原理以及对应的源码

    final Node<K,V>[] resize() {
    
    
        Node<K,V>[] oldTab = table;
        // 获取当前数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //当前阀值点 默认是12(16*0.75)
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
    
    
            if (oldCap >= MAXIMUM_CAPACITY) {
    
    
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            /**
             * 1)通过位移运算,将容量扩大为原来的2倍,且扩大2倍以后仍然要小于最大容量
             * 2)原来的容量大小要大于等于默认的初始化容量16
             */
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {
    
                   
        	// zero initial threshold signifies using defaults
        	// 直接使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            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;
        // 创建新的hash表
        @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 {
    
     // 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;
                        }
                    }
                }
            }
        }
        return newTab;
    }
1.2、hashMap特点及使用建议

特点:

  • 存取无序的
  • 键和值位置都可以是null,但是键位置只能是一个null,因为键不能重复
  • 键位置是唯一的,底层的数据结构控制键的
  • jdk1.8前数据结构是:链表 + 数组 jdk1.8之后是 : 链表 + 数组 /红黑树
  • 阈值(边界值) > 8 并且数组长度大于64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。

使用时建议:

  • 使用时如果知道数据量的大小,尽量使用设置初始容量的构造函数
  • 建议设置初始容量的计算公式为:需要存储的元素个数/ 负载因子(0.75F) + 1.0F

2、LinkedHashMap类

    LinkedHashMap是HashMap的子类,对于数据进行存储的过程与HashMap中的一致;但是也有一定的区别,这里对两者的区别以及LinkedHashMap的特点进行简单总结:

  • HashMap是无序的,但是LinkedHashMap则是有序的,默认是按照数据插入的顺序;
  • jdk1.8以后,HashMap的数据结构为 链表 + 数组 /红黑树,LinkedHashmap数据结构为: 链表 + 双向数组 /红黑树;
  • 键和值位置都可以是null,但是键位置只能是一个null;
  • 两个实现类均为非线程安全。
2.1、源码分析

先来看一下LinkedHashMap的集成关系:

public class LinkedHashMap<K,V> 
			extends HashMap<K,V> 
			implements Map<K,V>{
    
    ......}

LinkedHashMap中新增加的成员变量:

    /**
     * The head (eldest) of the doubly linked list.
     * 双向链表的头节点,最先插入的节点
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     * 双向链表的尾节点,最后插入的节点
     */
    transient LinkedHashMap.Entry<K,V> tail;
    /**
     * 用来设置顺序,默认为false:按照插入的顺序来排序
     * 设置为true:按照访问的顺序来排序,最近访问的数据节点会被插入到链表的最末端
     */
    final boolean accessOrder;

再来看一下LinkedHashMap的真实结构如下:

    static class Entry<K,V> extends HashMap.Node<K,V> {
    
    
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
    
    
            super(hash, key, value, next);
        }
    }

与HashMap相比,LinkedHashMap在原来的基础上添加了两个属性before,after;所以现在Entry(对应HashMap为Node)的属性包括以下几个:

K key
V value
Entry<K, V> next
int hash
// 下面为新增的
Entry<K, V> before
Entry<K, V> after

再来看一下LinkedHashMap的构造函数,LinkedHashMap共包括5个构造函数,如下所示:

	// 与HashMap的构造函数相比,只是在每一个构造函数中多了一个控制顺序的设置,
	// 默认是按照插入的顺序排序
    public LinkedHashMap(int initialCapacity, float loadFactor) {
    
    
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
    
    public LinkedHashMap(int initialCapacity) {
    
    
        super(initialCapacity);
        accessOrder = false;
    }

    public LinkedHashMap() {
    
    
        super();
        accessOrder = false;
    }
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
    
    
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
    
    
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

接下里说一下LinkedHashMap是如何控制顺序的,先来看一下LinkedHashMap获取节点数据的过程,代码如下:

    public V get(Object key) {
    
    
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        // 如果设置了accessOrder=true,则重新进行序列调整;否则按照插入顺序
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }
	// 将已经获取的节点,重新覆写到链表的尾节点
    void afterNodeAccess(Node<K,V> e) {
    
     // move node to last
        LinkedHashMap.Entry<K,V> last;
        // accessOrder=true
        // 且刚获取的节点不在链表的尾部,就将节点移动到链表的尾部
        if (accessOrder && (last = tail) != e) {
    
    
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            // 如果刚获取的节点的前一个节点为null,说明该节点在头部,设置其下一个节点为头节点
            // 否则就将当前节点的下一个节点设置为其前一个节点的下一个节点 
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
    
    
                p.before = last;
                last.after = p;
            }
            tail = p;
            // 计数修改的次数
            ++modCount;
        }
    }

接下来介绍一下LinkedHashMap添加数据的过程,LinkedHashMap添加数据时调用的是父类的put方法,但在具体执行的时候则对其中的一些方法进行了重写,先来看一下父类的put方法:

    public V put(K key, V value) {
    
    
        return putVal(hash(key), key, value, false, true);
    }
    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 ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        	// LinkedHashMap对newNode()进行了重写,下边会介绍
            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当accessOrder=true时,
                 * 将操作(查询、覆盖值)过的节点移动到链尾,具体看上边的源码分析
                */
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        // 这个方法,在jdk1.8中没有用到,具体参见下边的分析
        
        afterNodeInsertion(evict);
        return null;
    }

具体的过程就不解释了,在LinkedHashMap中,对上述方法中的newNode()、putTreeVal()中的newTreeNode()进行了重写,并且实现了两个回调方法afterNodeAccess(上面已经介绍过了)和afterNodeInsertion,下面看一下:

   Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    
    
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }
    // 新建一个节点,并将节点方至链表的尾部
   private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    
    
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
    
    
            p.before = last;
            last.after = p;
        }
    }
    // afterNodeInsertion()没有用到,因为removeEldestEntry()方法始终返回false
   void afterNodeInsertion(boolean evict) {
    
     // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
    
    
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

最后来说一下移除节点的操作,移除的过程大致如下:

  • 先根据key计算节点在数组table中的索引位置
  • 遍历链表或者红黑树删除节点
  • 移除节点以后维护双向链表
    public V remove(Object key) {
    
    
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
	
	final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
    
    
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
    
    
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
    
    
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
    
    
                    do {
    
    
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
    
    
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
    
    
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                // 与HashMap有区别的地方是,移除完节点以后对于双向链表结构的维护
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

    void afterNodeRemoval(Node<K,V> e) {
    
     // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // 将要移除的节点的前驱和后继节点置为null
        p.before = p.after = null;
        // b为null,说明要删除的节点为头节点
        if (b == null)
            head = a;
        else
            b.after = a;
        // a为null,说明要删除的节点为尾节点
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

关于LinkedHashMap暂时就讲到这里,关于未说到的可以自己看源码在研究,跟HashMap的内容差不多。
参考博客:
LinkedHashMap 源码详细分析(JDK1.8)
LinkedHashMap如何保证顺序性

3、TreeMap类

    TreeMap是一种可以实现自排序的数据结构,底层的数据结构为红黑树,红黑树是平衡树中的一种,关于红黑树的性质可以自己下去研究,这里就不展开了,接下来进行源码分析。

3.1 源码分析

先来看一下TreeMap的继承关系:

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
    
    ......}

接下来介绍下实现类中的成员变量及构造函数:

    /**
     * The comparator used to maintain order in this tree map, or
     * null if it uses the natural ordering of its keys.
     * 自定义比较器,可以为null,如果为null时则默认使用key来进行排序
     */
    private final Comparator<? super K> comparator;
	
	// 红黑树的根节点
    private transient Entry<K,V> root;

    /**
     * entry的个数
     */
    private transient int size = 0;

    /**
     * The number of structural modifications to the tree.
     * 结构改变的次数
     */
    private transient int modCount = 0;

    /**
     * 构建一个空的构造函数
     */
    public TreeMap() {
    
    
        comparator = null;
    }

    /**
     * 根据自定义的比较器来构建构造函数
     */
    public TreeMap(Comparator<? super K> comparator) {
    
    
        this.comparator = comparator;
    }

    /**
     * 使用默认比较器将Map类型数据转化为一个新的TreeMap结构
     */
    public TreeMap(Map<? extends K, ? extends V> m) {
    
    
        comparator = null;
        putAll(m);
    }

    /**
     * Constructs a new tree map containing the same mappings and
     * using the same ordering as the specified sorted map.  This
     * method runs in linear time.
     *
     * @param  m the sorted map whose mappings are to be placed in this map,
     *         and whose comparator is to be used to sort this map
     * @throws NullPointerException if the specified map is null
     * 使用已知的SortedMap对象来构建TreeMap
     */
    public TreeMap(SortedMap<K, ? extends V> m) {
    
    
        comparator = m.comparator();
        try {
    
    
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
    
    
        } catch (ClassNotFoundException cannotHappen) {
    
    
        }
    }

再来看一下TreeMap每一个节点中的真实结构:

    static final class Entry<K,V> implements Map.Entry<K,V> {
    
    
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
	}

一个典型的红黑树节点结构,其中color属性用来设置节点的链接是黑链接还是红链接;
添加数据的操作:

  public V put(K key, V value) {
    
    
        Entry<K,V> t = root;
        // 如果根节点为null,则创建根节点
        if (t == null) {
    
    
        	/**
        	* 检查key类型,这一点在构造函数的注释上就可以看到原因
        	* All keys inserted into the map must be <em>mutually
    		* comparable</em> by the given comparator: {@code comparator.compare(k1,
            * k2)} must not throw a {@code ClassCastException} for any keys
            * {@code k1} and {@code k2} in the map.
			*/
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
    
    
        	// 这里执行的是一个红黑树遍历比较的操作
            do {
    
    
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
    
    
        	// 用户没有设定比较器时,使用默认比较器对key进行遍历比较
        	// TreeMap的key值不能为null
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
    
    
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        // 执行完上边的操作以后,节点已经被插入到树中
        // fixAfterInsertion()方法是对插入的节点进行调整,就是红黑树中着色、左旋、右旋使树再次平衡,这里可以参考红黑树的调整过程
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

移除节点:

    public V remove(Object key) {
    
    
        Entry<K,V> p = getEntry(key);
        // 如果没有找到,返回null
        if (p == null)
            return null;
		// 找到以后返回原来的数据
        V oldValue = p.value;
        // 删除指定的entry
        deleteEntry(p);
        return oldValue;
    }

	// 先根据key找到指定的entry
    final Entry<K,V> getEntry(Object key) {
    
    
        // Offload comparator-based version for sake of performance
        if (comparator != null)
        	// 根据特定的比较器来找到entry
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        while (p != null) {
    
    
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }

关于TreeMap这里讲解的比较简单,关于红黑树的具体过程没有详细展开来说,有兴趣的可以自己下去研究,或者参考此篇博客中的内容:【深入理解java集合】-TreeMap实现原理

小结:

  • TreeMap使用时可以自己定义排序比较的规则,可扩展性更好
  • TreeMap的键key不能为null,值可以为null
  • TreeMap有序但是非线程安全

4、HashTable类

    在JDK1.8中,HashTable采用数组+链表的数据结构,大致的结构如下:

在这里插入图片描述
图片来源于网络

4.1、源码分析

首先我们来看一下HashTable的继承关系:

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
    
    ......}

在HashTable中包含有5个成员变量,4个构造函数,接下来我们对这部分内容进行介绍:

    /**
     * 存储数据的Entry数组,与hashMap中的一致
     */
    private transient Entry<?,?>[] table;

    /**
     * 记录entry的个数,即键值对的个数
     */
    private transient int count;

    /**
     * The table is rehashed when its size exceeds this threshold.  (The
     * value of this field is (int)(capacity * loadFactor).)
     * 扩容的阈值等于table的容量*负载因子
     */
    private int threshold;

    /**
     * 负载因子,默认为0.75
     */
    private float loadFactor;

    /**
     * 计数器,记录hashTable结构变化的次数
     */
    private transient int modCount = 0;


	/***********************HashTable构造函数*******************************/

	/**
	 * 构造函数的具体实现:
	 * 	 1)如果设置的容量为0,则默认设置为1
	 * 	 2)初始化数组table
	 *   3) 扩容的阈值为数组的容量(默认为11)*负载因子(默认为0.75)
	 * Hashtable中数组的容量没有限制为2的n次幂,可以取大于0的任意值
	 */
	 public Hashtable(int initialCapacity, float loadFactor) {
    
    
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

    /**
     * Constructs a new, empty hashtable with the specified initial capacity
     * and default load factor (0.75).
     */
    public Hashtable(int initialCapacity) {
    
    
        this(initialCapacity, 0.75f);
    }
    /**
     * Constructs a new, empty hashtable with a default initial capacity (11)
     * and load factor (0.75).
     */
    public Hashtable() {
    
    
        this(11, 0.75f);
    }

    /**
     * Constructs a new hashtable with the same mappings as the given
     * Map.  The hashtable is created with an initial capacity sufficient to
     * hold the mappings in the given Map and a default load factor (0.75).
     *
     * @param t the map whose mappings are to be placed in this map.
     * @throws NullPointerException if the specified map is null.
     * @since   1.2
     */
    public Hashtable(Map<? extends K, ? extends V> t) {
    
    
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }	

接下来看一下HashTable添加数据的过程:

	/**
	* 添加的方法采用synchronized 关键字进行修饰,所以是线程安全的
	*/
    public synchronized V put(K key, V value) {
    
    
        // value不能为空,否则抛出空指针异常;
        if (value == null) {
    
    
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        // key也不能为null,因为null.hashCode会抛出异常
        int hash = key.hashCode();
        // 计算entry在数组table中的索引的计算方法,hash值与2^31-1求与,然后与数组的长度进行求余
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        // 遍历table[index]所连接的所有链表,查找是否有节点的key与要插入的key相同,如果存在则替换
        for(; entry != null ; entry = entry.next) {
    
    
            if ((entry.hash == hash) && entry.key.equals(key)) {
    
    
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
	// 如果不存在相同的key,则执行addEntry操作
        addEntry(hash, key, value, index);
        return null;
    }


    private void addEntry(int hash, K key, V value, int index) {
    
    
        modCount++;

        Entry<?,?> tab[] = table;
        // 判断hashTable中的键值对的个数是否大于等于扩容阈值,
        // 如果大于等于,则执行扩容的操作
        if (count >= threshold) {
    
    
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        // 取出table中index位置的entry给到e,也就是对应位置链表给到e
        Entry<K,V> e = (Entry<K,V>) tab[index];
		// 把新插入的entry插入到链表的第一个节点
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

扩容的操作:

    @SuppressWarnings("unchecked")
    protected void rehash() {
    
    
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
        // 新的数组的容量为原来数组的2倍+1
        int newCapacity = (oldCapacity << 1) + 1;
        // 判断新的数组容量值是否大于最大值
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
    
    
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        //使用新的数组容量初始化一个新的数组
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
        // 计算扩容阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;
		// 循环遍历将原来的数据添加到新的数组中
        for (int i = oldCapacity ; i-- > 0 ;) {
    
    
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
    
    
                Entry<K,V> e = old;
                old = old.next;

                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }

最后看一下移除数据的操作:

    public synchronized V remove(Object key) {
    
    
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        // 先根据key找到table中对应的索引位置处
        Entry<K,V> e = (Entry<K,V>)tab[index];
        // 循环遍历数组index索引处对应的链表
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
    
    
            if ((e.hash == hash) && e.key.equals(key)) {
    
    
                modCount++;
                // prev==null,说明链表中的第一个节点即为要移除的节点
                // 执行的是一个删除链表中节点的操作,相对简单
                if (prev != null) {
    
    
                    prev.next = e.next;
                } else {
    
    
                    tab[index] = e.next;
                }
                count--;
                V oldValue = e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }
4.2、HashMap与HashTable的比较
  • HashMap中的键和值均可以为null,但是HashTable中的键和值均不能为null
  • JDK1.8中HashMap采用数组+链表/红黑树结构实现,而HashTable中采用的是数组+链表实现
  • HashMap中出现hash冲突时,如果链表节点数小于8时是将新元素加入到链表的末尾(大于8时就要扩容操作了),而HashTable中出现hash冲突时采用的是将新元素加入到链表的开头
  • HashMap中数组容量的大小要求是2的n次方,如果初始化时不符合要求会进行调整,而HashTable中数组容量的大小可以为任意正整数
  • 计算entry在数组中的位置的策略不同
  • HashMap不是线程安全的,HashTable为线程安全的,其中的添加、移除、获取数据的方法均添加了synchronized关键字
  • HashMap的默认容量为16,HashTable默认容量为11

猜你喜欢

转载自blog.csdn.net/qgnczmnmn/article/details/108584966