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; } } }