天天看点

Java容器(三)Map一、TreeMap二、HashMap三、ConcurrentHashMap四、LinkedHashMap

Java容器(三)Map一、TreeMap二、HashMap三、ConcurrentHashMap四、LinkedHashMap

一、TreeMap

TreeMap封装了红黑树,在代码里的体现就是private transient Entry<K,V> root; 也就是一个key,value键值对。具体的红黑树操作都在里面,就不细讲了。

1.1 put(k,v)

remove(k)也是和下面道理相似,理解就行。

public V put(K key, V value) {
		// 获得根结点
        Entry<K,V> t = root;
        // 如果根节点为空,那么新建一个Entry(),赋值为根节点
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        
        // 下面这段代码目的是找出key应该插在那个父节点之下
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        // 如果用户自定义了比较器
        if (cpr != null) {
        	// 找到t == null ,那么此时parent就是想要的父节点。
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                // 判断归属于左子树还是右子树。
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                    // 如果已经存在这个key,那么覆盖value
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            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);
        }
		// 通过找出parent, 在此结点之下挂上新的结点, 红黑树调整都在Entry内部进行。
        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;
    }
           

1.2 一帮以getEntry()方法为基础的获取元素的方法,其中包括containsKey(),get(),remove()等

// 根据key,找出对于的红黑树结点Entry<K,V>
 final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            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;
    }
           

1.3 一帮以getFirstEntry(),getLastEntry()为基础的获取头和尾元素的方法,其中包括:firstKey(),lastKey();firstEntry(),lastEntry();pollFirstEntry(),pollLastEntry();

//得到最左,即得到最小的key
final Entry<K,V> getFirstEntry() {
        Entry<K,V> p = root;
        if (p != null)
            while (p.left != null)
                p = p.left;
        return p;
    }
    final Entry<K,V> getLastEntry() {
        Entry<K,V> p = root;
        if (p != null)
            while (p.right != null)
                p = p.right;
        return p;
    }
           

1.4 KeySet 和 EntrySet

由于map没有Iterator,因此遍历需要借助Set

keySet保存了map中的key作为集合,之后可以调用map.get(key)得到value对象。

public static void main(String[] args) {
        TreeMap<Integer, Integer> map = new TreeMap<>();
        map.put(1,2);
        map.put(2,3);
        map.put(4,5);
        map.put(-1,-8);
        map.put(-4,-9);

        Set<Integer> keySet = map.keySet();
        Iterator<Integer> it = keySet.iterator();
        while (it.hasNext()) {
            Integer key = it.next();
            System.out.println(map.get(key));
        }
    }
           

map.entrySet()直接把红黑树的keySet取出来

由于取出来的Set上面已经是保存在红黑树上的K,V,因此就可以直接访问K,V。

public static void main(String[] args) {
        TreeMap<Integer, Integer> map = new TreeMap<>();
        map.put(1,2);
        map.put(2,3);
        map.put(4,5);
        map.put(-1,-8);
        map.put(-4,-9);

        Set<Map.Entry<Integer, Integer>> keySet = map.entrySet();
        Iterator<Map.Entry<Integer, Integer>> it = keySet.iterator();
        while (it.hasNext()) {
            Map.Entry<Integer,Integer> entry = it.next();
            System.out.println(entry.getValue());
        }
    }
           

两种方法总结,keySet得到key后,还需要在到红黑树中跑一次二分,最终得到V; entrySet直接得到红黑树的keySet,那么就能直接获得值。因此entrySet的方法速度更快。

二、HashMap

2.1 hash解决冲突的方法

1)开发地址法
Java容器(三)Map一、TreeMap二、HashMap三、ConcurrentHashMap四、LinkedHashMap

m是地址长度

di是增量,对di的取法有三种

线性探测再散列:di = 1 , 2 , 3 , … , m-1

平方/二次探测再散列:di = 1^2 , -1^2 , 2^2 , -2^2 , 3^2 , -3^2 , … , k^2 , -k^2

随机探测再散列 di 是一组伪随机数列

例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元,参图8.26 (a)。如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元,参图8.26 (b)。如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,………,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元。

2)拉链法

每个桶维护一个链表

3)再哈希法

构造多个不同的哈希函数,当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

4)建立公共溢出区

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

2.2 hashmap的源码分析

cvte面试的时候被问到了hashmap的put(k,v)实现,可见hashmap的重要性

数据域

public class More ...HashMap extends AbstractMap
     implements Map, Cloneable, Serializable {
      private static final long serialVersionUID = 362498820763181265L;
      // 默认的初始容量(容量为HashMap中槽的数目)是16
      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;
      // 某个链表长度大于8,有可能转换成红黑树
      static final int TREEIFY_THRESHOLD = 8;
      // 删除冲突节点后,hash相同的节点数目小于这个数,红黑树就恢复成链表
      static final int UNTREEIFY_THRESHOLD = 6;
      // 扩容的临界值
      static final int MIN_TREEIFY_CAPACITY = 64;
      // 存储元素的数组
      transient Node[] table;
           

HashMap的key可以是null

table[0]存放了null的键值对。

HashMap中的结点Node

//继承了Map.Entry<K,V> 每个结点上保存 K,V对
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 K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

		// 重写了hashcoed的计算方法
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
	// 比较两个结点是否相同
        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;
        }
    }
           

红黑树

封装了红黑树,一大段代码都是红黑树的操作,例如插入,删除,返回头节点,还有保持平衡性的调整。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        // 返回根节点
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
           

容量大小capacity 和 加载因子 loadFactor

如果桶的数量 >= 容量(默认为16) * 加载因子(默认0.75) , 那么就进行扩容。

为什么hashMap的实际容器大小是2^n

传入capacity之后,实际的桶大小是 大于capactity的最近的2^n的数。

hash值的计算是这样的 : 调用了(length-1) & hash ,因此必须要保证leng = 2^n

对于length = 16, 对应二进制”1 0000”, length-1=”0 1111”

假设此时h = 17 .

(1) 使用”h % length”, 也就是”17 % 16”, 结果是1 .

(2) 使用”h & (length - 1)”, 也就是 “1 0001 & 0 1111”, 结果也是1 .

我们会发现, 因为”0 1111”低位都是”1”, 进行”&”操作, 就能成功保留”1 0001”对应的低位, 将高位的都丢弃, 低位是多少, 最后结果就是多少 .

刚好低位的范围是”0~15”, 刚好是长度为length=16的所有索引 .

HashMap的四个构造函数

// 指定“容量大小”和"加载因子"
 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);
    }
    //指定“容量大小”的构造函数
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    // 传入子map,会根据子map的大小,当前容器进行调整大小, 最后赋值到当前map上
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
           

put方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

//传入key的hash值,value,false,true
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // tab:哈希桶,是一个头节点数组 , n: 桶的大小 ,   p: key的哈希值,对于桶的位置i,该位置桶的头节点    
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0) //如果哈希桶是空的
            n = (tab = resize()).length; // 调用resize(),对哈希桶进行扩容


        if ((p = tab[i = (n - 1) & hash]) == null) //如果 key的hash值,在桶中的位置是头一次出现
            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;
    }
           

get方法

public V get(Object key) {
    Node e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node getNode(int hash, Object key) {
    Node[] tab; Node 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) {
            // 在树中get
            if (first instanceof TreeNode)
                return ((TreeNode)first).getTreeNode(hash, key);
            // 在链表中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
           

rehash

rehash主要是在扩容的时候执行。 扩容的时候,newCap = oldCap << 1 , 也就是*2 。

rehash做了哪些事呢?

第一步:扩容,<<1位

第二步:取出所有就的Node<K,V>[]oleTable , 对于每个桶上面的每个结点,都需要挂到新的桶上,它根据新的容量进行下标计算,再把结点挂上去。

多线程执行rehash导致的死锁问题

假设桶1后面挂着的点是这样的。

1)table[1] -> p1 -> p2;

2)线程1对table[1]进行重新挂点时,被中断,那么此时线程1的e和next分别如下

table[1] -> p1(e) -> p2(next);

do{
    Entry<K,V> next = e.next;// <--假设线程一执行到这里就被调度挂起了
    inti = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
}while (e != null);
           

3)此时线程2完成了rehash,把桶1改变了结构

由于用了头插法,因此线程2执行完之后变成了这样。

table[1] -> p2(next) -> p1(e)

4)这个时候线程1恢复执行,可以发现,e 和 next 构成了循环链表,陷入了死循环。

三、ConcurrentHashMap

HashMap线程安全有以下几种选择:

1)HashTable:内部调用synchronizaed 方法,对类进行上锁

2)Collections.synchronizedMap(map) :内部持有一个mutex,每个方法都对mutex进行上锁,因此效果和1)差不多。

3)ConcurrentHashMap:分段锁技术

分段锁

早期

默认维护了16把锁,把哈希表通过某种规则划分成16组。多个线程访问同一组,那么他们是互斥的。但是如果多个线程访问不同组,那么他们就可以并发。

java8之后

利用CAS算法和synchronized对链表的头节点上锁实现同步

插入
Java容器(三)Map一、TreeMap二、HashMap三、ConcurrentHashMap四、LinkedHashMap

ConcurrntHashMap学习这方面有助于理解并发编程

  • size()方法和mappingCount()方法的异同,两者计算是否准确?
  • 多线程环境下如何进行扩容?

四、LinkedHashMap

父类就是HashMap

public LinkedHashMap() {
        // 调用HashMap的构造方法,其实就是初始化Entry[] table
        super();
        // 这里是指是否基于访问排序,默认为false
        accessOrder = false;
    }