Map源码解析

时间:2022-07-24
本文章向大家介绍Map源码解析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

聊一下Map。主要有以下几个类:

(1)HashMap

(2)Hashtable

(3)ConcurrentHashMap

(4)LinkedHashMap

(5)WeakHashMap

环境是java8,上述hashMap和ConcurrentHashMap在java7的时候实现会有不同。

Map主要是用来存储<K,V>键值对,那么现在来一个一个看一下底层是如何进行实现的。

一、HashMap

这是平常最常用的一个类,也是非线程安全的。都知道是数组+链表实现的,那具体是如何实现呢?hash冲突如何进行解决呢?

(1)默认容量、加载因子

//默认容量为16 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//容量*加载因子,默认是12,扩容时会相应的变化
int threshold;

何为加载因子?

用于在元素个数超过(容量X加载因子)时,就会进行扩容。

(2)底层数据结构

是为一个Node类型的数据

transient Node<K,V>[] table;

而Node是一个实现了Map.Entry的类,这个是用来构成链表的

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}

(3)put方法解析

put方法会调用putVal方法,所以主要来看这个东西。根据hash值计算索引(也即是数组中的下标),如果对应索引上为空,则new一个node填充,如果不为空,则看是否有hash冲突的情况,hash和key都一样,不做任何操作;只是hash一样,即是hash冲突,此时往链表尾部插入一个节点,同时如果链表长度大于8,会将链表转化成红黑树。(这是Java1.8优化的,是为了优化查询,如果链太长,会在查询的时候耗费大量时间,所以转成红黑树。这个也是java7和java8的区别)

【注意:在发生hash冲突的时候,不会对原有值进行操作。】

/**
  *hash:key的hash值
  *onlyIfAbsent:如果是true,当key已存在时不改变原有值
  */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
        //如果table为null或者容量为0,触发resize
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据hash值计算索引,如果对应为null,新创建一个Node  
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            HashMap.Node<K,V> e; K k;
            //如果根据hash得到的索引有值,并且hash一样,key也一样
            //则将原有值进行返回,并不做任何操作 
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判断对应的链表是否是红黑树
            else if (p instanceof HashMap.TreeNode)
                //调用红黑树的putTreeVal方法
                e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //此块是解决Hash冲突的关键 
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //往链表中插入一个节点 
                        p.next = newNode(hash, key, value, null);
                        //如果链表长度大于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;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果当前容量已经超过了(容量*加载因子)
        //进行扩容 
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

(4)resize方法,扩容

两倍扩容,同时扩容阙值也会变成之前的两倍,扩容之后重新计算所有元素的hash值并重新计算索引

Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //扩容为原来的两倍 
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //阙值也会相应的扩为原来的两倍  
                newThr = oldThr << 1; // double threshold
        }
        threshold = newThr;
        
        //省略重新计算元素hash值的代码
}

二、Hashtable

一般这个会在面试的时候比较常问,比如HashMap跟Hashtable的区别?

主要有几个

(1)Hashtable是线程安全的,用Synchronized在方法层面进行加锁

(2)初始容量是11,加载因子一样,扩容为原有容量的两倍加1

protected void rehash() {
        int oldCapacity = table.length;
        int newCapacity = (oldCapacity << 1) + 1;
}

其他跟HashMap基本一样。

三、ConcurrentHashMap

这个属于JUC里面的。属于多线程并发安全。key和value都不能为空。

不一样的是其在设置值的时候用的是CAS(比较并交换,之前聊过)

casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))

Node类型数组是用volatile修饰的,

transient volatile Node<K,V>[] table;

跟HashMap同样是数组+链表的方式实现的,也会相应的在链表超过8的时候会转成红黑树。

接下来看一下put,put方法调用的putVal方法

 V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                //初始化
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //根据hash值得到数组对应下标的元素为null是,
                //用CAS进行填充 
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   
            }
            //当hash=-1时,进行扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //用synchronized保证多线程并发
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                      //省略,基本跟hashMap一样
                    }
                }
                if (binCount != 0) {
                    //如果链的长度大于8,则转化哼红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //扩容主要看此方法
        addCount(1L, binCount);
        return null;
    }

扩容主要是在addCount方法里面

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        //这里的CAS主要是看当前元素数量是否超过阈值(容量*加载因子)
        //比如容量为16的话,BASECOUNT是阈值,baseCount是当前元素数量  
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
             //省略无关代码
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            //sizeCtl元素数量
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    //省略。。。
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    //最终扩容
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

最终扩容方法实在transfer方法里面

  private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //省略。。。
        if (nextTab == null) {            // initiating
            try {
                //n<<1,两倍扩容
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }

            
        }
        //省略。。。
}

可以看到,扩容方式跟hashMap一样,也是两倍扩容。

这个比较复杂,基本就是跟Hashtable一样的,Hashtable也是线程安全的,但是Hashtable在读和写时都只能一个线程操作,而ConcurrentHashMap只在写的时候有加锁,读是分享的。

这个是java8的ConcurrentHashMap,而java7的实现是不一样的,java7是用Segment[]+HashEntry[]实现的,分段锁技术,先用key的hash值获取到哪一个Segment,然后再Segment中用hash值获取到具体的索引。但是查询效率低,所以改成java8的模式。

java7的结构:

java8的结构:(与HashMap是一样的)

四、LinkedHashMap

双向链表,继承自HashMap,没有重写put方法,初始容量,加载因子和扩容方式都跟HashMap一样。

双向链表是因为其Entry不同于HashMap,多了一个before引用。

结构如下:

五、WeakHashMap

弱键HashMap,具有跟HashMap一样的初始容量和加载因子,在系统GC的时候一定会被回收。之前在垃圾回收机制的时候说过弱引用的回收机制。

如果代码中有缓存需要的话,建议使用此类。

(1)底层也是数组加链表

//Entry数组
Entry<K,V>[] table;
//链表entry是继承自WeakReference的
class Entry<K,V> extends WeakReference<Object> 
    implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }

(2)扩容也是两倍

上述就是Map的相关解析。难搞,英语不好看源码注释都看不太懂