HashMap

1. 特点

  1. 存取无序
  2. 键和值都可以是null,但是只能有一个null键
  3. JDK 1.8之前是数组+链表的组合实现,JDK1.8以后数组+链表+红黑树
  4. 阈值问题:只有同时满足链表长度大于8、数组长度大于等于64时,链表会被转化为红黑树,即数组+链表+红黑树,其他情况还是使用数组+链表。(比如当hashmap满足数组长度大于64,有部分key的链表小于8,那么它也是采用的链表的形式,只有链表长度大于8的链表,才使用红黑树)

2.存储过程

  1. 调用HashMap的构造函数,创建一个长度为16的Entry[]数组存储键值对;而在jdk1.8以后,不在构造函数初始化存储键值对的数组,而是在第一次执行put时创建(延迟加载的概念),并且不是Entry[]数组,而是Node[]数组(符合链表的概念)
  2. 执行put时,先根据key,调用类的hashCode()计算hash,然后结合数组长度计算索引(多种算法),如果计算出的索引空间没有数据,直接将键值对放入。

面试题:哈希表采用何种算法计算索引?还有哪些算法可以计算出索引?
答:hashCode计算hash值,结合数组长度进行计算,具体算法有
1.无符号右移(>>>)
2.按位异或(^)
3.按位与(&)
备选(效率较低)
1.平方取中
2.取余
3.伪随机数

  1. 如果根据hashCode计算得到的hash根据算法算出来的索引处存在数据,则比较两者的hash,如果不一样,则在此索引空间新建一个Node将键值对存储,并将Node放到该索引空间中,与之前存在的数据构成一个链表(拉链法
  2. 如果索引一样,hash也一样,则发生哈希碰撞,那么会调用类的成员方法equals比较两个键是否一样,如果一样,则使用后者的值覆盖;如果不一样,则继续依次比较该索引空间上的链表上的键,如果都不相等,如第3步,拉链处理
    完整过程如下图:
    在这里插入图片描述

3.HashMap集合类的成员

面试题:HashMap的容量为什么是2的n次幂?
在进行hash运算时使用位与(&)方式计算索引,公式hashCode&(length-1),2的n次幂减1,进行位与运算后可以保留低位,最大程度上计算出的索引的均匀性,也就可以最大程度上保证数组中各索引空间的均匀分布,避免造成空间浪费,也可以避免过多数据插入时发生hash碰撞,减少链表、红黑树长度等。总之,可以最大化的提高效率。
上面讨论的是位与(&)运算计算索引的方式,如果是取余:hashCode%length,则不需要考虑数组长度为2的n次幂,但是这种算法开销比较大,效率不高。

面试题:如果用户创建HashMap的时候输入的容量不是2的n次幂,会发生什么现象?
会扩容到比输入容量稍大的2的n次幂的容量,具体做法是容量减1,然后进行右移1、2、4、8、16,每一次右移会执行或运算,得出的结果再进行下一次的右移、或运算,直到hashmap的最大容量2^30。这样可以保证即使用户输入不是2的2次幂,容量也被保证为2的n次幂。

面试题:如果用户创建HashMap的时候输入的容量不是2的n次幂,会发生什么现象?
会扩容到比输入容量稍大的2的n次幂的容量,具体做法是容量减1,然后进行右移1、2、4、8、16,每一次右移会执行或运算,得出的结果再进行下一次的右移、或运算,直到hashmap的最大容量2^30。这样可以保证即使用户输入不是2的2次幂,容量也被保证为2的n次幂。

面试题:为什么链表长度大于等于8才转为红黑树,小于6又转化为链表的结构?
根据官方解释,根据统计学泊松分布的公式,假设在hashCode的离散性比较理想的情况,链表长度等于8的概率非常小,但是jdk无法保证用户计算hashCode的算法是能够保证理想的离散性的,因此基于空间和时间的考虑,选择了8这个数字作为threshold。
另外一种通俗的理解是如果长度为8,红黑树的平均查找长度为log(8)=3,链表的平均查找长度为n/2=4,对数开始比多项式效率高。如果链表长度为6,红黑树的平均查找长度为log(8)=2.6,链表的平均查找长度为n/2=3,虽然速度也很快,但是红黑树需要转化为树和生成树的时间并不会太短,因此综合考虑会选择8作为threshold,其实这也是泊松分布的具体表现。

面试题:为什么负载因子为0.75?
负载因子决定HashMap中node数量达到整体的一定情况进行扩容,大量测试得出的结论0.75是一个折中、高效的选择。

面试题:HashTable和HashMap的区别?
HashTable不允许键为null,原因是HashTable在进行push操作的时候会直接调用key的hashCode方法,如果为null会报空指针异常,HashMap中对key为null的情况会把0的结果返回。

面试题:为什么进行hashCode调用完后还需要右移16位,取异或的结果?
避免高位变化很大,低位变化很小的情况,在这种情况下很容易导致最后计算出的索引是大概率相同的,大大增加hash碰撞的概率。

4.扩容

  1. resize():扩容的核心,当数组中的节点个数大于等于数组长度*loadFactor、链表长度小于6并且数组长度小于64时会优先执行扩容操作,此操作比较耗时,一般在程序设计之处最好知道HashMap大小,尽量减少扩容次数。
  2. 扩容的计算:数组长度n*2,然后执行hash & (n-1)。最后的结果只有两种:索引不变原索引+旧数组的容量,这两种结果取决于hash & (n-1)的多出的1bit是1还是0,计算结果为0则是前者,为1则是后者。因此,jdk8以后,在扩容的时候不需要重新计算hash,只需要看旧数组容量的二进制那一位和hash按位与(&)的结果即可。计算过程和resize示意图如下:
    在这里插入图片描述

5.遍历HashMap

  1. 分别遍历key和values
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
 
public class Test {
	public static void main(String[] args) throws IOException {
		Map<Integer, Integer> map = new HashMap<Integer, Integer>();
		map.put(1, 10);
		map.put(2, 20);
 
		// 迭代键
		for (Integer key : map.keySet()) {
			System.out.println("Key = " + key);
		}
 
		// 迭代值
		for (Integer value : map.values()) {
			System.out.println("Value = " + value);
		}
	}
}
  1. 使用Iterator迭代器迭代
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
 
public class Test {
	public static void main(String[] args) throws IOException {
		Map<Integer, Integer> map = new HashMap<Integer, Integer>();
		map.put(1, 10);
		map.put(2, 20);
 
		Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
		while (entries.hasNext()) {
			Map.Entry<Integer, Integer> entry = entries.next();
			System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
		}
	}
}


  1. 通过get方式(不建议使用)
    在这里插入图片描述

说明:根据阿里手册,不建议使用,因为迭代两次。keySet获取Iterator一次,还有通过get又迭代一次,降低性能

  1. jdk8以后使用Map集合的默认方法
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
 
public class Test {
 
	public static void main(String[] args) throws IOException {
 
		Map<Integer, Integer> map = new HashMap<Integer, Integer>();
		map.put(1, 10);
		map.put(2, 20);
		map.forEach((k, v) -> System.out.println("key: " + k + " value:" + v));
	}
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值