zoukankan      html  css  js  c++  java
  • JDK7HashMap

    JDK7HashMap

    成员变量

    HashMap中定义了非常多的成员变量以及常量,各成员变量含义具体如下:

    //默认初始化长度-16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //空entry数组
    static final Entry<?,?>[] EMPTY_TABLE = {};
    //table存放数据
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    //数组包含元素个数
    transient int size;
    //数组扩容阈值=capacity*load_factor
    int threshold;
    //加载因子
    final float loadFactor;
    

    DEFAULT_INITIAL_CAPACITY为什么设置为16,创建HashMap的时候需要设置值嘛?

    这些问题将等下揭晓。

    DEFAULT_LOAD_FACTOR为什么设置为0.75?

    JDK官方基于空间与时间成本做出的均衡,加载因子越大,扩容越晚,节省空间,哈希碰撞越多,put、get效率越低,至于为什么是0.75,这是一个数学or统计问题?再次不做深究

    HashMap的存储结构

    //成员变量Entry数组
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    static final Entry<?,?>[] EMPTY_TABLE = {};
    
    //内部类Entry,实现了Map.Entry接口
    static class Entry<K,V> implements Map.Entry<K,V>{
        	final K key;
            V value;
            Entry<K,V> next;
            int hash;
    
            /**
             * Creates new entry.
             */
            Entry(int h, K k, V v, Entry<K,V> n) {
                value = v;
                next = n;
                key = k;
                hash = h;
            }
    }
    

    由此可以看出,JDK7的HashMap的存储结构是通过数组加链表的方式实现的,了解了put方法之后也能明白,HashMap采用的是冲突链表的方式解决哈希碰撞

    JDK7的HashMap构造方法

    public HashMap() {   
        //调用默认初始长度16,默认加载因子0.75
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    public HashMap(int initialCapacity) {
        //默认加载因子0.75
            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);
    		//对loadFactor赋值以及threshold赋值
            this.loadFactor = loadFactor;
            threshold = initialCapacity;
        	//空方法,交由子类实现,在HashMap中无用
            init();
    }
    
    //插入整个map
    public HashMap(Map<? extends K, ? extends V> m) {
            this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                          DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
            inflateTable(threshold);
    
            putAllForCreate(m);
    }
    

    我们在构造函数中传入的capacity实际上赋值给了threshold参数,而不是table数组真正的大小,table数组真正的大小在put第一个元素时

    核心方法put()详解

    put()

    public V put(K key, V value) {
        	//判断table是否为EMPRT_TABLE
            if (table == EMPTY_TABLE) {
                //用大于threshold的2次幂初始化table
                inflateTable(threshold);
            }
        	//如果key为null
            if (key == null)
                return putForNullKey(value);
        	//对key进行hash
            int hash = hash(key);
        	//根据hash值确定数组下表,通过&操作获得
            int i = indexFor(hash, table.length);
        	//遍历链表,寻找key相同的元素,并且修改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;
                    //recordAccess空方法
                    e.recordAccess(this);
                    return oldValue;
                }
            }
    
            modCount++;
            addEntry(hash, key, value, i);
            return null;
        }
    

    inflateTable()

    如果是第一次put,在构造函数处我们传入的capacity赋值给了threshold,而threshold被传递到了toSize,我们的capacity才真正的起作用

    private void inflateTable(int toSize) {
            // Find a power of 2 >= toSize寻找一个大于toSize的2次幂
            int capacity = roundUpToPowerOf2(toSize);
    		//我们传入的capacity赋值给了threshold,然而此刻threshold又被修改了,hashMap老渣男了
            threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
            table = new Entry[capacity];
        	//哈希种子,跳过
            initHashSeedAsNeeded(capacity);
    }
    

    通过费劲心机的一通位运算,三目运算符,拿到了一个比在构造函数中传入的capacity大的二次幂,这是为啥呢?

    为什么就非得是个二次幂?

    这个问题等下hash回答

    费劲心机的roundroundUpToPowerOf2()

    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        int rounded = number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (rounded = Integer.highestOneBit(number)) != 0
                ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
                : 1;
    
        return rounded;
    }
    

    解释一下上面的代码,可以说非常之精妙的了,由于里面参杂了大量的三目运算符使得看起来非常难受,下面用伪代码解释一下,其中以数字7举例,其二进制的后四位为:0111

    if number>=MAXIMUM_CAPACITY
    	rounded=number=MAXIMUM_CAPACITY
    else
    	//这里返回的是number二进制的最高位那个1,通俗点来说就是小于number的最大2次幂
    	//会有详细分析JDK是如何操作的
    	//rounded=0100=4
    	rounded=Integer.hightestOneBit(number)
    	if rounded!=0
    		//这里统计了number二进制表示中1的个数,0111--3个1
    		if Integer.bitCount(number)>1
    			//如果超过1,则rounded左移一位就是大于num的最小二次幂,也就是1000--8>7
    			rounded=rounded<<1
    		else
    			//如果==1,表明rounded==number
    			rounded=rounded
    	else
    		//rounded为0,赋值为1
    		rounded=1
    

    其中highestOneBit()、bitCount()中包含了大量的位运算,非常的精妙,详解另一篇讲解(待补)

    hash()

    回到put方法中,在第一次放入时,我们已经创建好了table了,capacity也被我们设置好了,threshold也被重新设置了,然我们暂且忽略掉putForNullKey这个分支,进入hash方法,在这里也会了解为什么capacity非得是个二次幂

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
    
        h ^= k.hashCode();
    
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    

    哈希方法最重要的性能考虑之一就是散,也就是尽量减少哈希碰撞

    例如字符串jack的hashcode的表现形式为
    1100011010011111011111
    

    直接取这个hashcode值当作hash值行不行?当然可以,但是有问题

    在了解了下面的indexFor方法之后,发现hashMap采用位运算的方式计算元素对应的下标,这样会有什么问题呢?

    1100011010011111011111
    xxxxxxxxxxxxxxxxxxx111
    

    问题很明显了,如果后面111相同,在当前情况下,前面的29位不同都会被映射到同一个位置,这无疑导致了大量的哈希碰撞,那么为了弥补在indexFor中的过错,hash值的计算就需要尽可能综合所有bit的信息,所以hash方法中加入了扰动计算,算是为indexFor的高效擦屁股吧!

    indexFor()

    hashMap就是通过如下的方法来计算某个key所计算出来的hash已经对应到数组的哪个位置的,它相较于直接对length取余有何优势呢?

    • 与操作速度比除法取余的方式更快
    • 不用担心负数的问题
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }
    

    而着就要求length也就是capacity必须是一个二次幂

    举例说明 8--1000 8-1=7=0111,前28位的0省略

    任何hash值与0111进行于操作,它的值只能落在0000-0111之间,也就正好是数组的范围

    这样做值得嘛?

    值得,entry被放入map中之后,它的hash值也被保存到了自身的成员变量之中一般来说不会变化,直接访问即可,而在hashMap的最频繁调用的方法get()中,indexFor效率的提升肯定是非常棒的,因此牺牲hash的高效换取indexFor的高效无疑提高了整个HashMap的效率,何况hash中的操作也都是位运算

    HashTable中的hash方法与index的计算
    private int hash(Object k) {
        // hashSeed will be zero if alternative hashing is disabled.
        return hashSeed ^ k.hashCode();
    }
    
    int index = (hash & 0x7FFFFFFF) % tab.length;
    

    可以看出hashTable则选择了较为朴素的实现,为什么hashTable不跟进效率高的实现呢?总的来说就是Hashtable和HashMap的容量选取策略不同

    • hashtabl选取素数容量,默认为11,翻倍也是2*n+1的翻倍,素数的选择使得简单的取余分布就很均匀,这应该是数学上的知识了,不予证明
    • hashMap选取二次幂作为初始容量,默认16,翻倍也是2倍的翻倍,通过位运算实现高效的index计算,但是需要扰动计算hash值

    addEntry()

    put方法中找不到相同的key,此时需要添加新的entry

    void addEntry(int hash, K key, V value, int bucketIndex) {
        	//如果size超过了threhold,
            if ((size >= threshold) && (null != table[bucketIndex])) {
                //扩容扩容大小为原数组的两倍
                resize(2 * table.length);
                //扩容之后需要重新计算hash重新寻找index
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
    		//头插法插入链表
            createEntry(hash, key, value, bucketIndex);
    }
    
    头插法createEntry()
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
    

    头插法导致的问题见

    老生常谈之扩容

    resize()

    以两倍的容量扩张

    void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
    		//resize(2 * table.length);
            Entry[] newTable = new Entry[newCapacity];
        	//转移数据
            transfer(newTable, initHashSeedAsNeeded(newCapacity));
            table = newTable;
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    
    transfor()
    void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Entry<K,V> e : table) {
                //拷贝数组
                while(null != e) {
                    Entry<K,V> next = e.next;
                    //如果需要重新hash的话则重新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;
                }
            }
    }
    
    

    为什么扩容之后要重新对元素进行hash然后再散列呢?

    举例:

    扩容前

    length=16

    h= 0001 1001

    & 0000 1111

    = 0000 1001=9

    扩容后

    h= 0001 1001

    & 0001 1111

    = 0001 1001=25

    可见扩容之后的位置有两种情况:1.原位置不动 2.向后移动原数组长度个位置。因此,扩容之后在对数据进行复制的时候需要重新计算hash和index,这样的扩容能够实现对链表长度的削减,以提高整体HashMap的查询效率

  • 相关阅读:
    第二十一章流 1流的操作 简单
    第二十章友元类与嵌套类 1友元类 简单
    第十九章 19 利用私有继承来实现代码重用 简单
    第二十章友元类与嵌套类 2嵌套类 简单
    第十九章 8链表类Node 简单
    第二十一章流 3用cin输入 简单
    第十九章 10 图书 药品管理系统 简单
    第十九章 11图书 药品管理系统 简单
    第二十一章流 4文件的输入和输出 简单
    第十九章 12 什么时候使用私有继承,什么时候使用包含 简单
  • 原文地址:https://www.cnblogs.com/danzZ/p/14075147.html
Copyright © 2011-2022 走看看