java集合:hashmap

一 map

上一篇整理里list,本文继续整理hashmap.先说一下map的基本接口

Map将key和value封装至一个叫做Entry的对象中,Map中存储的元素实际是Entry。只有在keySet()和values()方法被调用时,Map才会将keySet和values对象实例化。

所以map的遍历右三种方式:

package com.daojia.collect;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;

public class HashMapTest {
    private static final int a[] = {10, 40, 30, 60, 90, 70, 20, 50, 80};

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		HashMap map = new HashMap();
		   for(int i=0; i keys = map.keySet();
	        for (Integer key : keys){
	            System.out.println(key+" "+map.get(key)+"");
	        }
	        System.out.println("");
           System.out.println("value遍历:");
           Collection values =map.values();
           Iterator  ite=values.iterator();
           while(ite.hasNext()){
           	 System.out.print(ite.next()+" ");
           }
           System.out.println();
           Set entryset = map.entrySet();
           for(Object o:entryset){
        	   Entry entry = (Entry)o;
        	   System.out.println(entry.getKey()+" "+entry.getValue());
           }
	}
}

java集合:hashmap_第1张图片

Map 的实现类主要有 4 种:
Hashtable 慢,线程安全
HashMap 速度很快,但没有顺序
TreeMap 有序的,效率比 HashMap 低
LinkedHashMap 结合 HashMap 和 TreeMap 的特点,有序的同时效率也不错,内存占用大。

二 hashmap

2.1 hashmap原理与特点

HashMap 是一个采用哈希表实现的键值对集合,继承自 AbstractMap,实现了 Map 接口 。 

HashMap 的特殊存储结构使得在获取指定元素前需要经过哈希运算,得到目标元素在哈希表中的位置,然后再进行少量比较即可得到元素。jdk1.7及以前版本是底层实现是 链表数组,JDK 1.8 后又加了 红黑树

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {

java集合:hashmap_第2张图片

实现了 Map 全部的方法
key 用 Set 存放,所以想做到 key 不允许重复,key 对应的类需要重写 hashCode 和 equals 方法
允许空键和空值(但空键只有一个,且放在第一位,下面会介绍)
元素是无序的,而且顺序会不定时改变
插入、获取的时间复杂度基本是 O(1)(前提是有适当的哈希函数,让元素分布在均匀的位置)
遍历整个 Map 需要的时间与 桶(数组) 的长度成正比(因此初始化时 HashMap 的容量不宜太大)
两个关键因子:初始容量、加载因子
除了不允许 null 并且同步,Hashtable 几乎和他一样。

2.2 参数:

   默认初始容量:16,必须是 2 的整数次方

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    static final int MAXIMUM_CAPACITY = 1 << 30; //   最大容量: 2^ 30 次方

   static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子的大小:0.75,

   static final int TREEIFY_THRESHOLD = 8;//树形阈值,大于此值桶用红黑树而不是链表

  static final int UNTREEIFY_THRESHOLD = 6;//非树形阈值,小于此值时,桶存储把树转变为链表存储

   static final int MIN_TREEIFY_CAPACITY = 64; //树的最小容量,为避免resize时冲突,最小 least 4 * TREEIFY_THRESHOLD

    transient Node[] table;//哈希表中的链表数组

    transient Set> entrySet;//缓存的 键值对集合
    transient int size;//键值对的数量

    transient int modCount;//当前 HashMap 修改的次数,这个变量用来保证 fail-fast 机制

    final float loadFactor;//哈希表的加载因子

由于 HashMap 扩容开销很大(需要创建新数组、重新哈希、分配等等),因此与扩容相关的两个因素:
容量:数组的数量
加载因子:决定了 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);
    }
    /**
     * 默认:容量大小(16) ,load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

涉及的方法: tableSizeFor(int) 来根据指定的容量设置阈值,这个方法经过若干次无符号右移、求异运算,得出最接近指定参数 cap 的 2 的 N 次方容量。假如你传入的是 5,返回的初始容量为 8 。

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

链表节点

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

        Node(int hash, K key, V value, Node next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

树节点:

static final class TreeNode extends LinkedHashMap.Entry {
        TreeNode parent;  // red-black tree links
        TreeNode left;
        TreeNode right;
        TreeNode prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node next) {
            super(hash, key, val, next);
        }

2.3添加

    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[] tab; Node p; int n, i;
        //table为空就创建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //确定插入table的位置,算法是(n - 1) & hash
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //在table的i位置发生碰撞,有两种情况,1、key值是一样的,替换value值,
        //2、key值不一样的有两种处理方式:2.1、存储在i位置的链表;2.2、存储在红黑树中
        else {
            Node e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //2.2
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            //2.1
            else {
                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;
                }
            }
            //如果e为空就替换旧的oldValue值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //threshold=newThr:(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        //默认0.75*16,大于threshold值就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果该位置桶为空,新旧节点并放进去。否则从桶中第一个元素开始查找哈希值对应位置 如果桶中第一个元素的哈希值和要添加的一样,替换,结束查找。如果第一个元素不一样,而且当前采用的还是 JDK 8 以后的树形节点,调用 putTreeVal() 进行插入。否则还是从传统的链表数组中查找、替换,结束查找当这个桶内链表个数大于等于 8-1,就要调用 treeifyBin() 方法进行树形化

  1. 最后检查是否需要扩容

哈希值

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

java集合:hashmap_第3张图片

1 由于哈希表的容量都是 2 的 N 次方,在当前,元素的 hashCode() 在很多时候下低位是相同的,这将导致冲突(碰撞),因此 1.8 以后做了个移位操作:将元素的 hashCode() 和自己右移 16 位后的结果求异或。由于 int 只有 32 位,无符号右移 16 位相当于把高位的一半移到低位:这样可以避免只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,可以避免哈希值分布不均匀。

2 具体的计算过程用如下图表示,因为目前的table长度2的N次方,2n-1得到的二进制数的每个位上的值都为1,而计算下标的时候,使用&位操作,得到的和原hash的低位相同,而非%求余,更高效。加之上面设计的hash(key)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。所以说当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,g。高效查询,在jdk1.8更加入了红黑树来改善大链表的查询性能。

java集合:hashmap_第4张图片

请看putVal代码27/28行,当桶bucket大于TREEIFY_THRESHOLD(8)值时就执行treeifyBin,如果是之前java7之前的代码的话是要进行扩容的,但是java8可能会把这个bucket的链表上的数据转化为红黑树

final void treeifyBin(Node[] tab, int hash) {
        int n, index; Node e;
        //当tab.length hd = null, tl = null;
            do {
                TreeNode p = replacementTreeNode(e, null);//新建一个树形节点,内容和当前链表节点 e 一致
                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);
        }
    }

2.4 扩容resize:

final Node[] resize() {
    //复制一份当前的数据
    Node[] 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)
            //如果旧容量小于等于 16,新的阈值就是旧阈值的两倍
            newThr = oldThr << 1; // double threshold
    }
    //如果旧容量为 0 ,并且旧阈值>0,说明之前创建了哈希表但没有添加元素,初始化容量等于阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        //旧容量、旧阈值都是0,说明还没创建哈希表,容量为默认容量,阈值为 容量*加载因子
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果新的阈值为 0 ,就得用 新容量*加载因子 重计算一次
    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[] newTab = (Node[])new Node[newCap];
    table = newTab;
    //接下来就得遍历复制了
    if (oldTab != null) {
        //将原来map中非null的元素rehash之后再放到newTab里面去
        for (int j = 0; j < oldCap; ++j) {
            Node 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)e).split(this, newTab, j, oldCap);
                else { //保留旧哈希表桶中链表的顺序
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node next;
                    //do-while 循环赋值给新哈希表
                    do {
                        next = e.next;
                        // 这里的操作就是 (e.hash & oldCap) == 0 这一句,起到了判断作用:0表示新位置下标不变,如果不是0那么表示位置有变动。
                          因为oldCap和newCap是2的次幂,并且newCap是oldCap的两倍,就相当于oldCap的唯一一个二进制的1向高位移动了一位
                             (e.hash & oldCap) == 0就代表了(e.hash & (newCap - 1))还会和e.hash & (oldCap - 1)一样。
                        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;
}

jdk1.7

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //遍历旧表
        for (Entry e : table) {
            //当桶不为空
            while(null != e) {
                Entry next = e.next;
                //如果hashSeed变了,需要重新计算hash值
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //得到新表中的索引
                int i = indexFor(e.hash, newCapacity);
                //将新节点作为头节点添加到桶中
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

从代码上看,resize的过程1.7中是通过控制hashSeed的变化导致hash()方法得到的hash值,而JDK1.8中一旦得到了一个键的hash值后,就不会再改变了,而是通过hash&cap==0为区分,将链表分散,而1.7是通过更新hashSeed将旧表中的链表分散,可以看出jdk1.8设计的优化点:省却了重新计算哈希值的过程。 所以区别: 

1 当1.8中的桶中元素处于链表的情况,遍历的同时最后如果没有匹配的,直接将节点添加到链表了尾部,而1.7在遍历的同时没有添加数据,而是另外调用了addEntry()方法。 addEntry中默认将新加的节点作为链表的头节点,而1.8中会将新加的结点添加到链表末尾 addEntry中默认将新加的节点作为链表的头节点,而1.8中会将新加的结点添加到链表末尾 。

1.8rehash时保证原链表的顺序,而1.7中rehash时将改变链表的顺序

看了resize的过程就知道尽管jdk1.8有所优化,还是很耗性能,尽量避免,一开始预估好容量。

2.5 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;
    //tab 指向哈希表,n 为哈希表的长度,first 为 (n - 1) & hash 位置处的桶中的头一个节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //如果桶里第一个元素就相等,直接返回
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //否则就得慢慢遍历找
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                //如果是树形节点,就调用树形节点的 get 方法
                return ((TreeNode)first).getTreeNode(hash, key);
            do {
                //do-while 遍历链表的所有节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
查找就是先计算hash值,(n-1)&hash判断桶位置,接着判断是否第一个节点,不是就在桶里遍历查找(可能是树节点)。

2.6 红黑树相关

我们在上面的put的方法里面,treeifyBin就是桶的树形化。

  • 根据哈希表中元素个数确定是扩容还是树形化
  • 如果是树形化 遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容

下面看看把一个桶中的链表结构变成红黑树结构

final void treeify(Node[] tab) { 
            TreeNode root = null;
            for (TreeNode x = this, next; x != null; x = next) {
                next = (TreeNode)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 p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h) //当比较节点的哈希值比 x 大时
                            dir = -1;
                        else if (ph < h) //当比较节点的哈希值比 x 小时
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0) //如果hash值相等,则比较k值,用其Compare,如果还相等,则走tieBreakOrder
                            dir = tieBreakOrder(k, pk);

                        TreeNode xp = p; //把 当前节点变成 x 的父亲
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {// 根据dir的值选取左右子结点,子结点不为空,继续循环寻找
                            x.parent = xp; //x节点父节点指向当前节点
                            if (dir <= 0)   //如果当前比较节点的哈希值比 x 大,x 就是左孩子,
                                xp.left = x;
                            else           //否则x就是右孩子
                                xp.right = x;
                            root = balanceInsertion(root, x); // 插入后,平衡红黑树,使之满足红黑树性质
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);//将root移到桶中的第一个  
        }

balanceInsertion就是红黑树的修正。参见之前的。

如果:插入的如果一个桶中已经是红黑树结构,就要调用红黑树的添加元素方法 putTreeVal()。

   else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
final TreeNode putTreeVal(HashMap map, Node[] tab,
                                   int h, K k, V v) {
        Class kc = null;
        boolean searched = false;
        TreeNode root = (parent != null) ? root() : this;
        //每次添加元素时,从根节点遍历,对比哈希值
        for (TreeNode 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 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))
                        //如果从 ch 所在子树中可以找到要添加的节点,就直接返回
                        return q;
                }
                //哈希值相等,但键无法比较,只好通过特殊的方法给个结果
                dir = tieBreakOrder(k, pk);
            }

            //经过前面的计算,得到了当前节点和要插入节点的一个大小关系
            //要插入的节点比当前节点小就插到左子树,大就插到右子树
            TreeNode xp = p;
         //这里有个判断,如果当前节点还没有左孩子或者右孩子时才能插入,否则就进入下一轮循环 
            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                Node xpn = xp.next;
                TreeNode 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)xpn).prev = x;
                //红黑树中,插入元素后必要的平衡调整操作
                moveRootToFront(tab, balanceInsertion(root, x));
                return null;
            }
        }
    }

跟上面的桶转换红黑树类似,少了根节点的处理,没有双层循环。

上面的get方法里面:如果找到桶之后,头节点恰好是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。

    final TreeNode getTreeNode(int h, Object k) {
            return ((parent != null) ? root() : this).find(h, k, null);
        }
 final TreeNode find(int h, Object k, Class kc) {
            TreeNode p = this;
            do {
                int ph, dir; K pk;
                TreeNode pl = p.left, pr = p.right, q;
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
            return null;
        }
这里find如果对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相等,相等就直接返回,不相等从子树查询。后面的判断条件有些多,反复的指向左右子树,不是特别理解。


其他:

HashMap 允许 key, value 为 null,同时他们都保存在第一个桶中。因为计算hash的时候判断是null返回0

参考:

http://www.cnblogs.com/huaizuo/p/5371099.html

https://blog.csdn.net/u011240877/article/details/53351188

其他:

HashMap 允许 key, value 为 null,同时他们都保存在第一个桶中。因为计算hash的时候判断是null返回0


你可能感兴趣的:(java)