MAP学习

2018-09-24  本文已影响12人  uranusleon

MAP

学习目标

Java中的Map主要有哪几种?之间有什么区别。链接:链接:https://t.zsxq.com/j2fAQfu
Java中遍历Map的几种方式。链接:链接:https://t.zsxq.com/ujYFUfu
HashMap和HashTable有何不同?链接:链接:https://t.zsxq.com/e66QNr3
HashMap 和 ConcurrentHashMap 的区别?链接:链接:https://t.zsxq.com/Rb6uVVv
同样是线程安全的Map,HashTable和ConcurrentHashMap之间有什么区别?链接:链接:https://t.zsxq.com/q3Nnaqf
hashCode()和equals()方法的作用,二者有什么关系?链接:链接:https://t.zsxq.com/rRZBeAY
HashMap和TreeMap的区别是什么?链接:链接:https://t.zsxq.com/a23JYr7
ConcurrentHashMap和LinkedHashMap有什么区别?链接:链接:https://t.zsxq.com/UF6yrJA
所有的类都可以作为Map的key吗?有什么需要注意的吗?链接:链接:https://t.zsxq.com/vNJAmAI
什么是ConcurrentSkipListMap?他和ConcurrentHashMap有什么区别?链接:链接:https://t.zsxq.com/vB2NbuN

What: Map是什么,有哪些实现

Java中的Map主要有HashMap, HashTable, LinkedHashMap和ConcurrentHashMap几种常用的。

四种Map的区别

HashMap

wiki

HashMap概述

HashMap的API

void                 clear()
Object               clone()
boolean              containsKey(Object key)
boolean              containsValue(Object value)
Set<Entry<K, V>>     entrySet()
V                    get(Object key)
boolean              isEmpty()
Set<K>               keySet()
V                    put(K key, V value)
void                 putAll(Map<? extends K, ? extends V> map)
V                    remove(Object key)
int                  size()
Collection<V>        values()

HashMap的数据结构

HashMap的存储结构是一个Node<K,V>数组,存放Node<K,V>实体;

transient Node<K,V>[] table;

Node<K,V>的定义

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

    // 一个hash节点包括hash值,键值k,值v和下一个节点
    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; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    // 判断两个Entry相等的条件:key和value都相等。
    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存储方法

HashMap的底层主要是基于数组和链表实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储位置的。HashMap中主要是通过key的hashCode来计算hash值,然后通过hash值选择不同的数组来存储。只要hashCode相同,计算出来的hash值就一样,如果存储对象多了,就有可能不同的对象计算出来的hash值是相同的,这就出现了所谓的hash冲突,解决hash冲突的方法很多,具体可以参见我这篇博客:数据结构与算法07 指哈希表。HashMap的底层是通过链表来解决hash冲突的。

图片

图中紫色部分代表哈希表,其实就是哈希数组,数组的每个元素都是一个单链表的头结点,链表是用来解决hash冲突的,如果不同的key映射到了数组的同一位置,那么就将其放入单链表中。下面的图可能在代码的角度更能说明问题:

图片

源码分析(基于jdk1.8)

关键的变量

//默认初始容量是16,必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 
//最大容量(必须是2的幂且小于2的30次方,传入容量过大会被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
 
//默认加载因子,所谓加载因子是指哈希表在其容量自动增加之前可以达到多满的一种尺度
static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
//存储Entry的默认空数组
static final Entry<?,?>[] EMPTY_TABLE = {};
 
//存储Entry的数组,长度为2的幂。HashMap采用拉链法实现的,每个Entry的本质是个单向链表
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
 
//HashMap的大小,即HashMap存储的键值对数量
transient int size;
 
//HashMap的阈值,用于判断是否需要调整HashMap的容量
int threshold;
 
//加载因子实际大小
final float loadFactor;
 
//HashMap被修改的次数,用于fail-fast机制
transient int modCount;

我们主要来看看loadFactor属性,loadFactor表示Hash表中元素的填满程度。

若加载因子设置过大,则填满的元素越多,无疑空间利用率变高了,但是冲突的机会增加了,冲突的越多,链表就会变得越长,那么查找效率就会变得更低;

若加载因子设置过小,则填满的元素越少,那么空间利用率变低了,表中数据将变得更加稀疏,但是冲突的机会减小了,这样链表就不会太长,查找效率变得更高。


构造方法

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

从上述代码可以看出HashMap主要设置loadFactorthreshold两个变量


HashMap中的红黑树

使用红黑树的原因是查找时间复杂度为O(lgn).

红黑树的五条性质

  1. 每个结点要么是红的,要么是黑的。
  2. 根结点是黑的。
  3. 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。
  4. 如果一个结点是红的,那么它的俩个儿子都是黑的。
  5. 对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。

红黑树重点关注点

treeify:将链表转化为红黑树
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) { // 初始化数的根节点
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                // 比较插入元素和当前元素hash值大小,当前元素初始为root;
                // dir表示比较的结果
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                // 针对hash值相等的情况进行判断
                // 首先通过comparableClassFor方法判断key是否定义了compare方法;
                // 然后通过compare方法比较大小
                // 如果都相等,调用tieBreakOrder方法比较大小
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}
balanceInsertion方法:新增元素后平衡红黑树

红黑树插入节点后需要恢复红黑树的性质,首先考虑父节点是祖父节点的右孩子还是左孩子;

如果父节点是祖父节点的左孩子,需要分为下面三种情况

如果父节点是祖父节点的右孩子,和上述情况类似,只不过对于情况3,进行右旋后,可以变为情况2。

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
    x.red = true;
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        // 需要考虑当前节点的父节点是祖父节点的左孩子还是右孩子
        if (xp == (xppl = xpp.left)) {
            // 修复情况1:当前结点的父结点是红色,并且祖父结点的另一个子结点(叔叔结点)是红色
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                // 修复情况2:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的右子
                if (x == xp.right) {
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                // 修复情况3:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的左子
                if (xp != null) {
                    // 父结点变为黑色
                    xp.red = false;
                    if (xpp != null) {
                        // 祖父结点变为红色
                        xpp.red = true;
                        // 在祖父结点为支点右旋
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        else {
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.left) {
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

putTreeVal()方法
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                               int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    TreeNode<K,V> root = (parent != null) ? root() : this;
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

rotateLeft:左旋和rotateRight:右旋

rotateLeft方法的输入参数为root和将进行左旋的节点p,左旋的意义在于将节点p变为其右孩子的左孩子。

方法中使用r表示节点p的右孩子,pl表示节点p的左孩子,zhu

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
    TreeNode<K,V> r, pp, rl;
    if (p != null && (r = p.right) != null) {
        // 将节点r的左孩子rl变为节点p的右孩子;
        if ((rl = p.right = r.left) != null)
            rl.parent = p;
        // 将r的父节点更换为p的父节点
        // 如果p为root节点,则将r变为root节点;
        if ((pp = r.parent = p.parent) == null)
            (root = r).red = false;
        // 需要判断p是父节点的左孩子还是右孩子
        else if (pp.left == p)
            pp.left = r;
        else
            pp.right = r;
        // 将p作为r的左孩子
        r.left = p;
        p.parent = r;
    }
    return root;
}

rotateRight方法的输入参数为root和将进行右旋的节点p,右旋的作用在于将节点p变为其左孩子的右孩子。

方法中使用l表示节点p的右孩子,pl表示节点p的左孩子

static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                               TreeNode<K,V> p) {
    TreeNode<K,V> l, pp, lr;
    if (p != null && (l = p.left) != null) {
        // 将节点l的右孩子lr变为节点p的左孩子
        if ((lr = p.left = l.right) != null)
            lr.parent = p;
        // 将l的父节点更换为p的父节点
        // 如果p为root节点,则将l变为root节点
        if ((pp = l.parent = p.parent) == null)
            (root = l).red = false;
        else if (pp.right == p)
            pp.right = l;
        else
            pp.left = l;
        // 将p作为l的右孩子
        l.right = p;
        p.parent = l;
    }
    return root;
}

区分左旋和右旋的方法:

  • 对某一节点P进行左旋和右旋,作用都是将节点变为孩子节点的左孩子或者右孩子;
  • 如果将节点P变为做孩子节点的左孩子,则要求P的值比孩子节点的值小,所以是将节点P变为右孩子节点的左孩子;
moveRootToFront方法

在将链表转化为红黑树后,Root节点可能不是tab中对应索引的元素,通过转换可以将tab对应索引的元素设置为Root。

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        int index = (n - 1) & root.hash;
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        if (root != first) {
            Node<K,V> rn;
            tab[index] = root; // 将tab对应索引的元素设置为Root。
            TreeNode<K,V> rp = root.prev;
            if ((rn = root.next) != null)
                ((TreeNode<K,V>)rn).prev = rp;
            if (rp != null)
                rp.next = rn;
            if (first != null)
                first.prev = root;
            root.next = first;
            root.prev = null;
        }
        assert checkInvariants(root);
    }
}

hash的计算:hash()方法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

图片

计算hash值可以分为两部分

  1. 高位运算:h^(h >>> 16):右移16位,正好是32位的一半,高半区和低半区做异或,是为了混合原始哈希码的高位和低位,加大低位的随机性。

  2. 取模运算:(n-1) & hash:可以解释HashMap的数组长度是2的整数幂

    首先,h & (length-1)相当于h % length,但是h % length效率比较低(HashTable中是这儿干的)。为啥h & (length-1)相当于h % length呢?现在假设length为2的幂,那么length就可以表示成100......00的形式(表示至少1个0),那么length-1就是01111....11。对于任意小于length的数h来说,与01111...11做&后都是h本身,对于h=length来说,&后结果为0,对于大于length的数h,&过后相当于h-j*length,也就是h % length。容量为2的整数幂,方便做&运算,效率高。

    其次,length为2的次幂的话,是偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h & (length-1)的最后一位可能为0也可能为1(取决于h的值),即结果可能为奇数,也可能为偶数,这样便可以保证散列的均匀性,即均匀分布在数组table中;而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h & (length-1)的最后一位肯定为0,级只能为偶数,这样任何hash值都会被映射到数组的偶数下标位置上,这便浪费了近一半的空间!因此,length去2的整数次幂,也是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀的散列。


扩容方法:resize()方法

final 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) { // 如果扩容前数组大小以及达到最大值,修改阈值为为Int的最大值,则以后不会再扩容。
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        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;
    @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; // 释放旧的Node列表中的引用
                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;
}

resize()方法可以分为两部分

jdk1.8中计算元素在新数组位置的方法和jdk1.7中相同,实现方法不同。可以参考wiki:[Java 8系列之重新认识HashMap](


插入,删除,查找,修改

插入方法
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)
        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(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

首先判断table是否为null或者是否长度为0,如果table为null或者长度为0,则进行扩容;

然后计算key的hash值并与数组长度做取模运算,获得新的节点在table中的索引;

判断table中索引节点的情况


https://tech.meituan.com/java_hashmap.html)


treeifyBin()方法

treeifyBin()的主要作用是将链表中的元素全部转化为红黑树的节点TreeNode,并且按照顺序排列,最后调用TreeNode类的方法treeify()将链表转化为红黑树。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 判断是需要扩容或者树形化
    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);
    }
}

查找方法
查找单个节点
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
keyset
上一篇下一篇

猜你喜欢

热点阅读