HashMap原理解析
用了好久的HashMap呀,但是一直都是只会用而已,根本就不太懂里面是啥 怎么实现的... 最近终于稍微深入了解了一下 。
1.首先我们来讲一下 HashMap的基本使用:存储数据 与 获取数据
>>>创建map
HashMap<String,String> map = new HashMap<String,String>();
>>>存储数据
map.put("name","zhangsan");
map.put("age", "18");
map.put("sex", "male");
>>>获取数据方式1 通过keyset遍历
Iterator iterator = map.keySet().iterator();
while(iterator.hasNext()){
String key = (String) iterator.next();
String value = map.get(key);
System.out.println("KeySet遍历:key="+key+",value="+value);
}
>>>获取数据方式2 通过entryset遍历
Iterator entryIterator = map.entrySet().iterator();
while(entryIterator.hasNext()){
Entry entry = (Entry) entryIterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("EntrySet遍历:key="+key+",value="+value);
}
***遍历方式 keyset 与 entryset 的比较
keySet: 通过 next()方法获取到map的key,然后再通过 map.get(key) 获取对应的value值 (存到Set集合里面的只是map集合的 key)
entrySet: 通过 next()方法得到 Entry对象,通过entry.getKey(), entry.getValue() 分别就能获取到对应的值 (存到Set集合里面的是 Entry对象,包含集合的 key,value)
综合来说: entrySet 比 keySet遍历方式更高效些,建议使用 方式2 entrySet 遍历map集合。
HashMap使用代码示例: HashMapApp.java
package com.study.thread.juc_thread.base;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
public class HashMapApp {
public static void main(String[] args) {
//创建map
HashMap<String,String> map = new HashMap<String,String>();
//存储数据
map.put("name","zhangsan");
map.put("age", "18");
map.put("sex", "male");
//获取数据方式1 通过keyset遍历
Iterator iterator = map.keySet().iterator();
while(iterator.hasNext()){
String key = (String) iterator.next();
String value = map.get(key);
System.out.println("KeySet遍历:key="+key+",value="+value);
}
System.out.println("---------------遍历方式分割线------------------");
//获取数据方式2 通过entryset遍历
Iterator entryIterator = map.entrySet().iterator();
while(entryIterator.hasNext()){
Entry entry = (Entry) entryIterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("EntrySet遍历:key="+key+",value="+value);
}
}
}
2. 接下来我们来看 HashMap的初始化 ,也就是创建map对象
默认的容量值为: 16
默认的负载因子: 0.75
容量: 即map集合的初始化大小,之后会根据存储对象多少以及 负载因子的值来进行扩容
负载因子: 即当map集合存储的数据超过容量的 0.75时,可能会发生扩容操作
例如: 容量为 16 ,负载因子为 0.75 , 当存储数据量大于12,发生hash冲突,且每个map的数组中每个bucket下都已有值了,则会发生扩容 (大小变为原来集合的 2倍)
***推荐指定容量创建map,因为扩容会很消耗资源。 初始容量值 = (需要存储的对象 / 负载因子) +1
HashMap提供了四个初始化方法,如下
/**
1.指定初始化容量,负载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
/**
指定初始化容量值
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
无参的构造方法
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
指定map
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
3.然后我们来看一下HashMap如何存储数据,这里我们就需要了解HashMap的数据结构了(如何解决hash冲突)。
HashMap的数据结构为: 数组 + 链表 / 红黑树
HashMap存储数据主要是根据 key 的 hashcode 来找到对应的位置存储,这里就有可能会发生哈希冲突,HashMap通过使用链表来处理hash冲突。
假设 我们的HashMap初始容量为 6 ,负载因子为 0.75
存储数据
map.put("key1","value1");
map.put("key2","value2");
map.put("key3","value3");
map.put("key4","value4");
map.put("key5","value5");
map.put("key6","value6");
map.put("key7","value7");
map.put("key8","value8");
分别调用hashcode方法,计算出每个key的hash值,然后通过 hash值 % 容量 ,决定每个数据存储在数组中的位置

从上图中 我们可知, key5 ,key7 ,key8 发生了hash冲突,所以会在数组下标 4的位置处建立一个链表,存储这些冲突的数据
这里存储冲突数据根据JDK版本不同,加入的位置也不同,之前是在添加在链表的头结点的(添加后需要重新移动链表),新版本的JDK做了优化 将数据添加在链尾。
JDK8的时候,还使用了红黑树来存储冲突数据。 当冲突数据 > 8 时,链表会转换为红黑树; 当冲突数据 < 6时,红黑树又会转换为链表
4.最后我们来看一下HashMap如何获取数据。
HashMap获取数据,主要逻辑就是 map.get(key) 得到对应的value值;
>>map会先调用 key的 hashCode()方法,得到hash值,
>>根据hash值数组中找到对应的位置
>>如果存在冲突数据,则遍历链表,通过比较key值来获取对应value值,直至找到为止。

这里我们主要来讲一下为什么新建对象要复写equals方法时还要复写hashCode()方法
新建一个实体类 Person,里面有两个属性 姓名与年龄,我们先不复写它的equals() 以及hashcode方法 //这里为了测试方便,我就直接将Person类定义为内部类了
创建两个Person对象 per1 ,per2,设定这两个对象的姓名与年龄都相同。
分别使用 equals , == 判断这两个对象是否相等,// 我们都知道 == 比较的是对象的引用,在堆中这两个对象per1 per2肯定是不同的引用,所以 == 的结果会为false ; 实际应用中,equals我们应该是期望结果为true的~
创建一个map集合,将per1作为key存储进去,理论上per1应该是等于per2的,所以我们使用per2作为key去map集合里面取出对应的值应该是没有问题的,在执行下方代码之前先思考一下结果~
package com.study.thread.juc_thread.base;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.net.ssl.HandshakeCompletedEvent;
public class HashMapTest {
public static void main(String[] args){
Person per1 = new HashMapTest().new Person();
per1.setAge(18);
per1.setName("dfx");
Person per2 = new HashMapTest().new Person();
per2.setAge(18);
per2.setName("dfx");
System.out.println("per1 == per2 结果为:"+(per1 == per2));
System.out.println("per1.equals(per2)结果为:"+per1.equals(per2));
System.out.println("per1的hashcode:"+per1.hashCode());
System.out.println("per2的hashcode:"+per2.hashCode());
//创建map
HashMap map = new HashMap();
map.put(per1, "Hello World!");
System.out.println("map获取value值:"+map.get(per2));
}
class Person {
private String name;
private int age;
public Person(){}
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;
}
}
}
执行结果:

从结果中我们可以发现 per1 不等于 per2 , 且per1的hashcode与per2的hashcode不同,所以最后也没有办法使用per2作为key将map的value取出来。
接下来我们在内部类Person里添加如下两个方法 ,也就是复写Person对象的equals与hashcode方法,如果不复写就默认使用父类 Object的equals/hashcode方法。
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
再执行一遍上方代码,结果如下:

在复写equals方法后,我们new出来的Person对象,属性值相同,这两个对象equals为true
在复写hashCode方法后,per1 与 per2的hash值相等了,
根据我们今天讲的HashMap原理,存储的时候是通过 计算key的hash值,取数据的时候也是通过计算key的hashcode然后去找对应的数据
所以最后我们能使用map.get(per2)来获取 key为per1存储的值。现在理解了为什么要复写equals以及hashcode方法了吧 哈哈哈~
完整测试类如下:
package com.study.thread.juc_thread.base;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.net.ssl.HandshakeCompletedEvent;
public class HashMapTest {
public static void main(String[] args){
Person per1 = new HashMapTest().new Person();
per1.setAge(18);
per1.setName("dfx");
Person per2 = new HashMapTest().new Person();
per2.setAge(18);
per2.setName("dfx");
System.out.println("per1 == per2 结果为:"+(per1 == per2));
System.out.println("per1.equals(per2)结果为:"+per1.equals(per2));
System.out.println("per1的hashcode:"+per1.hashCode());
System.out.println("per2的hashcode:"+per2.hashCode());
//创建map
HashMap map = new HashMap();
map.put(per1, "Hello World!");
System.out.println("map.get(per2)获取value值:"+map.get(per2));
}
class Person {
private String name;
private int age;
public Person(){}
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 int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
}