经典面试题之HashMap(二)

简介: 我们知道int 的极限最大值 Integer.MAX_VALUE 是2的31次方减1,即2147483647,如果 1 << 30 改为 1 << 31 ,由于int是有符号数,这个值将为 -2147483648,而且hashMap的容量都是2的整数次幂,也就只能是2的30次方了。然而这并不是HashMap的最大容量。

接上文 经典面试题之HashMap(一)


三 不考虑内存限制,HashMap可以无限存储数据吗?


不可以,HashMap是有最大容量上限的。我们还是来看下源码注释:


/**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;


很明显,如果构造函数传入的值大于MAXIMUM_CAPACITY ,那么替换成该数 MAXIMUM_CAPACITY 是 1 << 30 即 2的30次方。


为什么是1 << 30?  1 <<31 不行吗?


注意看这个值是int 类型的。我们知道int 的极限最大值 Integer.MAX_VALUE 是2的31次方减1,即2147483647,如果 1 << 30 改为 1 << 31 ,由于int是有符号数,这个值将为 -2147483648,而且hashMap的容量都是2的整数次幂,也就只能是2的30次方了。然而这并不是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);
    }


上面代码中有一句


if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;


如果我要存的数目大于 MAXIMUM_CAPACITY,你还把我的容量缩小成 MAXIMUM_CAPACITY?其实不是的,在resize()方法中有一句


if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }


在这里我们可以看到其实
hashmap的“最大容量“是Integer.MAX_VALUE;


threshold=capacity*loadFactor threshold 表示当HashMap的size大于threshold时会执行resize操作。size就是HashMap中实际存在的键值对数量。


思考题:如果到达了HashMap的容量上限,再继续添加元素,会怎样?


其实你可以计算一下,当HashMap到达容量上限后占用的内存大小,已经很大了,所以一般情况下是内存溢出,我们在日常使用时,一般情况下不会把那么大的数据全部放到一个HashMap中。然而如果不考虑内存溢出的情况,就是你有一个超大的内存,那这个时候会怎样?


四 如何确定哈希桶数组索引位置?


我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的,在HashMap中是这样做的:调用下面的代码来计算该对象应该保存在table数组的哪个索引处。


static int indexFor(int h, int length) {  
    //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 取模运算
}


这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。


注意  h& (length-1) 当且仅当length(即capacity)是2的整倍数的时候才等于 h % length ,从这个度也说明了capacity为什么一定要用2的整次幂。


数组长度-1 正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。


14.png


五 了解HashMap的hash函数吗?


我们先来说说hash算法的一般实现:

  • 大数变小数-->取模
  • 让结果的规律性不明显--> 异或、改变原始数据、移位
  • 碰撞是存在的,主要是看解决碰撞的方案


java中常用的hashCode算法


  • Object类的hashCode。返回对象的经过处理后的内存地址。由于每个对象的内存地址都不一样,所以哈希码也不一样,这是个native方法。取决于JVM的内部设计,一般是某种C地址的偏移。
  • String 类的hashCode,根据String类包含的字符串的内容,根据一种特殊的算法返回哈希码,只要字符串的内容相同,返回的哈希码也相同。
  • Integer 等包装类,返回的哈希码就是Integer对象里所包含的那个整数的值,例如 Integer  i1 = new Integer(100), i1.hashCode() 的值就是 100。由此可见,两个一样大小的Integer对象,返回的哈希码也一样。
  • int、char这样的基础类,它们不需要hashCode,如果需要存储时,将进行自动装箱操作,计算方法同上。


我们来看下HashMap中的hash算法是如何实现的:


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


hash(Object key)的作用就是重新计算hash值。


HashMap中为什么不直接用key.hashCode()获取哈希值,而是使用(h = key.hashCode()) ^ (h >>> 16)?


我们通过上文了解了HashMap如何计算出数组索引位置,但其实有一个问题,就是即使我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比蛋疼。


这时候“扰动函数”的价值就体现出来了


15.png



右位移16位,正好是
32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。(JDK 7做了4次右移,估计是边际效应的原因,JDK8就只做了一次右移)


总结 :(h = key.hashCode()) ^ (h >>> 16)这样写有点类似重写了hashCode,确保得出的数足够的随机,因为进行hash计算的时候 确保它的数足够的分散,以便于计算数组下标的时候存放的值足够分散。

相关文章
|
3月前
|
存储 算法 Java
【Java集合类面试八】、 介绍一下HashMap底层的实现原理
HashMap基于hash算法,通过put和get方法存储和获取对象,自动调整容量,并在碰撞时用链表或红黑树组织元素以优化性能。
|
28天前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
54 5
|
1月前
|
存储 算法 安全
HashMap常见面试题(超全面):实现原理、扩容机制、链表何时升级为红黑树、死循环
HashMap常见面试题:红黑树、散列表,HashMap实现原理、扩容机制,HashMap的jd1.7与jdk1.8有什么区别,寻址算法、链表何时升级为红黑树、死循环
|
3月前
|
Java 索引
【Java集合类面试九】、介绍一下HashMap的扩容机制
HashMap的扩容机制包括初始容量16,以2的次方进行扩充,使用负载因子0.75判断是否扩容,以及链表长度达到阈值时转换为红黑树,以优化性能。
【Java集合类面试九】、介绍一下HashMap的扩容机制
|
3月前
|
存储 安全 Java
一天十道Java面试题----第二天(HashMap和hashTable的区别--------》sleep、wait、join)
这篇文章是关于Java面试的第二天笔记,涵盖了HashMap与HashTable的区别、ConcurrentHashMap的实现原理、IOC容器的实现方法、字节码的概念和作用、Java类加载器的类型、双亲委派模型、Java异常体系、GC如何判断对象可回收、线程的生命周期及状态,以及sleep、wait、join、yield的区别等十道面试题。
一天十道Java面试题----第二天(HashMap和hashTable的区别--------》sleep、wait、join)
|
3月前
|
存储 Java
【Java集合类面试七】、 JDK7和JDK8中的HashMap有什么区别?
JDK7中的HashMap使用数组加链表解决冲突,而JDK8增加了红黑树结构以优化链表过长时的性能,提高查找效率。
|
3月前
|
安全 Java
【Java集合类面试十五】、说一说HashMap和HashTable的区别
HashMap和Hashtable的主要区别在于Hashtable是线程安全的,不允许null键和值,而HashMap是非线程安全的,允许null键和值。
|
3月前
|
安全 Java
【Java集合类面试十三】、HashMap如何实现线程安全?
实现HashMap线程安全的方法包括使用Hashtable类、ConcurrentHashMap,或通过Collections工具类将HashMap包装成线程安全的Map。
|
3月前
|
Java
【Java集合类面试十一】、HashMap为什么用红黑树而不用B树?
HashMap选择使用红黑树而非B树,是因为红黑树在内存中实现简单,节点更小,占用内存少,且在插入、删除和查找操作上提供更好的平衡性能。
|
3月前
|
安全 Java
【Java集合类面试十六】、HashMap与ConcurrentHashMap有什么区别?
HashMap是非线程安全的,而ConcurrentHashMap通过减少锁粒度来提高并发性能,检索操作无需锁,从而提供更好的线程安全性和性能。