java jdk 源码解析之util包:map集合类源码分析

       map 集合为我们提供了一种映射关系,其中是以元素对的形式储存的(entry<k,v>对象),每个键对应着一个值。在实际应用中为了让我们能够更为了好的处理这种数据形式,java jdk提供了很多对应不同场景的map集合类。其中主要有hashmap,treemap,linkhashmap,concurrenthashmap这四个是我们今天主要分析的集合类。

map类的定义如下:

  • 每个元素释义键值对的形式存在的,集合通过键去访问值。
  • 每个键只能对应一个值,但是每个值能对应多个键
  • 储存的每个键不能相同,相同则会将所对应的值进行覆盖

开始前我们先那其中一个类看他们的继承关系

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

  Map<K,V> ,AbstractMap 作为接口和继承它的抽象方法,为map集合类提供了一系列规范。

1.AbstractMap

       作为map的抽象类,为map实现了大部分的方法,大部分的方法都是通过调用一个叫entrySet的抽象方法来获取一个Set<Entry<K,V>>集合进行操作的,所以我们只需要继承它并实现entrySet方法,就可以创建一个map类。

2.Entry<k,v>

     Map.Entry<k,v> 中的内嵌接口类,实现它用于保存键与值的关系。通过读取entry对象能一次性返回键值对

2.HashMap

    hashmap适用于常规的删除插入操作,但输出的顺序是无序的。  

    hashMap集合中以node对象实例为存储单元,node类是Entry<k,v>类的实现,是一个静态内部类。node源码如下:

扫描二维码关注公众号,回复: 6511772 查看本文章
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 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;
        }
}

    在hashmap中当我们要插入一个键值对时,先计算出key中的哈希值,通过哈希值去计算出数组的下标并定位到对应对象中,如果当前对象不存在则新建一个node对象,反之存在便进行判断执行更新操作。其put源码和注释如下:

/**
     * Implements Map.put and related methods
     *
     * @param hash key的哈希值
     * @param key the key
     * @param value  
     * @param onlyIfAbsent 如果是true 不能替换存在值
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */ 
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)  //如果为空,则调用resize创建集合对象,初始化阀值
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)  //根据hash值算出数组下标并返回table中对应的对象,为空则创建新的节点
            tab[i] = newNode(hash, key, value, null);
        else {   //如果不为空
            Node<K,V> e; K k;   //e定义为需要更新的节点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) //判断当前节点是否是需要更新的节点,如果是则使e指向当前节点
                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) // 当链表内的元素大于等于TREEIFY_THRESHOLD(默认值为8) 会将其转换成树结构
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))  //如果当前节点的hash和key相等则停止循环
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key  e指向节点不为空
                V oldValue = e.value;    
                if (!onlyIfAbsent || oldValue == null) //如果可以覆盖旧节点值则更新
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)  //threshold为需要扩容的阀值
            resize();
        afterNodeInsertion(evict);
        return null;
    }

       以上为jdk8中hashmap的源码,和jdk7主要的区别就是链表部分,当元素超过TREEIFY_THRESHOLD 个时会将链表改成树结构(treeifyBin方法),是对jdk7版本的优化

       除了hashmap 还有一个HashTable的集合类,他的用法很类似也是根据hash值和key对元素进行定位存储,不同的是HashTable没有链表结构,只有线性结构,并且他是线程安全的。这个类是为了兼容旧版本功能才会保留至今,所以官方注解有了下面这段话:

* Java Collections Framework</a>.  Unlike the new collection
* implementations, {@code Hashtable} is synchronized.  If a
* thread-safe implementation is not needed, it is recommended to use
* {@link HashMap} in place of {@code Hashtable}.  If a thread-safe
* highly-concurrent implementation is desired, then it is recommended
* to use {@link java.util.concurrent.ConcurrentHashMap} in place of
* {@code Hashtable}.

     大致意思是 HashTable是同步的,如果不需要线程安全的话推荐使用HashMap,如果是希望在高并发环境下使用的推荐用ConcurrentHashMap中代替HashTable

3.LinkHashMap

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

    /**
     * 记录链表头部
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * 链表尾部
     */
    transient LinkedHashMap.Entry<K,V> tail;

...
}

      继承hashMap实现了双链表功能,他的元素node类里增加了before和after两个变量用于记录上个和下个元素。

 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中newCode方法,插入时额外增加了完成记录链表的操作

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;
    }

      linkNodeLast 方法如下

  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;
        }
    }

    完成链表操作。

 4.TreeMap

     TreeMap类是有序的,内部元素按照自然顺序或比较器进行排序。数据结构为红黑二叉树。红黑二叉树主要特征有:

1.根节点(root)为黑色

2.叶子节点总为黑色(默认null为黑色)

3.某一节点为红色那么他的父节点和子节点不能为红色

4.某一节点到任意的子树叶子节点的路径都包含相同数目的黑色节点

    TreeMap类put的思路主要是:先遍历红黑二叉树;查询是否存在含有Key的节点,如果存在则更新节点的value,如果不存在则插入叶节点,并调用 fixAfterInsertion(Entry<k,v> e) 方法调整树结构;put代码主要如下:

public V put(K key, V value) {
        Entry<K,V> t = root;  
        if (t == null) {  //如果是第一个
            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) { //如果设置了比较器遍历树 查找等于key的节点
            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 {
            if (key == null)  //如果没有比较器则key不能为null
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;  //获取key的Comparable对象
            do {  //查找对于key的节点
                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);
        } 
        //如果没有找到key的节点则创建一个节点
        Entry<K,V> e = new Entry<>(key, value, parent);  
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);  //平衡红黑二叉树
        size++;
        modCount++;  //操作次数
        return null;
    }
fixAfterInsertion 方法
  每当插入一个节点时会有以下情况发生
情况一:父节点为黑色,则直接插入无需操作
情况二:当父节点为红色,其兄弟节点为红色时,此时插入点为红色违反特征3,则将父节点及其兄弟节点设置成黑色,爷爷节点设置成红色,
       保证子树规则成立
情况三:当父节点为红色,其兄弟节点为黑色时,设置父节点为黑色,爷爷节点为红色,则将爷爷节点向兄弟节点的方向旋转(如果兄弟节点
       是右节点,则需要插入节点是左节点才能对爷爷节点做旋转,如果节点是右节点则需要父节点进行一次旋转),此时会得到一个黑色
       节点下有两个红色节点的结果,保证子树规则成立。

   /** From CLR */
    private void fixAfterInsertion(Entry<K,V> x) {
        x.color = RED;  //第一次插入的是红色

        while (x != null && x != root && x.parent.color == RED) {  //向下遍历不符合特征3的节点
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { //如果父节点是左边节点
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));  
                if (colorOf(y) == RED) {  //父节点的兄弟节点是红色时无需旋转操作
                    setColor(parentOf(x), BLACK); //将父节点和父节点的兄弟节点设置成黑色,爷爷节点设置成红色,保证符合特征3
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {  //如果是黑色证明为null节点,因为如果不是null就违反特征4
                    if (x == rightOf(parentOf(x))) { //当新节点是右叶子节点插入时旋转父节点
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x))); //旋转爷爷节点,结果变成了一个黑节点下存在两个红节点的子树
                }
            } else {
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;  //设置根节点为黑色,有可能会应为多次变换后根节点变成红色
    }
 

  以上就是我针对map几个常用的map插入方法的分析,不足之处还请大家指点一下,谢谢

   

猜你喜欢

转载自blog.csdn.net/mxy88888/article/details/90761607