HashMap、LinkedMap、TreeMap底层源码分析,jdk1.7,1.8区别

Map的实现类的结构:

Map:双列数据,存储key-value键值对的数据

  • HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value ,如果有线程安全需求,可以用Collections的方法转变为线程安全的map,一般不会用Hashtable
    • 子类-LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
    • 原因:在原有的HashMap底层存储键值对类的结构基础上,在里面添加了一对指针,指向前一个和后一个元素。(LinkedHashSet的底层)对于频繁的遍历操作,此类执行效率高于HashMap。
  • TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序,底层使用红黑树,TreeSet的底层
  • Hashtable:作为古老的实现类;用了许多synchronized修饰,线程安全的,效率低;不能存储null的key和value
    • 子类-Properties:常用来处理配置文件。key和value都是String类型

HashMap的底层:数组+链表(HashSet底层用的就是HashMap) (jdk7及之前)

数组+链表+红黑树 (jdk 8),红黑树防止数组位置上链表太长降低检索效率

面试题:

  • HashMap的底层实现原理?

  • HashMap 和 Hashtable的异同?

    两者底层结构非常相似,hashMap线程不安全,他的key和value都可以是空的

  • CurrentHashMap 与 Hashtable的异同?(暂时不讲)

    CurrentHashMap 实现分段锁,一个Map分为几段,多个线程同时访问多段,同一时间,本来一个Map只能被一个线程访问

三、HashMap的底层实现原理?以jdk7为例说明:
  •  HashMap map = new HashMap():
    
  •  在实例化以后,底层创建了长度是16的一维数组Entry[] table。
    
  •  Entry实体里面封装key和value
    
  •  map.put(key1,value1)//...可能已经执行过多次put...
    
  •  首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
    
  •  如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
    
  •  如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
    
  •  如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
    
  •  如果key1的哈希值和已经存在的==某一个==(有时候应该不止一个,应该比较完整个链表的hashcode)数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法----情况3
     *              进行比较:
     *              如果equals()返回false,此时key1-value1添加成功。
     *              如果equals()返回true:使用value1替换value2。
    
  •  补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。
    
  •  在不断的添加过程中,会涉及到扩容问题,Entry总数量当超出临界值(且要==存放的位置非空,即Entry[i]非空==)时,扩容。因为Entry[i]为空,存储进去不需要进行对比hashcode值,而且说明可能很多索引位置都还是空的,不必急着扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。
    
四、HashMap的底层实现原理?以jdk8为例说明:
  • jdk8 相较于jdk7在底层实现方面的不同:

    ​ 1.new HashMap():底层没有创建一个长度为16的数组

    ​ 2.jdk 8底层的数组是:Node[],而非Entry[]

    ​ 3.首次调用put()方法时,底层创建长度为16的数组(类似ArrayList在JDK 7和JDK 8中区别)

    ​ 4.jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。

    •     4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
          4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
      
  • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16

  • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75

  • threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12

  • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8

  • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64

  • size 已存放多少个Entry,包括链表上的节点,当size>=threshold,扩容
五.jdk7 HashMap源码
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
public HashMap() {
    //调用有参构造器,初始默认容量16,默认加载因子0.75
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
    //初始容量小于0,抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //初始容量大于最大容量
    //static final int MAXIMUM_CAPACITY = 1 << 30;1左移30位
    //00.....1变成010000.....0
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //加载因子小于0
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    // Find a power of 2 >= initialCapacity,找到一个2的倍数,是大于被传进构造器的初始容量,保证Entry数组容量是2的倍数
    int capacity = 1;
    while (capacity < initialCapacity)
        //capacity=capacity<<1,m每次左移1位,从0001变为010,100,1000直到大于初始容量
        capacity <<= 1;
	//加载因子,存储上了到达容量百分之几会扩容
    this.loadFactor = loadFactor;
    //threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//用min方法防止溢出
    // transient Entry[] table;存储数据的数组
    table = new Entry[capacity];
    //
    useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    init();
}
public V put(K key, V value) {
    if (key == null)
    
        //--------------------------------------
        //putForNullKey 一号被引用方法
        //--------------------------------------
        
        return putForNullKey(value);
    //根据算法值hash
    int hash = hash(key);
    
    //根据hash,取得应该存放于哪个位置
    //--------------------------------------
    //indexFor 三号被引用方法
    //--------------------------------------
    
    int i = indexFor(hash, table.length);
    //若table[i]已存值,在table[i]位置的链表遍历,当存在hash值相等的entry,先判断key,和e.key是否引用同一个对象(地址一样)或用equals()判断它们是否属于同一个对象,同一个对象的话更改value值就可以
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    
    //--------------------------------------
    //addEntry 四号被引用方法
    //--------------------------------------
    //table[i]的链表还没有存在该key值的Entry
    
    addEntry(hash, key, value, i);
    return null;
}

  • 一号被引用方法
private V putForNullKey(V value) {
	//将key=null的Entry存放于table[0](Entry数组),检索table[0]和它连接的链表是否已有key=null的Entry,没有直接添加,有的话更改那个节点的value值,没有直接添加节点
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            //recordAccess HashMap这个方法为空,子类中实现了它--在下方
            //--------------------------------------
        	//recordAccess 二号被引用方法
        	//--------------------------------------
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

  • 二号被引用方法
//LinkedHashMap中实现了这个方法。
void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    		//accessOrder为true时,即当前被修改节点使用的是最近最少使用的次序
            if (lm.accessOrder) {
                //modCount修改次数
                lm.modCount++;
                //删除当前被修改节点
                remove();
                //将当前被修改的节点移动到header节点之前,即链表的尾部。             
                addBefore(lm.header);
            }
        }

  • 三号被引用方法
static int indexFor(int h, int length) {
    //根据h值,即hash找到元素应该在Entry数组存放位置
    //&与%完全没关系,但效率更高
    //&按位与操作符 两个操作数中位都为1,结果才为1,否则结果为0,即保存下两者间二进制数位都是1的位置
    //h为得出哈希值hash,length为数组长度
    //假设h=88,length=16,h=01011000,length-1=00001111
    //h&(length-1)=00001000=8
    return h & (length-1);
}
  • 四号被引用方法
void addEntry(int hash, K key, V value, int bucketIndex) {
    //size(已存放数据,包括链表上节点)大于等于临界值,且要存放数据的位置table[bucketIndex]已有数据,才扩容,因为即使超过临界值,但是所在索引为空存放进去也不麻烦,没有遍历流程,没必要立即扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //扩容为用来2倍
        //扩容改变数组长度length改变,hash可能也改变了,所以调用indexFor方法时得到的值,即存放于数组的位置改变了,所以链表可能会被改变
        resize(2 * table.length);
        //更新hash值
        hash = (null != key) ? hash(key) : 0;
        //更新bucketIndex值
        bucketIndex = indexFor(hash, table.length);
    }
	
    
     //--------------------------------------
     //createEntry 五号被引用方法
     //--------------------------------------
	//创建Entry同时头插法插入table[bucketIndex]
    createEntry(hash, key, value, bucketIndex);
}
  • 五号被引用方法
void createEntry(int hash, K key, V value, int bucketIndex) {
    //保存第一个table[bucketIndex]
    Entry<K,V> e = table[bucketIndex];
    //头插法插入Entry节点,注意之前的头节点e传入创建Entry的构造方法,保存在了创建的next指针
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    //说明了size是hashMap中所有Entry数量
    size++;
}
六.jdk8 HashMap源码

​ 与jdk7不同,初始化HashMap时并没有立即创建数组

public HashMap() {
    //加载因子为0.75
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

​ 存储数据实体变为Node

transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> 
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;
    
    //创建HashMap数组后的首次添加才进入if
    //注意---------当table非空,会检测 (n = tab.length) == 0)同时 给n赋值n=tab.length
    //注意给tab赋值 tab = table
    if ((tab = table) == null || (n = tab.length) == 0)
        ///
        //调用resize()  一号被调用方法
        ///
        //初始化Node数组
        n = (tab = resize()).length;
    
    //当插入的位置为空,直接插入
    //注意 同时给p赋值要插入那个位置的Node,给i赋值 要插入数组的位置的索引
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    
    //插入位置非空
    else {
        Node<K,V> e; K K;
        
        //p(即table[i])的key等于传进来的传参进来的key
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //e占存p节点
            e = p;
        
        //p是红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        
        else {
            
            
            for (int binCount = 0; ; ++binCount) {
                //为什么判断key是否一样放后面,因为第一个节点已经在前面判断equals
                //尾插法,节点插入结尾
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //当链表的长度大于TREEIFY_THRESHOLD(8),执行						treeifyBin方法判断是否要变成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1)    
                        
                //       
        		//treeifyBin()  二号被调用方法
        		///
                        treeifyBin(tab, hash);
                    break;
                }
                
                //e的key与传参key相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    //此时e!=null
                    break;
               
                p = e;
               
               //只有两者方式能退出循环
                //第一种,遍历到了尽头,e=p.next=null,不进入下方if结果改变节点value值
                //第二种,找到key相等节点,e!=null,进入下方if结构,改变value值
            }
        }
        
        
        //节点e的key和要存储的key一样,所以更改evlaue就行
        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;
}
  • 一号被调用方法
final Node<K,V>[] resize() {
    //保存之前旧Node数组
    Node<K,V>[] oldTab = table;
    //oldCap为Node数组改变前的大小,刚创建HashMap,第一次添加数据为0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //临界值一开始为0
    int oldThr = threshold;
    int newCap, newThr = 0;
    //旧Node数组容量大于零
    if (oldCap > 0) {
        
        
        //当扩容前数组大小已经为了默认最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            //更改临界值为数组最大值保证之后不会扩容
            threshold = Integer.MAX_VALUE;
            //无法扩容,直接返回
            return oldTab;
        }
        
        
        //oldCap进行扩容,左移一位,变成2倍时还小于最大容量,且旧容量大于等于 默认的初始化容量
        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 {               
        //创建完HashMap第一次put数据才会进入分支
        //新容量和新临界值
        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;
    @SuppressWarnings({"rawtypes","unchecked"})
    //创建新Node数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //更改指向
    table = newTab;
    //当旧Node数组非空,得进行复制工作
    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;
}
  • 二号被调用方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //MIN_TREEIFY_CAPACITY=64,当数组小于64或数组为空,即使链表长度大于8,也不要改变链表为红黑树,而是扩容,就是你链表太长原因可能是数组长度太小,导致节点都挤到链表上,所以直接扩容一下,增加数组大小,这样改变了节点的存放位置,就不会挤在一起
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
     
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
七.LinkedHashMap
  • LinkedHashMap的底层实现原理(了解)

  • 源码中:

    //存储数据的实体改变了,增加了两个指向前后指针,按照添加进Map的时间顺序指向节点,可以按添加时间遍历实体
    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);
         }
     }
    
    //增加头节点和尾节点
    /**
     * 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;
    

纯手打不易,望点赞支持

你可能感兴趣的:(java基础,java)