HashMap源码分析

当hash表中的负载因子达到负载极限的时候,hash表会自动成倍的增加容量(桶的数量),并将原有对象重新分配并加入新的桶内,这个过程称为rehash,这个过程是十分耗性能的,一般不这样子弄,一般建议设置比较大的初始化容量,防止rehash,但是也不能太大。

这是自己粗糙的实现的hashMap的源码:测试过的

package org.llyf.hashmap;

public class MyHashMap<K,V>{

    private  Entry[] table;
    private static Integer CAPACITY =8;
    //
    private int size=0;

    public MyHashMap() {
        this.table = new Entry[CAPACITY];
    }

    //这个size()方法其实就是统计你hashMap有多少个元素,所以说你返回table.length显然是不对的 没新增一个元素的时候就size++
    public int size() {
        return size;
    }

    public Object get(Object key) {
        int hash = key.hashCode();
        int i=hash %8;
        //get值的话也是循环该链表,找到下标,
        for(Entry<K,V> e=table[i]; e !=null;e=e.next){
            //判断循环到该节点的key与我们传进来的key是否相等
            if(e.k.equals(key)){
                return e.getV();
            }
        }
        return null;
    }


    public Object put(K key, V value) {
        //把这个节点添加到数组里面 首先要计算该节点的下标用hashcode然后再取模
        int hash = key.hashCode();
        int i=hash %8;
        //这里hashmap所对应的返回值,如果是(曹操 1) 再put一个(曹操 2)这个时候就是key相同,值不相同的情况,hashmap对于这样的情况,
        //就会把老的值给更新掉 1变成2,然后把老的给返回出来,这时候的操作就是遍历该链表,找到key 把value值更新 把老的value返回出去
        //这个for循环从头节点开始循环,条件是e.next !=null 等于空的话就代表结束了
        for(Entry<K,V> e=table[i];e !=null;e=e.next){
            //只要判断循环到的这个节点的key和我们传进来的key
            if(e.k.equals(key)){
                //如果相等我们得存一下老值
                V oldValue=e.v;
                e.v=value;
                return oldValue;
            }
        }
        //计算addEntry(key, value, i);下标以后jdk1.7是放在数组节点的头部,jdk1.8是放在数组节点的后边 这时候该节点可以用table[i]来表示
        addEntry(key, value, i);
        return null;
    }
    //增加一个节点
    private void addEntry(K key, V value, int i) {
        Entry<K,V> entry = new Entry(key,value,table[i]);
        table[i]=entry;
        size++;
    }

    //源码中多存了一个hash  这个hash就是key多对应的hash值
    class Entry<K,V>{
        private  K k;
        private  V v;
        private Entry<K,V> next;

        public Entry(K k, V v, Entry<K, V> next) {
            this.k = k;
            this.v = v;
            this.next = next;
        }

        public K getK() {
            return k;
        }

        public V getV() {
            return v;
        }

        public Entry<K, V> getNext() {
            return next;
        }
    }

    public static void main(String[] args){
        MyHashMap<String,String> myHashMap = new MyHashMap<>();
        for(int i=0;i<10;i++){
                myHashMap.put("周瑜"+i,"周瑜"+i);
        }

        System.out.println(myHashMap.get("周瑜"));
    }

}

 

 

 

这是随便给的图,可以参考这张图分析更直观:

 

这个是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);

        // Find a power of 2 >= initialCapacity  这里的意思是比方说我传的11则大于11的2的次方数是16  ,则下面是new出来16大            //小的数组


        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;

      //这里算出来的阈值是16*0.75=12
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }

 

 

static int indexFor(int h, int length) {
        return h & (length-1);
    }

这个是求数组下标的操作,下面解释下为什么要这么做  length=16这个大家都知道

        0000 1000     8

        0001 0000     16        

       0000 1111      16-1      上面就是15来&hash(因为hashcode是比较乱的随便举一个1010 0101) 所以这个操作就变成了

       0000 1111

&     1010 0101           从这里可以看出来这结果被控制在后四位了 因为前四位都是0000 最终的取值范围也就是 后四位的取值大小,那么这后四位的取值范围就是0-15  最终i的取值范围肯定是0-15毋庸置疑了 ,这也就说明了为什么会是2的次方数,因为拿到2^n会去减1,减1之后的效果就是它的低位全部都是1,低位全是1的效果就能保证它最后算出来的值就是这低位的取值范围,这样就能保证数组下标不会越界,


h ^= k.hashCode();   那么它拿到这个hashcode为什么要进行这样的操作(异或和右移)呢?  因为一个好的hash算法,它要求的散列性是很高的,正如上面刚才说的真正能够控制hash值对应数组下标的就是这后四位,前面的高四位并没有什么用,你再来很多hashcode的时候会是什么情况呢,出来的结果很多都是一样的概率很大,这就导致对应的数组下标都是一样的,这样的散列性其实就是很差的

   0000 1111

   1010 0101    

            1010 0101    这是右移之后的效果,最终的效果就是你所有的位数,包括你高的几位都参与了运算,最终你算出来的这个hashcode,所有的高位和低位都能影响这个hash值,这样达到的最终散列性就会比较高了

它这种做法就可以节省你的内存使用率,

h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);

 

这是另外一段源码 增加节点的

 void addEntry(int hash, K key, V value, int bucketIndex) {

     //数组size>阈值的时候就进行扩容,但是&&后边的条件是什么意思呢??结合图

 容量是8 假如说达到6个就开始扩容,但是那个项羽如果放在那个空的地方的时候是不会进行扩容的,只有落在1-4这中间才会触发扩容,所以想要扩容,还得加一个条件就是

table[bucketIndex]不能为空。但是在jdk8中就没有这个条件了

这里讲下jdk7与jdk8的区别

1 jdk8会将链表变成红黑树

左边是完全平衡二叉树,右边是红黑树,为什么不偏偏用红黑树呢?

插入效率:链表>红黑树>完全平衡二叉树     

查询效率:链表<红黑树<完全平衡二叉树     红黑树的插入和查找是处于两者之间的,那为什么要用红黑树呢,因为我们在用hashMap的时候不仅仅时需要查询的,而且余插入等等,这些操作需要平衡,所以就选择红黑树来达到这些平衡,jdk7用的是链表,如果数据量比较大的时候,你的链表还是比较长,你在查这个链表的时候性能还是比较低的,jdk8就是想优化一些查询

static final int TREEIFY_THRESHOLD = 8; 这里是jdk8多出来的属性,当打到这个属性值8的时候,这个链表就会变成红黑树

static final int UNTREEIFY_THRESHOLD = 6;当删除节点时候达到这个值的时候就会从红黑树变成链表,
 

2 新节点插入链表的顺序不同,jdk7是插入链表头节点,jdk8因为要遍历链表把链表变成红黑树所以采用插入尾节点

3 hash算法简化

 

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

4 resize的逻辑修改,(jdk7 会出现死循环,jdk8不会,)(死锁场景http://www.importnew.com/22011.html)


        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//这里建议你new hashMap的时候指定容量,指定的话就不会扩容了,在并发高德情况下你扩容很可能会引发死锁
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

4 resize的逻辑修改,(jdk7 会出现死循环,jdk8不会,)(死锁场景http://www.importnew.com/22011.html)

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

死锁的场景,因为它是从一个老的数组一到一个新的地方去之后,因为你那个元素并没有去复制,而是把引用给不同的数组,所以会出现死锁的场景

 

关于二进制跟&运算,扩展一道编程题

编程题:输入一个整数,输出该数二进制表示中1的个数,负数用补码表示:

public class Solution { 

    public int NumberOf1(int n) { 

        int count = 0; 

        while(n!= 0){ 

            count++; 

            n = n & (n - 1); 

         } 

        return count; 

    } 





分析一下代码: 这段小小的代码,很是巧妙。 

如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。 

举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。 

每次 n & (n-1) 都会使得二进制数据的最右边的一个1变为0,所以有多少个1就会执行循环多少次,直至没有1(也就是0)


 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值