03-HashMap

HashMap

1、HashMap的主要参数都有哪些?

  • 初始化桶容量,默认16,
  • 负载系数,默认0.75
  • 最大容量,2的30次方
  • 扩容阈值,默认容量*负载系数,当元素个数达到阈值,再put进来元素,就会触发扩容
  • table数组,

2、HashMap 的数据结构?

  • 1.7是数组 + 链表,实体的数据结构是k-v,hash,next引用
  • 1.8是数组+链表/红黑树

3、hash的计算规则?

  • JDK8中使用扰动函数,32位hashCode中,高位不变,然后将高16位和低16位进行异或运算的结果作为低16位,最后的结果就是hash值,hash对长度减一做取余操作定位对应的桶;
  • JDK7中稍微有点不一样

4、hash碰撞和解决方法?

  • hash碰撞就是两个key通过hash值计算桶的位置的index,得到相同的index,这就是hash碰撞
  • 解决hash碰撞,采用链地址法,jdk8中链的元素大于等于8个会转换为红黑树,小于等于6个则转换为链表。另外hash值计算时的扰动函数也可以减少hash碰撞

5、关于扩容

5.1 为什么扩容是以2的幂次?

  • 为了便于hash计算后定位到桶,hash值对2的幂次取余在计算上是对该数-1进行按位与运算,效率高。(hash % len = hash | len-1 )
  • 另外2倍扩容,可以减少resize的数据重分配。部分元素不需要移动,比如原来16,位于hash为21的在5的位置,扩容之后就在21的位置了,如果hash37原本也是在5的位置,扩容之后还是在5的位置,这样有助于分散处于一个桶中的多个元素,

5.2 HashMap的扩容时机,什么时候会进行rehash?

  • 都是在插入元素的时候。不过稍微有点不一样,代码中1.8是在插入完成之后会检查扩容,1.7则是扩容之后,再头插法新增节点。扩容的思路都类似,就是扩大二倍,如果达到了最大值则不会扩容了,扩容的条件是元素个数达到阈值;

6、存取

6.1 HashMap put的过程

  • 定位到桶,如果桶为空则直接插入,如果不为空,1.7 以前是在链表进行头插法,在1.8是进行尾插法,1.8还会进行链表和红黑树转换的阈值判断,检查是否需要转换为红黑树。
    另外在1.8中增加了putOnlyIfAbsent 的功能,在1.7中没有
//1.8
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //1.扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //2.如果定位到桶没有元素,那么构建节点直接返回
        if ((p = tab[i = (n - 1) & hash]) == null)
            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) {
                    //如果遍历的过程未找到,就会到链表最后一个位置,因此1.8是尾插法
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果大于等于8就转为红黑树,binCount大于等于7的话,链表总元素个数就大于等于8了
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果遍历的过程中找到了,就会在这里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;
                //考虑onlyIfAbsent的情况
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                //返回旧元素
                return oldValue;
            }
        }
        ++modCount;
        //扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }


//1.7
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //处理key为null
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        //遍历链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果找到一样的key,那就替换,返回
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //如果一直没有找到,就进行插入,这里面是头插法
        addEntry(hash, key, value, i);
        return null;
    }
    
 void addEntry(int hash, K key, V value, int bucketIndex) {
        //检查扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            //重新计算hash
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
    
    //这里面能够看出来是头插法,原本的桶中元素作为next赋值给了新的节点
     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++;
    }

6.2 HashMap的get过程

  • 先得到key的hash值,再把这个hash值与length-1按位与(取余),得到table数组的下标。取出这个下标值的key,与传入的key比较,如果相同那就是这个了。如果不同呢,那就沿着这个单向链表向后找,直到找到或找到结束也找不到,找不到返回null。

7、HashMap初始化传入的容量参数的值就是HashMap实际分配的空间么?

  • 不是,空间是2的幂次,不管传入的数字是什么,最后会转成一个大于该数字的2的幂次。且构造之后不会分配空间,会在第一次put元素的时候存入
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;
    }
//方法执行的效果就是将这个数字的所有位都置为1,最后加上1,就得到了2的幂次,并且数字不会过大,因为都是把这个数字的地位置为1了

8、为什么String, Interger这样的wrapper类适合作为键?

  • 一个是因为不可变,另一个是因为已经重写了hashcode和equals方法
  • String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

9、自定义对象做key什么要重写hashcode方法和equals方法?如果都不重写会怎么样?如果值重写一个会怎么样?

  • 假设我们使用自定义对象作为key,new了两个一模一样的对象A和B,先用map.put(A,“123”),再使用map.get(B),肯定不能获取到“123”,因为这两个对象的hash值不等且equals也是返回false。但是我们肯定期望能够获取到123,因此我们需要重写这两个方法,让他们在属性一致的时候,hash值一致且equals为true,这样才能满足需求。
  • 如果重写了hash,equals不一样,那么在get的时候,因为二者的equals为false,因此也是get不到的,
  • 如果重写了equals,没有重写hash,那么AB的hash值不一样,那么也是get不到的,从源码我们可以看到不管是put还是get,判断key一致的条件是hash值首先要一样,然后要么equals为true,要么是一个对象(==wei true)
//key一致的条件首先要hash一致,其次要equals为true或者==为true,二者是必须同时满足的
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
  • 结合HashMap的原理这里我们也可以看出我们我们不但要重写hashcode方法,还要尽量降低hashcode方法的冲突

10、HashMap的key是否可以为null

  • key和value都可以为null

11、HashMap的复杂度

  • HashMap整体上性能都非常不错,但是不稳定,为O(N/Buckets),N就是以数组中没有发生碰撞的元素。
  • 新增查找获取的复杂度都是O(N/Buckets),设计良好的话接近一O(1)
  • 如果某个桶中的链表记录过大的话(大于等于8),就会把这个链动态变成红黑二叉树,使查询最差复杂度由O(N)变成了O(logN)

12、HashMap在JDK7和8的区别

  • https://my.oschina.net/hosee/blog/618953
  • https://blog.csdn.net/qq_36520235/article/details/82417949
对比维度1.71.8
数据结构数组 + 链表数组 + 链表/红黑树
插入链表头插法尾插法
扩容计算hash不计算hash要么不动要么往后移动扩容的大小(通过和旧容量与运算得到)。
hash计算9次扰动处理(4次位运算 + 5次异或)2次扰动处理( 1次位运算 + 1次异或)
//1.7
final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

//1.8
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

13、我们能否让HashMap同步?

  • Map m = Collections.synchronizedMap(map);思想很简单,返回的是一个SynchronizedMap,该类实现了Map接口,因此可以直接调用Map接口的方法,它内部持有真正的非线程安全的Map和一把锁(其实就是一个Object对象),然后调用的时候都加锁,再调用内部Map的方法,让所有方法变成了同步方法,方式其实并不是很可取,推荐使用CocurrentHashMap。

14、HashMap 和 HashTable有何不同?

  • HashTable的相关方法都是synchronized同步的get方法也是,比如get、put,putAll,remove、size、isEmpty、keys、contains、containsKey、clear、forEach等,甚至toString和equals都是同步方法,我们看到很多只读方法也是同步的,其实读取方法有时候没有必要同步,这样让它性能很低,比如单线程写多线程读取的时候我可以用HashMap,但是用HashTable多线程get都要排队,性能低。
  • HashTable无论key或value都不能为null,HashMap只能允许一个key为null,可以运行多个value为null。而且HashTable是线程安全的,HashMap是线程不安全的。

15、JDK1.8中对 HashMap做了哪些性能优化?

  • 主要是:链表和红黑树的转换,阈值分表为6和8
  • 旧版本的HashMap存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(TREEIFY_THRESHOLD默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能(O(logn))。当长度小于(UNTREEIFY_THRESHOLD默认为6),就会退化成链表。
  • 扩容时,1.7需要重新计算hash,1.8不需要,并且要么元素不动,要么元素移动一个2的N次方
  • 参考

16、高并发下HashMap为什么会线程不安全?

  • JDK 1.7 下死循环(头插法导致)
  • JDK 1.8 下值覆盖(尾插法规避了死循环,但是会出现值覆盖),两个线程对同一个位置进行put操作,对应桶为null且hash冲突时,线程A put完之后,线程B解挂后再操作时,因B之前已经判断过hash,就不判断直接写入

参考:HashMap 线程不安全的体现

  • 下面是我给出的一个简单的1.8下值覆盖的例子,hashmap保存的键值对中键和值都是一样的,理论上线程安全就会有10万个键值对,由于线程不安全,少于10万个,并且打印出键值不等的键值对
public class HashMapTest {

    public static final CountDownLatch latch = new CountDownLatch(5);

    public static void main(String[] args) throws InterruptedException {
        HashMapThread thread1 = new HashMapThread("HashMap-TestThread-1");
        HashMapThread thread2 = new HashMapThread("HashMap-TestThread-2");
        HashMapThread thread3 = new HashMapThread("HashMap-TestThread-3");
        HashMapThread thread4 = new HashMapThread("HashMap-TestThread-4");
        HashMapThread thread5 = new HashMapThread("HashMap-TestThread-5");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
        latch.await();
        System.out.println("size: " + HashMapThread.map.size());
        for (Integer i : HashMapThread.map.keySet()) {
            if (!HashMapThread.map.get(i).equals(i)) {
                System.out.println(i + "  》》 " + HashMapThread.map.get(i));
            }
        }
    }
}


public class HashMapThread extends Thread {

    private static AtomicInteger count = new AtomicInteger();
    public static Map<Integer, Integer> map = new HashMap<>();

    public HashMapThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (count.get() < 1000000) {
            map.put(count.get(), count.get());
            count.incrementAndGet();
        }
        HashMapTest.latch.countDown();
    }
}
  • 部分打印如下,可以看到只有不到6万个键值对,每次都不一样,而且被覆盖的值和原本正确的值相差只有一两个数,因为原子变量是递增的,这体现出了HashMap的值覆盖问题
size: 597274
786435  》》 786437
786495  》》 786496
786504  》》 786506
786505  》》 786506
786516  》》 786517
786514  》》 786515
786532  》》 786533
786555  》》 786556
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值