Java 中HashMap 详解
本篇重点:
1.HashMap的存储结构
2.HashMap的put和get操作过程
3.HashMap的扩容
4.关于transient关键字

HashMap的存储结构
1. HashMap 总体是数组+链表的存储结构, 从JDK1.8开始,当数组的长度大于64,且链表的长度大于8的时候,会把链表转为红黑树。
2. 数组的默认长度是16。数组中的每一个元素为一个node,也就是链表的一个节点,node的数据包含: key的hashcode, key, value,指向下一个node节点的指针。
部分源码如下:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
...
}
3. 随着put操作的进行,如果数组的长度超过64,且链表的长度大于8的时候, 则将链表转为红黑树,红黑树节点的结构如下,TreeNode继承的LinkedHashMap.Entry是继承HashMap.Node的,所以TreeNode是上面Node的子类。
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
//...
}
4. HashMap类的主要成员变量:

/* ---------------- Fields -------------- */
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
HashMap的put操作过程
本小节讲述put操作中的主要步骤,细小环节会忽略。
1. map.put(key, value),首先计算key的hash,得到一个int值。
2.如果Node数组为空则初始化Node数组。这里注意,Node数组的长度length始终应该是2的n次方,比如默认的16, 还有32,64等
3.用 hash&(length-1) 运算得到数组下标,这里要提一句,其实正常我们最容易想到的,而且也是我之前很长一段时间以为的,这一步应该进行的是求模运算:hash % length,这样得到的正好是0~length-1之间的值,可以作为数组的下标,那么为何此处是位与运算呢?
先说结论:上面提到数组的长度length始终是2^n,在这个前提下,hash & (length-1) 与hash % length是等价的。 而位与运算更快。这里后面会另开一遍进行详解。
4. 如果Node[hash&(length-1)]处为空,用传入的的key, value创建Node对象,直接放入该下标;如果该下标处不为空,且对象为TreeNode类型,证明此下标处的元素们是按照红黑树的结构存储的,将传入的key,value作为新的红黑树的节点插入到红黑树;否则,此处为链表,用next找到链表的末尾,将新的元素插入。如果在遍历链表的过程中发现链表的长度超过了8,此时如果数组长度<64则进行扩容,否则转红黑树。
5. 如果key的hash和key本身都相等则将该key对应的value更新为新的value
6. 需要扩容的话则进行扩容。
注意:
1. 如果key是null则返回的hash为0,也就是key为null的元素一直被放在数组下标为0的位置。
2. 在JDK 1.8以前,链表是采用的头部插入的方式,从1.8改成了在链表尾部插入新元素的方式。 这么做是为了防止在扩容的时候,多线程时出现循环链表死循环。具体会新开一遍进行详细演绎。
HashMap的get操作过程
get的过程比较简单。
1. map.get(key). 首先计算key的hash。
2. 根据hash&(length-1)定位到Node数组中的一个下标。如果该下标的元素(也就是链表/红黑树的第一个元素)中key的hash的key本身都和传入的key相同,则证明找到了元素,直接返回即可。
3.如果第一个元素不是要找的,如果第一个元素的类型是TreeNode,则按照红黑树的查找方法查找元素,如果不是则证明是链表,按照next指针找下去,直到找到或者到达队尾。
HashMap的扩容
先说这里的两个概念: size, length.
size:是map.size() 方法返回的值,表示的是map中有多少个key-value键值对儿
length: 这里是指Node数组的长度,比如默认长度是16.
如下面的代码:
Map<Integer,String> map = new HashMap<>();
map.put(1,"a");
map.put(2,"b");
map.put(3,"c");
没有在构造函数中指定HashMap的大小,则数组的长度length取默认的16,put了3个元素,则size为3.
Q: 何时需要扩容呢?
A: 在put方法中,每次完成了put操作,都判断一下++size是否大于threshold,如果大于则进行扩容: 调用resize()方法。
Q: 那么threshold又是如何得到的呢?
A: 简单来讲threshold = length * loadfactor(默认为0.75)。 也就是说默认情况下,map中的键值对的个数(size)大于Node数组长度(length)的75%时,就需要扩容了。
Q: 扩容时具体做什么呢?
A: 首先计算出新的数组长度和新的threshold(阈值). 简单来讲,新的length/capacity 是原来的2倍(位运算左移一位),新的threshold为原来的2倍。 还有一些细节此处不再赘述。创建新的Node数组,将原来数组中的元素重新映射到新的数组中。
关于transient关键字
transient关键字的作用:用transient关键字修饰的字段不会被序列化
查看下面的例子:

public class TransientExample implements Serializable{
private String firstName;
private transient String middleName;
private String lastName;
public TransientExample(String firstName,String middleName,String lastName) {
this.firstName = firstName;
this.middleName = middleName;
this.lastName = lastName;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("firstName:").append(firstName).append("\n")
.append("middleName:").append(middleName).append("\n")
.append("lastName:").append(lastName);
return sb.toString();
}
public static void main(String[] args) throws Exception {
TransientExample e = new TransientExample("Adeline","test","Pan");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/path/testObj"));
oos.writeObject(e);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/path/testObj"));
TransientExample e1 = (TransientExample) ois.readObject();
System.out.println("e:"+e.toString());
System.out.println("e1:"+e1.toString());
}
}
输出结果:
e:firstName:Adeline
middleName:test
lastName:Pan
e1:firstName:Adeline
middleName:null
lastName:Pan
被transient关键字修饰的middleName字段没有被序列化,反序列化回来的值是null
Q:HashMap类是实现了Serializable接口的,那么为何其中的table, entrySet变量都标为transient呢?
A:我们知道,table数组中元素分布的下标位置是根据元素中key的hash进行散列运算得到的,而hash运算是native的,不同平台得到的结果可能是不相同的。举一个简单的例子,假设我们在目前的平台有键值对 key1-value1,计算出key1的hash为1, 计算后存在table数组中下标为1的地方,假设table被序列化了,并传输到了另外的平台,并反序列化为了原来的HashMap,key1-value1仍然存在下标1的位置,当在这个平台运行get("key1")的时候,可能计算出key1的hash为2,就有可能到下标为2的地方去找该元素,这样就出错了。
Q:那么HashMap是如何实现的序列化呢?
A:HashMap是通过实现如下方法直接将元素数量(size), key, value等写入到了ObjectOutputStream中,实现的定制化的序列化和反序列化。在Serializable接口中有关于这种做法的说明。
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;

Java 中HashMap 详解的更多相关文章
- 【转】 java中HashMap详解
原文网址:http://blog.csdn.net/caihaijiang/article/details/6280251 java中HashMap详解 HashMap 和 HashSet 是 Jav ...
- java中HashMap详解(转)
java中HashMap详解 博客分类: JavaSE Java算法JDK编程生活 HashMap 和 HashSet 是 Java Collection Framework 的两个重要成 ...
- java集合(2)- java中HashMap详解
java中HashMap详解 基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了非同步和允许使用 null 之外,HashMap 类与 H ...
- 《转》Java中HashMap详解
HashMap 和 HashSet 是 Java Collection Framework 的两个重要成员,其中 HashMap 是 Map 接口的常用实现类,HashSet 是 Set 接口的常用实 ...
- java中HashMap详解
HashMap 和 HashSet 是 Java Collection Framework 的两个重要成员,其中 HashMap 是 Map 接口的常用实现类,HashSet 是 Set 接口的常用实 ...
- java中多线程详解-synchronized
一.介绍 当多个线程涉及到共享数据的时候,就会设计到线程安全的问题.非线程安全其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”.发生脏读,就是取到的数据已经被其他的线 ...
- JAVA 中 synchronized 详解
看到一篇关于JAVA中synchronized的用法的详解,觉得不错遂转载之..... 原文地址: http://www.cnblogs.com/GnagWang/archive/2011/02/27 ...
- Java中List详解
List是Java中比较常用的集合类,关于List接口有很多实现类,本文就来简单介绍下其中几个重点的实现ArrayList.LinkedList和Vector之间的关系和区别. List List 是 ...
- Java中PriorityQueue详解
Java中PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示.本文从Queue接口函数出发,结合生动的图解,深入浅出地分析PriorityQueue每个操作的具体过程和时间复杂度, ...
随机推荐
- jieba分词原理解析:用户词典如何优先于系统词典
目标 查看jieba分词组件源码,分析源码各个模块的功能,找到分词模块,实现能自定义分词字典,且优先级大于系统自带的字典等级,以医疗词语邻域词语为例. jieba分词地址:github地址:https ...
- Pytorch从0开始实现YOLO V3指南 part5——设计输入和输出的流程
本节翻译自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch ...
- Python 数据科学手册:读书笔记概论
为防止遗忘,在空闲时间将读书的笔记开始按照章节进行概括总结(2022.1.1): 第二章:NumPy 入门 第三章:Pandas 数据处理 第四章:Matplotlib 数据可视化 第五章:机器学习 ...
- ReentrantLock 公平锁源码 第1篇
ReentrantLock 1 这篇还是接着ReentrantLock的公平锁,没看过第0篇的可以先去看上一篇https://www.cnblogs.com/sunankang/p/16456342. ...
- 小米社区APP深度体验
小米社区APP深度体验 版本:3.0.210928 BUG 1,在暗黑模式下,会员一栏中的成就等级小字未作深色模式调整,从而造成文字难于识别. 2,在暗黑模式下,会员页中底部的会员产品首页视觉没有完美 ...
- Static、Final关键字详解
1.Static 详情见下面代码讲解 点击查看代码 package com.Tang.oop.demo07; public class Student { private static int age ...
- 记一次requests请求乱码的问题
太懒了,直接说原因吧: 请求返回的内容含有emoji表情 我的解决办法是替换掉emoji字符: 安装库:pip install emoji 替换:emoji.demojize(CONTENT) 注意, ...
- MySQL--数据过滤(AND、OR、IN、NOT操作符)
MySQL允许给出多个WHERE子句.这些子句可以两种方式使用:以AND子句的方式或OR子句的方式使用. 1.组合WHERE子句 1.1 AND操作符 SELECT prod_id,prod_pric ...
- can板间通信代码学习
一.板间通信 板间通信是底盘与上下云台之间的通信 A型板和两个C型板 主要可以分成两个方面,一是哨兵的模式选择和遥控器数据的解码:二是对于CAN发送和接收数据的处理. 二.CAN的板间通信相关函数 c ...
- 2499-springboot使用jar形式打包在linux上运行
由于maven使用的种种问题,以前springboot版本变化较快带来的一些不兼容问题,是否考虑下使用jar形式运行web程序,而不是固守于war包与tomcat: 主要原理有两点: 使用nohup来 ...