去面试时,hashmap总是被经常问的问题,下面总结了几道关于hashmap的问题。

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

2、hashmap的数据结构是什么样子的?自己如何实现一个hashmap?

3、hash计算规则是什么?

4、说说hashmap的存取过程?

5、说说hashmap如何处理碰撞的,或者说说它的扩容?

解答:以1.7为例,也会掺杂一些1.8的不同点。

1、

1)桶(capacity)容量,即数组长度:DEFAULT_INITIAL_CAPACITY=1<<4;默认值为16

  即在不提供有参构造的时候,声明的hashmap的桶容量;

2)MAXIMUM_CAPACITY = 1 << 30;

  极限容量,表示hashmap能承受的最大桶容量为2的30次方,超过这个容量将不再扩容,让hash碰撞起来吧!

3)static final float DEFAULT_LOAD_FACTOR = 0.75f;

  负载因子(loadfactor,默认0.75),负载因子有个奇特的效果,表示当当前容量大于(size/)时,将进行hashmap的扩容,扩容一般为扩容为原来的两倍。

4)int threshold;阈值

  阈值算法为capacity*loadfactory,大致当map中entry数量大于此阈值时进行扩容(1.8)

5)transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;(默认为空{})

  核心的数据结构,即所谓的数组+链表的部分。

2、hashmap的数据结构是什么样子的?自己如何实现一个hashmap?

  主要数据结构即为数组+链表。

  在hashmap中的主要表现形式为一个table,类型为Entry<K,V>[] table

  首先是一个Entry型的数组,Entry为hashmap的内部类:

  

  1. static class Entry<K,V> implements Map.Entry<K,V> {
  2. final K key;
  3. V value;
  4. Entry<K,V> next;
  5. int hash;
  6. }

  在这里可以看到,在Entry类中存在next,所以,它又是链表的形式。

  这就是hashmap的主要数据结构。

3、hash的计算规则,这又要看源码了:

  

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

  这是1.8的源码,1.7太复杂但原理是一致的,简单说这就是个“扰动函数”,最终的目的是让散列分布地更加均匀。

  算法就是拿存储key的hashcode值先右移16位,再与hashcode值进行亦或操作,即不求进位只求按位相加的值:盗图:

  

  最后是如何获得,本key在table中的位置呢?本身应该是取得了hash进行磨除取余运算,但是,源码:

  1. static int indexFor(int h, int length) {
  2. // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
  3. return h & (length-1);
  4. }

  为什么又做了个与运算求得位置呢?简单说,它的意义和取余一致。

  不信可以自己算一下。

  首先说,他利用了table的长度肯定是2的整数次幂的原理,假设当前length为16,2的4次方

  而与&运算,又是只求进位运算,比如1111&110001结果为000001

  只求进位运算(&),保证算出的结果一定在table的length之内,最大为1111。

  故而,它的运算结果与价值等同于取余运算,并且即使不管hash值有多大都可以算出结果,并且在length之内。

  并且,这种类型的运算,能够更加的节约计算机资源,少了加(计算机所有运算都是加运行)运算过程,更加地节省资源。

4、hashmap的存取过程

  源码1.7:

  1. /**
  2. *往hashmap中放数据
  3. */
  4. public V put(K key, V value) {
  5. if (table == EMPTY_TABLE) {
  6. inflateTable(threshold);//判断如果为空table,先对table进行构造
  7. //构造通过前面的几个参数
  8. }
  9. //首先判断key是否为null,为null也可以存
  10. //这里需要记住,null的key一定放在table的0号位置
  11. if (key == null)
  12. return putForNullKey(value);
  13. //算出key的hash值
  14. int hash = hash(key);
  15. //根据hash值算出在table中的位置
  16. int i = indexFor(hash, table.length);
  17. //放入K\V,遍历链表,如果位置上存在相同key,进行替换value为新的,且将替换的旧的value返回
  18. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  19. Object k;
  20. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  21. V oldValue = e.value;
  22. e.value = value;
  23. e.recordAccess(this);
  24. return oldValue;
  25. }
  26. }
  27. modCount++;
  28. //增加一个entry,有两种情况,1、如果此位置存在entry,将此位置变为插入的entry,且将插入entry的next节点变为原来的entry;2、如果此位置不存在entry则直接插入新的entry
  29. addEntry(hash, key, value, i);
  30. return null;
  31. }

取数据:

  1. //根据key获得一个entry
  2. public V get(Object key) {
  3. //如果key为null,获取0号位的切key为null的值
  4. if (key == null)
  5. return getForNullKey();
  6. //如果不是,获取entry,在下面方法
  7. Entry<K,V> entry = getEntry(key);
  8. //合法性判断
  9. return null == entry ? null : entry.getValue();
  10. }
  11. //获取一个key不为null的entry
  12. final Entry<K,V> getEntry(Object key) {
  13. //如果table为null,则返回null
  14. if (size == 0) {
  15. return null;
  16. }
  17. //计算hash值
  18. int hash = (key == null) ? 0 : hash(key);
  19. //根据hash值获得table的下标,遍历链表,寻找key,找到则返回
  20. for (Entry<K,V> e = table[indexFor(hash, table.length)];
  21. e != null;
  22. e = e.next) {
  23. Object k;
  24. if (e.hash == hash &&
  25. ((k = e.key) == key || (key != null && key.equals(k))))
  26. return e;
  27. }
  28. return null;
  29. }

5.扩容和碰撞

  先说碰撞吧,由于hashmap在存值的时候并不是直接使用的key的hashcode,而是通过扰动函数算出了一个新的hash值,这个计算出的hash值可以明显的减少碰撞。

  还有一种解决碰撞的方式就是扩容,扩容其实很好理解,就是将原来桶的容量扩为原来的两倍。这样争取散列的均匀,比如:

  原来桶的长度为16,hash值为1和17的entry将会都在桶的0号位上,这样就出现了碰撞,而当桶扩容为原来的2倍时,hash值为1和17的entry分别在1和17号位上,整号岔开了碰撞。

  下面说说何时扩容,扩容都做了什么。

  1.7中,在put元素的过程中,判断table不为空、切新增的元素的key不与原来的重合之后,进行新增一个entry的逻辑。

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. if ((size >= threshold) && (null != table[bucketIndex])) {
  3. resize(2 * table.length);
  4. hash = (null != key) ? hash(key) : 0;
  5. bucketIndex = indexFor(hash, table.length);
  6. }
  7. createEntry(hash, key, value, bucketIndex);
  8. }

  由源代码可知,在新增元素时,会先判断:

  1)当前的entry数量是否大于或者等于阈值(loadfactory*capacity);

  2)判断当前table的位置是否存在entry。

  经上两个条件联合判定,才会进行数组的扩容工作,最后扩容完成才会去创建新的entry。

  而扩容的方法即为:resize()看代码

  

  1. void resize(int newCapacity) {
  2. //拿到原table对象
  3. Entry[] oldTable = table;
  4. //计算原table的桶长度
  5. int oldCapacity = oldTable.length;
  6. //先判定,当前容量是否已经是最大容量了(2的30次方)
  7. if (oldCapacity == MAXIMUM_CAPACITY) {
  8. //假如达到了,将阈值设为int的最大值2的31次方减1,返回
  9. threshold = Integer.MAX_VALUE;
  10. return;
  11. }
  12. //创建新的table对象
  13. Entry[] newTable = new Entry[newCapacity];
  14. //将旧的table放入新的table中
  15. transfer(newTable, initHashSeedAsNeeded(newCapacity));
  16. //赋值新table
  17. table = newTable;
  18. //计算新的阈值
  19. threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  20. }
  21. //具体的扩容过程
  22. void transfer(Entry[] newTable, boolean rehash) {
  23. int newCapacity = newTable.length;
  24. //遍历原table,重新散列
  25. for (Entry<K,V> e : table) {
  26. while(null != e) {
  27. Entry<K,V> next = e.next;
  28. if (rehash) {
  29. e.hash = null == e.key ? 0 : hash(e.key);
  30. }
  31. int i = indexFor(e.hash, newCapacity);
  32. e.next = newTable[i];
  33. newTable[i] = e;
  34. e = next;
  35. }
  36. }
  37. }

至此,扩容就说完了。。。

HashMap常问面试题整理的更多相关文章

  1. 常问面试题:C++中sizeof的陷阱及应答

    C++中sizeof是经常被问到的一个概念,比如,下面的几个关于sizeof的面试题反复出现在各大IT公司的技术面试当中,我们有必要完全理解并掌握.注:在曾经面试大公司时,我的确被问到过这样的问题. ...

  2. Java常考面试题整理(四)

    有关所有Swing相关的面试题,都可以说是凑数的,感觉自己在敲这些的时候感觉一点用处都没有,可以从第72条开始看. 61.说出三种支持重绘(painting)的组件. 参考答案: Canvas,Fra ...

  3. Java常考面试题整理(二)

    21.Iterator和ListIterator的区别是什么? 参考答案: 下面列出了他们的区别: Iterator可以用来遍历Set和List集合,但是ListIterator只能用来遍历List. ...

  4. Java常考面试题整理(六)

    101.HTTP相应的结构是怎么样的? 参考答案: HTTP相应由三个部分组成: 1.状态码(status code):描述了相应的状态,可以用来检查是否成功的完成了请求.请求失败的情况下,状态码可以 ...

  5. Java常考面试题整理(五)

    81.RMI中的远程接口(Remote Interface)扮演了什么样的角色? 参考答案: 远程接口用来标识哪些方法是可以被非本地虚拟机调用的接口.远程对象必须要直接或者是间接实现远程接口.实现了远 ...

  6. Java常考面试题整理(三)

    明天又要去面试,Good luck to me.,让我在这段时间换个新的工作吧. 41.在Java中,对象什么时候可以被垃圾回收? 参考答案: 当对象对当前使用这个对象的应用程序变得不可触及的时候,这 ...

  7. Java常考面试题整理(一)

    1.什么是java虚拟机?为什么java被称作是"平台无关的编程语言". 参考答案: java虚拟级是一个可以执行java字节码的虚拟机进程,java源文件被编译成能被java虚拟 ...

  8. jsp常问面试题集

    1.Servlet总结 在Java Web程序中,Servlet主要负责接收用户请求 HttpServletRequest,在doGet(),doPost()中做相应的处理,并将回应HttpServl ...

  9. java基础常问面试题

    1.面向对象和面向过程的区别 面向过程 :面向过程性能比面向对象高. 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机.嵌入式开发.Linux/Unix ...

随机推荐

  1. HTML标签和属性三

    八.列表 1.列表的作用 让数据有条理的显示,在数据之前添加标识 但是现在页面布局,经常会使用到无序列表 2.列表的组成 ①有序列表 <ol> <li></li> ...

  2. nginx配置之代理功能

    nginx代理功能 至少需要两台主机: 代理机配置----如下: yum安装的/etc/nginx/nginx.conf location / { #服务机的ip端口 proxy_pass http: ...

  3. python3.x 基础一:str字符串方法

    *字符串不能更改值 数据类型字符串str |  capitalize(...)   返回字符串中第一个字母大写 |      S.capitalize() -> str |       |    ...

  4. Centos7中磁盘管理及扩展

    前提要求: 虚拟机:centos7 虚拟机软件:VMware Workstation 12 在安装Centos系统时,磁盘选择为LVM逻辑卷.当选择为LVM后才能创建逻辑卷等(必须) 数据格式选择的是 ...

  5. 一,初次接触html+css需要注意的小问题

    不足之处请不吝赐教,在评论区帮忙补充 html最基础的,入门学习的是标签,常用的标签有<a>      定义锚.<b>      定义粗体字.<br>      单 ...

  6. nodejs 开发服务端 部署到 iis 服务器环境 -- iisnode 安装问题解决记录

    开发环境 nodejs: v10.15.3 windows: 10 iis: 10 需求: 用Nodejs开发了服务端,要部署到IIS 需要在IIS服务器上安装iisnode,结果遇到问题:安装不上 ...

  7. 题解 P4071 【[SDOI2016]排列计数】 (费马小定理求组合数 + 错排问题)

    luogu题目传送门! luogu博客通道! 这题要用到错排,先理解一下什么是错排: 问题:有一个数集A,里面有n个元素 a[i].求,如果将其打乱,有多少种方法使得所有第原来的i个数a[i]不在原来 ...

  8. SSIS 数据类型 第二篇:变量的数据类型

    变量(Variable)用于存储在Package运行时用到的值,集成服务支持两种类型的变量:用户自定义的变量和系统变量,自定义的变量由用户来定义,系统变量由集成服务来定义. 变量的用途十分广泛,用于容 ...

  9. PHP获取今日、本周、本月、今年的开始日期和结束日期

    /** * 今天开始的Y-m-d H:i:s * * @return string */ public static function beginToday() { return date('Y-m- ...

  10. prometheus配置pushgateway功能测试

    一.环境: 1.prometheus服务器ip:192.168.0.208 2.node-exporter客户机ip:192.168.0.202 二.测试设计考虑: pushgateway类似一台信息 ...