HashMap源码解析之jdk1.7

jdk 1.7HashMap 底层实现是数组+链表(为什么用链表呢?详情看问题五中)。
存储结构
在这里插入图片描述

哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理就是基于此。
几种数据结构之间的情况对比:
1、数组:采用一段连续的内存空间来存储数据。对于指定下标的查找,时间复杂度为O(1),对于给定元素的查找,需要遍历整个数据,时间复杂度为O(n)。但对于有序数组的查找,可用二分查找法,时间复杂度为O(logn),对于一般的插入删除操作,涉及到数组元素的移动,其平均时间复杂度为O(n)。对应到集合实现,代表就是ArrayList。
2、二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。对应的集合类有TreeSet和TreeMap。
3、线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历整个链表,复杂度为O(n)。对应的集合类是LinkedList。
4、哈希表:也叫散列表,用的是数组支持元素下标随机访问的特性,将键值映射为数组的下标进行元素的查找。所以哈希表就是数组的一种扩展,将键值映射为元素下标的函数叫做哈希函数,哈希函数运算得到的结果叫做哈希值。哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀。
存储位置 = f(关键字)
其中,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。这会涉及到哈希冲突。
  哈希冲突(也叫哈希碰撞):不同的键值通过哈希函数运算得到相同的哈希值。哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址)、再散列函数法、链地址法。ThreadLocalMap由于其元素个数较少,采用的是开放寻址法,而HashMap采用的是链表法来解决哈希冲突,即所有散列值相同的元素都放在相同槽对应的链表中(也就是数组+链表的方式)
  HashMap是由数组+链表构成的,即存放链表的数组,数组是HashMap的主体,链表则是为了解决哈希碰撞而存在的,如果定位到的数组不包含链表(当前的entry指向为null),那么对于查找,删除等操作,时间复杂度仅为O(1),如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先需要遍历链表,存在相同的key则覆盖value,否则新增;对于查找操作,也是一样需要遍历整个链表,然后通过key对象的equals方法逐一比对,时间复杂度也为O(n)。所以,HashMap中链表出现的越少,长度越短,性能才越好,这也是HashMap设置阀值即扩容的原因。

一、前期

参数说明
1、加载因子loadFactor

  1. 默认0.75,哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。threshold=(int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1)。
  2. 表示Hsah表中元素的填满的程度。若加载因子越大,填满的元素越多。好处是空间利用率高了,但冲突的机会加大了。反之加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了。因此必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷。

2、threshold

  1. 当HashMap的size大于threshold(且null != table[bucketIndex])时会执行resize操作,threshold会重新赋值(int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1)。
  2. 初始化时默认16,构造方法中threshold = initialCapacity,但在put初始化数组时threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1)。即作用之一用于判断是否需要扩容的条件之一。

3、修改次数modCount

  1. 在迭代map过程中会判断modCount 和expectedModCount相等,不等抛出 ConcurrentModificationException

关于faild-fast机制:java.util.HashMap 不是线程安全的,因此如果在使用迭代器iterator的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map:注意到 modCount 声明为 volatile,保证线程之间修改的可见性。

4、hash种子hashSeed
在hash计算时用到,默认为0。当capacity >= Integer的最大值,且重新随机赋值hashSeed。

默认参数配置

/** 初始容量,2^4,默认16 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** 最大初始容量,2^30 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/** 临界值(HashMap 实际能存储的大小),公式为(threshold = capacity * loadFactor) */
int threshold; //默认构造时默认为DEFAULT_INITIAL_CAPACITY = 16
//加载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;
//默认装载因子  
static final float DEFAULT_LOAD_FACTOR = 0.75f;

构造方法
无参最终传入默认的初始容量,加载因子

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);
        // 设置负载因子,临界值此时为容量大小,后面第一次put时由inflateTable(int toSize)方法计算设置
        this.loadFactor = loadFactor;
        //初始阈值为初始容量,与JDK1.8中不同,JDK1.8中调用了tableSizeFor(initialCapacity)得到大于等于初始容量的一个最小的2的指数级别数,比如初始容量为12,那么threshold为16,;如果初始容量为5,那么初始容量为8
				threshold = initialCapacity;
        init();//空实现
    }
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    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);
    }

计算hash值 - jdk 1.8有变动

final int hash(Object k) {
        int h = hashSeed;//默认为0
        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);
    }

initHashSeedAsNeeded()方法,

final boolean initHashSeedAsNeeded(int capacity) {
    //当我们初始化的时候hashSeed为0,0!=0 这时为false.
        boolean currentAltHashing = hashSeed != 0;
        //isBooted()这个方法里面返回了一个boolean值,我们看下面的代码
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }

默认返回false,但是测试时发现vm启动后赋值为true,所以在上面initHashSeedAsNeeded()方法中,主要看capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD,然后决定是否需要重新赋值hashSeed。否则默认为0。

private static volatile boolean booted = false;
    public static boolean isBooted() {
        return booted;
    }

关于变量Holder.ALTERNATIVE_HASHING_THRESHOLD,看静态块中发现其值就是判断是否自定义altThreshold,一般不会定义,所以是null。那么主要看该等式是否成立:capacity >= Integer的最大值。所以一般不会rehash

static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;//这里为2147483647,它是HashMap的属性,初始化的时候就已赋值
    //Holder这个类是HashMap的子类,
     private static class Holder {
     //这里定义了我们需要的常量,但是它没赋值,我们看看它是怎么赋值的?
        static final int ALTERNATIVE_HASHING_THRESHOLD;

        static {
//是否有自定义传参threshold,一般不会,所以altThreshold==null
            String altThreshold = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction(
                    "jdk.map.althashing.threshold"));

            int threshold;
            try {
                threshold = (null != altThreshold)
                        ? Integer.parseInt(altThreshold)
                        : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;

                // disable alternative hashing if -1
                if (threshold == -1) {
                    threshold = Integer.MAX_VALUE;
                }

                if (threshold < 0) {
                    throw new IllegalArgumentException("value must be positive integer.");
                }
            } catch(IllegalArgumentException failed) {
                throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
            }

            ALTERNATIVE_HASHING_THRESHOLD = threshold;
        }
    }

计算key的位置,此时length是2^x价值体现出来(减少hash冲突,值能均匀分布)

static int indexFor(int h, int length) {
    return h & (length-1); // 与运算,相当于取模运算,等价于 h%length
}

二、增/改

put
1、第一次put,调用inflateTable(threshold)进行初始化
2、对key为null的处理,将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头。所以table[0]的位置上,永远最多存储1个Entry对象,该链表只有一个元素。
3、hash处理:1.重复,替换旧值;2. 对hash进行索引计算indexFor(hash, table.length),索引且hash不同,hash冲突,头插法。否则新建Entry对象
4、如果是改key的value,put后返回oldValue

public V put(K key, V value) {  
    // 如果table引用指向成员变量EMPTY_TABLE,那么初始化HashMap(设置容量、临界值,新的Entry数组引用)
    // 默认容量16,threshold = loadfactor
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    
    // 若“key为null”,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头
    // 所以table[0]的位置上,永远最多存储1个Entry对象,形成不了链表。key为null的Entry存在这里 
    if (key == null)  
        return putForNullKey(value);
    
    // 若“key不为null”,则计算该key的哈希值
    int hash = hash(key);  
    // 搜索指定hash值在对应table中的索引
    int i = indexFor(hash, table.length);  
    // 循环遍历table数组上存在的Entry对象的链表,判断该位置上hash是否已存在
    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))) {  
            // 如果这个key对应的键值对已经存在,就用新的value代替老的value,并返回老的value!
            V oldValue = e.value;
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;
        }  
    }  
    // 修改次数+1
    modCount++;
    // table数组中没有key对应的键值对,就将key-value添加到table[i]处 
    addEntry(hash, key, value, i);  
    return null;  
}

懒加载,新增的时候初始化hash map

	//初始化HashMap
  private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize - 找到一个 >= toSize的2^x次方数,即是2的次幂增长的
        int capacity = roundUpToPowerOf2(toSize); // 实现了增长为2的幂运算. 实现也比较简单

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);//初始化hashSeed变量
    }

找到一个 >= number的2^x次方数,即是2的次幂

 private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; 
        // 方法Integer.highestOneBit((number - 1) << 1) 即2^x <= ((number-1)<<1);如number = 7, (6<<1) =6*2^1=12 ,需要(2^3 = 8) >= 6,返回2^3
        //即该方法会返回一个大于等于(number-1)的2^x数,而Integer.highestOneBit(number) 返回的是小于等于number的2^x数。造成这个原因的是<<1将number的值变大
        //为什么要number-1?如果number是2^x的值,那么返回的是number。如number=8,Integer.highestOneBit(number << 1)返回16,不合需要做的含义
        //,Integer.highestOneBit((number - 1) << 1)返回的是8
    }

插入键为null的值

    private V putForNullKey(V value) {
        //可以看到键为null的值永远被放在哈希表的第一个桶中,即永远放在table[0]中,再次入值时,只会覆盖原来的,不会形成链表
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            //一旦找到键为null,替换旧值
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        //如果第一个桶中为null或没有节点的键为null的,插入新节点
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

新增一个Entry

void addEntry(int hash, K key, V value, int bucketIndex) {
        //如果尺寸已将超过了阈值并且桶中索引处不为null
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //扩容2倍
            resize(2 * table.length);
            //重新计算哈希值
            hash = (null != key) ? hash(key) : 0;
            //重新得到桶索引
            bucketIndex = indexFor(hash, table.length);
        }

        //创建节点
        createEntry(hash, key, value, bucketIndex);
    }

 void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        //将该节点作为头节点,此时如果hash冲突,则头插法,next指向原来的头
        table[bucketIndex] = new Entry<>(hash, key, value, e); 
        //尺寸+1,这个值在获取、删除时都有用到
        size++;
    }

三、查

get
1、处理key为null的情况,取table[0]的value
2、查找key不为null的value
注:都需要判断size是否为0,即是否为空map

获取key的的value值

    public V get(Object key) {
    		//如果Key值为空,则获取对应的值,这里也可以看到,HashMap允许null的key,其内部针对null的key有特殊的逻辑(详细看插入时的操作)
        if (key == null) 
            return getForNullKey();  
        Entry<K,V> entry = getEntry(key);//获取实体  

        return null == entry ? null : entry.getValue();//判断是否为空,不为空,则获取对应的值  
    }

获取key为null的值

    private V getForNullKey() {  
    	//如果元素个数为0,则直接返回null;说明没有值插入或者已删除完
        if (size == 0) {
            return null;  
        }  
        //key为null的元素存储在table的第0个位置;这个循环最多做一次操作,因为插入时值存储最新key为null的value
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
            if (e.key == null)//判断是否为null  
                return e.value;//返回其值  
        }  
        return null;  
    } 

获取键值为key的元素

    final Entry<K,V> getEntry(Object key) {  
        if (size == 0) {//元素个数为0  
            return null;//直接返回null  
        }  

        int hash = (key == null) ? 0 : hash(key);//获取key的Hash值
        for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {//计算存储位置并进行遍历
            Object k;  
            if (e.hash == hash &&  
                ((k = e.key) == key || (key != null && key.equals(k))))//判断Hash值和对应的key,合适则返回值  
                return e;  
        }  
        return null;  
    } 

关于e.hash == hash在代码中是否加入问题?详情看

四、删

remove
1、size为0,即空map,返回null。
2、查找key不为null且hash值相同的key并remove,返回删除的entry对象,否则返回null。

public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value); //判断
    }
    
final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) { //map中无值,直接返回null
            return null;
        }
        
        //计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //得到桶索引
        int i = indexFor(hash, table.length);
        //记录待删除节点的前一个节点
        Entry<K,V> prev = table[i];
        //待删除节点
        Entry<K,V> e = prev;

        //遍历
        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            //判断是否匹配,匹配则删除节点
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++; 
                size--; //数目减1
                //在链表中的操作
                if (prev == e) // 如果恰好是prev,则将下一结点作为头结点
                    table[i] = next;
                else
                    prev.next = next; //否则指向删除结点的子结点
                e.recordRemoval(this);
                return e;
            }
            //不匹配,继续遍历
            prev = e;
            e = next;
        }
	// 不存在,返回null
        return e;
    }

五、扩容

1.7的扩容是插入之前之前判断,而1.8是插入之后再判断是否需要扩容,不过都是扩容2倍 resize(2 * table.length)。
扩容条件:

  1. size >= threshold
    size:当前map中存在的所有元素,数组+链表
    初始化hash map时:threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1)
    再次扩容时:threshold = capacity * loadFactor

  2. null != table[bucketIndex]

扩容到新容量 - 扩容容易出现并发问题,线程不安全

 void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        //如果旧容量已经达到了最大,将阈值设置为最大值,与1.8相同
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        
        //创建新哈希表
        Entry[] newTable = new Entry[newCapacity];
        //将原数组中的元素迁移到扩容后的数组中 
        //死循环就是在这个方法中产生的
        transfer(newTable, initHashSeedAsNeeded(newCapacity)); // boolean initHashSeedAsNeeded()判断是否重新获取随机的hashSeed,这个值在hash()中用到
        table = newTable;
        //更新阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

如果hashSeed变了,那么rehash为true,否则为false。
计算得到新表中的索引,i的值有可能不同,所以可能和原来的存储位置不同,不同的newIndex = oldIndex + oldCapacity
比如:
hash值 1010 1010
容量16,则
15: 0000 1111
& 1010 1010
= 0000 1010
=10

扩容到32
31:0001 1111
& 1010 1010
= 0000 1010
=10
此时下标不变。
如果hash为1011 1010
则:
容量16,则
15: 0000 1111
& 1011 1010
= 0001 1010
= 16+10=26

扩容到32
31:0001 1111
& 1011 1010
= 0001 1000
= 16+10=26

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //遍历旧表
        for (Entry<K,V> e : table) {
            //当桶不为空
            while(null != e) { //循环遍历链表中的entry
                Entry<K,V> next = e.next;
                //如果hashSeed变了,需要重新计算hash值
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //计算得到新表中的索引,i的值有可能不同,所以可能和原来的存储位置不同,不同的newIndex = oldIndex + oldCapacity
                int i = indexFor(e.hash, newCapacity);
                //将新节点作为头节点添加到桶中
                //采用链头插入法将e插入i位置,最后得到的链表相对于原table正好是头尾相反的(新值总是插在最前面)
                e.next = newTable[i];
                newTable[i] = e;
                //下一轮循环
                e = next;
            }
        }
    }

boolean initHashSeedAsNeeded()判断是否重新获取随机的hashSeed,这个值在hash()中用到。

五、问题

1、为什么增删改查时,判断时e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))),为什么加入e.hash == hash??
有人觉得上面在定位到数组位置之后然后遍历链表的时候,e.hash == hash这个判断没必要,仅通过equals判断就可以。其实不然,试想一下,如果传入的key对象重写了equals方法却没有重写hashCode,而恰巧此对象定位到这个数组位置,如果仅仅用equals判断可能是相等的,但其hashCode和当前对象不一致,这种情况,根据Object的hashCode的约定,不能返回当前对象,而应该返回null。

2、为什么容量大小设置2^x次幂形式?
计算存放位置的公式为h & (length-1),如果length的值是2^x,如length为32,二进制为0010 0000,length - 1二进制为0001 1111,此时如果和有序hash进行与运算,总是有序且均匀获取到下标。从而减少碰撞,即减少hash冲突且值能均匀分布。如果不是2^x,那么hash冲突变大,链表变长,性能就会下降。
该计算方式等价h & (length-1) == h%length

link:https://blog.csdn.net/eaphyy/article/details/84386313

3、为什么底层用链表?
链地址法处理hash冲突,形成链表,且单向链表的速度高于数组。对于hash相同的key,取模获取的数组下标index肯定也相同,所以此时用链表存储不同的value。当get(key)时,即使获取的数组下标index相同,比较hash值是否相同而获取value。

4、为什么说HashMap线程不安全?不安全造成什么后果?
在扩容时多线程会出现死链。
两个线程A,B同时对HashMap进行resize()操作,在执行transfer方法的while循环时,若此时当前槽上的元素为a–>b–>null
  1.线程A执行到 Entry<K,V> next = e.next;时发生阻塞或cpu调度挂起,此时e=a,next=b
  2.线程B完整的执行了整段代码,此时新表newTable元素为b–>a–>null
  3.线程A继续执行后面的代码,执行完一个循环之后,newTable变为了a<–>b,此时退出该循环,造成数据丢失(只剩2个数据)
  4.当get(key)时,取该下标值,hash冲突进来,由get(key)代码可知,for循环取,此时如果获取不到hash相等等符合获取条件,将一致循环下去。 一直死循环,CPU飙升,可能会造成宕机。 – 形成循环链的后果。

5、多线程下扩容可能会出现数据丢失
同样在resize的transfer方法上
  1.当前线程迁移过程中,其他线程新增的元素有可能落在已经遍历过的哈希槽上;在遍历完成之后,table数组引用指向了newTable,这时新增的元素就会丢失,被无情的垃圾回收。
  2.如果多个线程同时执行resize,每个线程又都会new Entry[newCapacity],此时这是线程内的局部变量,线程之前是不可见的。迁移完成后,resize的线程会给table线程共享变量,从而覆盖其他线程的操作,因此在被覆盖的new table上插入的数据会被丢弃掉。

6、为什么hash冲突时,新数据放在链表头部?
头部快,如果放在其他地方,还需要检索,链表过长,性能底下

7、多个key为null的情况,hashmap怎么处理?
对于key为null的值,默认放在table[0]位置。新增时先判断原先是否有值,没有则新建一个entry对象;有则判断key是否为null,不为null,则覆盖value值。

8、加载因子loadfactor为什么时0.75?
默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生。理想状态下,在随机哈希值的情况,对于loadfactor = 0.75 ,虽然由于粒度调整会产生较大的方差,桶中的Node的分布频率服从参数为0.5的泊松分布。
扩容时:threshold=(int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1)

link:link:https://www.cnblogs.com/liyus/p/9916562.html

六、总结

1、HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变(发生扩容时,元素位置会重新分配)

2、对key进行hash计算,并int i = hashKey % table.length计算存在数组中的下标(下标范围[0, table.length -1])

3、JDK1.7和JDk1.8
对JDK1.7和JDk1.8中HashMap的相同与不同点做出总结。
首先是相同点:

  1. 默认初始容量都是16,默认加载因子都是0.75。容量必须是2的指数倍数
  2. 扩容时都将容量增加1倍,原来的2倍
  3. 根据hash值得到桶的索引方法一样,都是i=hash&(cap-1)
  4. 初始时表为空,都是懒加载,在插入第一个键值对时初始化
  5. 键为null的hash值为0,都会放在哈希表的第一个桶中

接下来是不同点,主要是思想上的不同,不再纠结与实现的不同:

  1. 最为重要的一点是,底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构
  2. 主要区别是插入键值对的put方法的区别。1.8中会将节点插入到链表尾部,而1.7中会将节点作为链表的新的头节点
  3. JDk1.8中一个键的hash是保持不变的,JDK1.7时resize()时有可能改变键的hash值
  4. rehash时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序
  5. JDK1.8是通过hash&cap==0将链表分散,而JDK1.7是通过更新hashSeed来修改hash值达到分散的目的
    6.关于扩容,jdk1.7在插入前检查;jdk1.8在插入后检查

link:
https://blog.csdn.net/qq_19431333/article/details/61614414
https://blog.csdn.net/xiaokang123456kao/article/details/77503784

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值