1.1简介
- 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
- 所有的key构成的集合是Set:无序的、不可重复的。所以, key所在的类要重写:equals()和hashCode()
- 所有的value构成的集合是Collection:无序的、可以重复的。所以, value所在的类要重写: equals()
- 一个key-value构成一个entry
- 所有的entry构成的集合是Set:无序的、不可重复的
- HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
- HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
1.2 HashMap存储结构
1.2.1JDK1.7前存储结构
JDK 7及以前版本: HashMap是数组+链表结构(即为链地址法)
- HashMap的内部存储结构其实是数组和链表的结合。 当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组, 这个长度在哈希表中被称为容量(Capacity), 在这个数组中可以存放元素的位置我们称之为“桶” (bucket), 每个bucket都有自己的索引, 系统可以根据索引快速的查找bucket中的元素。
- 每个bucket中存储一个元素, 即一个Entry对象, 但每一个Entry对象可以带一个引用变量, 用于指向下一个元素, 因此, 在一个桶中, 就有可能生成一个Entry链。而且新添加的元素作为链表的head。
- 添加元素的过程:向HashMap中添加entry1(key, value), 需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到), 此哈希值经过处理以后, 得到在底层Entry[]数组中要存储的位置i。 如果位置i上没有元素, 则entry1直接添加成功。 如果位置i上已经存在entry2(或还有链表存在的entry3, entry4), 则需要通过循环的方法, 依次比较entry1中key和其他的entry。 如果彼此hash值不同, 则直接添加成功。 如果hash值相同, 继续比较二者是否equals。 如果返回值为true, 则使用entry1的value去替换equals为true的entry的value。 如果遍历一遍以后, 发现所有的equals返回都为false,则entry1仍可添加成功。 entry1指向原有的entry元素。
- HashMap的扩容: 当HashMap中的元素越来越多的时候, hash冲突的几率也就越来越高, 因为数组的长度是固定的。 所以为了提高查询的效率, 就要对HashMap的数组进行扩容, 而在HashMap数组扩容之后, 最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置, 并放进去, 这就是resize。
- 那么HashMap什么时候进行扩容呢? 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值(DEFAULT_LOAD_FACTOR)为0.75, 这是一个折中的取值。 也就是说, 默认情况下, 数组大小(DEFAULT_INITIAL_CAPACITY)为16, 那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值, 也叫做临界值) 的时候, 就把数组的大小扩展为 2*16=32, 即扩大一倍, 然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作, 所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
1.2.2 JDK 8存储结构
JDK 8版本发布以后: HashMap是数组+链表+红黑树实现。
- HashMap的内部存储结构其实是数组+链表+树的结合。 当实例化一个HashMap时, 会初始化initialCapacity和loadFactor, 在put第一对映射关系时, 系统会创建一个长度为initialCapacity的Node数组, 这个长度在哈希表中被称为容量(Capacity), 在这个数组中可以存放元素的位置我们称之为“桶” (bucket), 每个bucket都有自己的索引, 系统可以根据索引快速的查找bucket中的元素。
- 每个bucket中存储一个元素, 即一个Node对象, 但每一个Node对象可以带一个引用变量next, 用于指向下一个元素, 因此, 在一个桶中, 就有可能生成一个Node链。 也可能是一个一个TreeNode对象, 每一个TreeNode对象可以有两个叶子结点left和right, 因此, 在一个桶中, 就有可能生成一个TreeNode树。 而新添加的元素作为链表的last, 或树的叶子结点。
- HashMap扩容: 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值(DEFAULT_LOAD_FACTOR)为0.75, 这是一个折中的取值。 也就是说, 默认情况下, 数组大小(DEFAULT_INITIAL_CAPACITY)为16, 那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值, 也叫做临界值)的时候, 就把数组的大小扩展为 2*16=32, 即扩大一倍, 然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作, 所以如果我们已经预知HashMap中元素的个数, 那么预设元素的个数能够有效的提高HashMap的性能。
- HashMap树化和链化: 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
- 扩容和树化举个例子:
-
- 初始情况下,hashmap 的capacity为16,因子为0.75。
- 当hashmap桶内元素小于等于8,且size小于12时,不进行扩容和树化的操作。
- 当hashmap桶内元素为9,因为capacity为16,因此不进行树化,而选择扩容,将capacity扩容为32。
- 当hashmap桶内元素为10,因为capacity为32,因此不进行树化,而选择扩容,将capacity扩容为64。
- 当hashmap桶内元素大于10,由于capacity已经达到64,此时进行树化。
- 最后当HashMap中元素个数超过48(64*0.75=48),进行扩容
- JDK1.8HashMap新变化
-
- HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
- 当首次调用map.put()时,再创建长度为16的数组
- 数组为Node类型,在jdk7中称为Entry类型
- 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
- 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。
1.3HashMap源码中的重要常量
- DEFAULT_INITIAL_CAPACITY : 默认16,HashMap的默认容量
- MAXIMUM_CAPACITY :默认2^30, HashMap的最大支持容量
- DEFAULT_LOAD_FACTOR:默认0.75, HashMap的默认加载因子
- TREEIFY_THRESHOLD:默认8 Bucket中链表长度大于该默认值,转化为红黑树
- UNTREEIFY_THRESHOLD: 默认6,Bucket中红黑树存储的Node小于该默认值,转化为链表
- MIN_TREEIFY_CAPACITY: 默认64,桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
- table: 存储元素的数组,总是2的n次幂
- entrySet: 存储具体元素的集合
- size: HashMap中存储的键值对的数量
- modCount: HashMap扩容和结构改变的次数。
- threshold: 扩容的临界值, =容量*填充因子
- loadFactor: 填充因子
1.3.1HashMap源码的重要方法
- resize:扩容
- treeifyBin:树化
- untreeify:链化
1.扩容
- 默认HashMap产生,capacity默认容量0,第一次使用时,扩容成16
- 当元素个数超过12,会扩容resize
- 每次扩容一倍
2.树化
- TREEIFY_THRESHOLD:树化的阈值,默认8
- UNTREEIFY_THRESHOLD: 链化的阈值,默认6
- MIN_TREEIFY_CAPACITY: 最小树化的容量,默认64,如果元素个数没有超过这个阈值(64),即使链超过8,会先进行扩容,超过64才进行树化
- 比如下面所有元素的hashCode相同
-
- 第一个元素,扩容到16
- 第二个元素,形成链,链里面2个元素
- ...一直到链中有8个元素
- 第9个元素,执行resize扩容到32
- 第10个元素,执行resize扩容到64
- 第11个元素,执行treeifyBin()方法树化
-
-
- Node(next) 链
- TreeNode(left,right)红黑树
-
3.链化
- 链的数量小于6,执行untreeify方法变成链
public class User {
private String name;
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
// @Override
// public boolean equals(Object o) {
// System.out.println("User equals()....");
return super.equals(o);
// if (this == o) return true; //内存地址相同返回true
// if (o == null || getClass() != o.getClass()) return false;
//
// User user = (User) o;
//
// if (age != user.age) return false;
// return name != null ? name.equals(user.name) : user.name == name;
// }
/**
* * 注:**为什么hashCode中,用31这个数子?
* *
* * - 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
* * - 并且31只占用5bits,相乘造成数据溢出的概率较小。
* * - 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。 (提高算法效率)
* * - 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除! (减少冲突)
* @return
*/
@Override
public int hashCode() { //return name.hashCode() + age;
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
}
测试:
public class TreeifyDemo1 {
public static void main(String[] args) {
Map map = new HashMap();
User u1 = new User("马云", 45);
User u2 = new User("马云", 45);
User u3 = new User("马云", 45);
User u4 = new User("马云", 45);
User u5 = new User("马云", 45);
User u6 = new User("马云", 45);
User u7 = new User("马云", 45);
User u8 = new User("马云", 45);
User u9 = new User("马云", 45);
User u10 = new User("马云", 45);
User u11 = new User("马云", 45);
User u12 = new User("马云", 45);
map.put(u1, "a");
map.put(u2, "a");
map.put(u3, "a");
map.put(u4, "a");
map.put(u5, "a");
map.put(u6, "a");
map.put(u7, "a");
map.put(u8, "a");
map.put(u9, "a");
map.put(u10, "a");
map.put(u11, "a");
map.put(u12, "a");
map.remove(u12);
map.remove(u11);
map.remove(u10);
map.remove(u9);
map.remove(u8);
map.remove(u7);
map.remove(u6);
map.remove(u5);
map.remove(u4);
map.remove(u3);
map.remove(u2);
map.remove(u1);
System.out.println(map);
}
}
1.3.2哈希取模算法
- 调用对象的hashCode(),确定数组中的槽位,确定数组中的槽位,采用的哈希取模算法
- put(key) -> (n-1) & hash
- 要求容量必须是2的n次方
- 面试题:HashMap的容量,为什么必须是2的n次幂
HashMap使用n-1 & hash得到槽位地址,这个运算n必须是2的n次幂,结论当容量是2的n次幂的时候(16,32...)
hash % n = (n-1) & hash,因为位运算性能高
如果初始容量不是2的n次幂,HashMap调用tableSizeFor自动转换成大于这个数最小的2的n次幂