参考:https://www.cnblogs.com/zengcongcong/p/11295349.html
https://zhuanlan.zhihu.com/p/79507868
http://baijiahao.baidu.com/s?id=1665667572592680093&wfr=spider&for=pc
HashMap 就是一个用来存储 键值对 的集合
一、HashMap 整和了数组 & 链表的优点
数组的特点:
1、优点:查询块–在内存中一块连续的存储空间,使用数组下标可随机访问元素
2、缺点:插入和删除慢–插入和删除时会涉及到大量元素的移动。以及扩容比较复杂
链表的特点:
1、优点:插入删除快–链式结构找到元素删除之后,直接修改next 指向即可
2、缺点:查找慢–每次查找都要遍历链表
而 HashMap 底层实现则是采用了 数组、链表、树结构,如下图所示:
put 方法
1、是否需要初始化
需要则先进行 resize() 初始化,默认 16
2、获取数据下标
构造(k,v) node 节点之后,根据底层的 hashcode() 函数 对 k 进行计算获取到 hash 值,然后再对 hash 值进行计算(% 上数组长度),得到数组下标。
3、存储元素
1、数组下标位置没有元素的时候,那直接将该节点放入下标位置,如果数组元素数量达到扩容要求(数量 /长度 >= 加载因子) ,则resize进行扩容 2 倍
2、数组下标有元素
1、且存储的是链表结构,则遍历链表,若存在 equals(k) 为 true,则覆盖该元素,不存在则进行链表的尾部插入。如果链表的长度 >=8 时(为什么是8 ,数学上的波松分布问题),需要转换为树结构
3、且存储的是树结构,若树结构中存在 equals(k) 为 true,则覆盖该元素,不存在则进行树的插入操作。
get 方法:
1、获取数组下标
根据底层的 hashcode() 函数 对 k 进行计算获取到 hash 值,然后再对 hash 值进行计算(% 上数组长度),得到数组下标。
2、获取元素
1、数组下标没有元素,返回 null
2、数组下标有元素
1、且存储的是链表结构,则在链表中进行查找 equals(k) 为 true,存在则返回对用 v,否则返回 null
2、且存储的是树结构,则在树中进行查找 equals(k) 为 true,存在则返回对用 v,否则返回 null
HashMap 的 特点:
1、存取效率高
如上底层实现结构,数组 和 链表配合
增删时结合了链表的增删,避免了数组的移动元素。
查询时结合了数组的下标访问,部分遍历。
2、需要扩容
因为采用了数组结构,不可避免需要扩容,扩容是一件消耗很大的事情,重新开辟空间,对每一个元素重新计算 hash 存储。
3、线程不安全
为了提高效率。内部没有实现锁机制,hashTable 是线程安全的实现了 synchronized
4、为什么扩容至 2 倍
其实在判断下标时时采用 (n-1)& hash,其中 n 是数组长度。当数组长度为 2的x次幂时,刚好 n - 1 的二进制表示每一位都是1, 执行 & 运算时能更好的均匀分布。
6. 举例:当HashMap的容量是16时,它的二进制是10000,(n-1)的二进制是01111,与hash值得计算结果如下: 11111
自己的使用小 trips:
如果自己程序中 v 可能会存在 null,判断是否存在 k 的时候,不能使用 get(key) == null
应该使用 continesKey(key) == null
这段是抄过来的,哈哈哈哈哈哈哈哈哈:
12.HashMap和HashTable的区别
相同点:都是存储key-value键值对的
不同点:
- HashMap允许Key-value为null,hashTable不允许;
- hashMap没有考虑同步,是线程不安全的。hashTable是线程安全的,给api套上了一层synchronized修饰;
- HashMap继承于AbstractMap类,hashTable继承与Dictionary类。
- 迭代器(Iterator)。
- HashMap的迭代器(Iterator)是fail-fast迭代器,所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException
- 而Hashtable的enumerator迭代器不是fail-fast的。
- 容量的初始值和增加方式都不一样
- HashMap默认的容量大小是16;增加容量时,每次将容量变为"原始容量x2"。
- Hashtable默认的容量大小是11;增加容量时,每次将容量变为"原始容量x2 + 1";
- 添加key-value时的hash值算法不同
- HashMap添加元素时,是使用自定义的哈希算法。
- Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。
二、map 常见的几种遍历方法
- 迭代器
- ForEach 遍历
- lambda 表达式遍历
- StreamsApi 遍历
public static void test1() {
HashMap<Student, Integer> studentInfoMap = new HashMap<>();
studentInfoMap.put(new Student("小张", 10), 80);
studentInfoMap.put(new Student("小王", 9), 88);
studentInfoMap.put(new Student("小李", 11), 90);
log.info("for 循环遍历 map, keySet");
for (Student student : studentInfoMap.keySet()) {
log.info("key: {}, value:{}",student.toString(),studentInfoMap.get(student));
}
log.info("for 循环遍历 map, entrySet");
for (Map.Entry<Student, Integer> entry: studentInfoMap.entrySet()) {
log.info("key: {}, value:{}",entry.getKey(),entry.getValue());
}
log.info("迭代器遍历 map, keySet");
Iterator<Student> iteratorKeySet = studentInfoMap.keySet().iterator();
while(iteratorKeySet.hasNext()){
Student student = iteratorKeySet.next();
log.info("key: {}, value:{}",student.toString(),studentInfoMap.get(student));
}
log.info("迭代器遍历 map, entrySet");
Iterator<Map.Entry<Student, Integer>> iteratorEntrySet = studentInfoMap.entrySet().iterator();
while(iteratorEntrySet.hasNext()){
Map.Entry<Student, Integer> next = iteratorEntrySet.next();
log.info("key: {}, value:{}",next.getKey(),next.getValue());
}
log.info("使用 lamb 表达式遍历 map");
studentInfoMap.forEach((key,value) ->{
log.info("key: {}, value:{}",key,value);
});
log.info("使用 streamApi 遍历 map, entrySet");
studentInfoMap.entrySet().stream().forEach( studentIntegerEntry -> {
log.info("key: {}, value:{}",studentIntegerEntry.getKey(),studentIntegerEntry.getValue());
});
log.info("使用 streamApi 遍历 map, keySet");
studentInfoMap.keySet().stream().forEach( student -> {
log.info("key: {}, value:{}",student,studentInfoMap.get(student));
});
log.info("使用 streamApi 多线程遍历 map, entrySet");
studentInfoMap.entrySet().parallelStream().forEach( studentIntegerEntry -> {
log.info("key: {}, value:{}",studentIntegerEntry.getKey(),studentIntegerEntry.getValue());
});
}