最困难的事情就是认识自己!
个人博客,欢迎访问!
前言:
现在面试时,面试官经常会问到HashMap,简单点就会问下HashMap的一些关键知识点,困难些的可能会当场让你手写一个HashMap,考察下你对HashMap底层原理的了解深度;所以,今天特别手写了一个简单的HashMap,只实现了 put、get、containsKey、keySet 方法的 HashMap,来帮助我们理解HashMap的底层设计原理。
手撕HashMap:
首先定义接口:
import java.util.Set;
/**
*@Title: MyMap
* @Description: 自定义map接口
* @date: 2019年7月13日 下午3:56:57
*/
public interface MyMap<K,V> {
/**
* @Description: 插入键值对方法
* @param k
* @param v
* @return
*@date: 2019年7月13日 下午3:59:16
*/
public V put(K k,V v);
/**
* @Description:根据key获取value
* @param k
* @return
*@date: 2019年7月13日 下午3:59:40
*/
public V get(K k);
/**
* @Description: 判断key键是否存在
* @param k key键
* @return
*@date: 2019年7月23日 下午4:07:22
*/
public boolean containsKey(K k);
/**
* @Description: 获取map集合中所有的key,并放入set集合中
* @return
*@date: 2019年7月23日 下午4:24:19
*/
public Set<K> keySet();
//------------------------------内部接口 Entry(存放key-value)---------------------
/**
* @Title: Enter
* @Description: 定义内部接口 Entry,存放键值对的Entery接口
* @date: 2019年7月13日 下午4:00:33
*/
interface Entry<K,V>{
/**
* @Description: 获取key方法
* @return
*@date: 2019年7月13日 下午4:02:06
*/
public K getKey();
/**
* @Description:获取value方法
* @return
*@date: 2019年7月13日 下午4:02:10
*/
public V getValue();
}
}
接口实现类:
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.alibaba.fastjson.JSONObject;
/**
*@Title: MyHashMap
* @Description: MyMap接口的实现类
* @date: 2019年7月13日 下午4:04:56
*/
@SuppressWarnings(value={"unchecked","rawtypes","hiding"})
public class MyHashMap<K, V> implements MyMap<K, V>{
/**
* Entry数组的默认初始化长度为16;通过位移运算向左移动四位,得到二进制码 "00010000",转换为十进制是16
*/
private static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大容量 ,如果有参构造中,传入的初始容量一定要小于此值
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 负载因子默认为0.75f;负载因子是用来标志当使用容量占总容量的75%时,就需要扩充容量了,
* 扩充Entry数组的长度为原来的两倍,并且重新对所存储的key-value键值对进行散列。
*/
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 可设置的初始容量
*/
private int defaultInitSize;
/**
* 可设置的负载因子
*/
private float defaultLoadFactor;
/**
* 当前已存入的元素的数量
*/
private int entryUseSize;
/**
* 存放key-value键值对对象的数组
*/
private Entry<K, V>[] table = null;
/**
* @Description: 判断在有参构造中传入的初始容量是否为 2的整数次方,如果不是需要将其转换为2的整数次方
* 因为后面进行put时计算下标时,2的整数次方做&运算不容易产生下标冲突。
* @param cap
* @return
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/**
* 无参构造,数组初始大小为16,负载因子大小为0.75f
*/
public MyHashMap() {
this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
}
/**
* 有参构造,只带有初始容量参数
* @param defaultInitialCapacity 初始容量
*/
public MyHashMap(int defaultInitialCapacity) {
this(defaultInitialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 有参构造,自己设置数组初始大小和负载因子大小
* @param defaultInitialCapacity 数组初始大小
* @param defaultLoadFactor2 负载因子
*/
public MyHashMap(int defaultInitialCapacity, float defaultLoadFactor2) {
//判断初始容量参数是否合法
if (defaultInitialCapacity < 0) {
//抛出非法参数异常
throw new IllegalArgumentException("输入的初始容量参数是非法的 :"+defaultInitialCapacity);
}
// 如果初始容量参数大于设置的最大值的话,直接初始化为最大容量
if (defaultInitialCapacity > MAXIMUM_CAPACITY){
defaultInitialCapacity = MAXIMUM_CAPACITY;
}
//判断负载因子参数是否合法,Float.isNaN()方法是判断数据是否符合 0.0f/0.0f
if (defaultLoadFactor2 < 0 || Float.isNaN(defaultLoadFactor2)) {
throw new IllegalArgumentException("输入的负载因子参数是非法的 :"+defaultLoadFactor2);
}
this.defaultInitSize = tableSizeFor(defaultInitialCapacity);
this.defaultLoadFactor = defaultLoadFactor2;
//初始化数组
table = new Entry[this.defaultInitSize];
}
/**
* @Description: 集合中的put方法
* @param k
* @param v
* @return 如是更新则返回key的旧value值,如是插入新的key-value则返回null
*@date: 2019年7月13日 下午6:29:47
*/
@Override
public V put(K k, V v) {
V oldValue = null;
//是否需要扩容?
//扩容完毕后一定会需要重新进行散列
if (entryUseSize >= defaultInitSize * defaultLoadFactor) {
//*****扩容并重新散列,扩容为原来的两倍******
//为什么扩容为原来的两倍,也就是为什么长度要取2的n次方, 例如:2的三次方 == 2*2*2
//因为:可以减少hash冲突; 具体点就是:在求数组的index下标时,使用是hash码值、数组长度减1进行与运算,
//2的n次方减1转换为二进制后bit位都是1,然后与hash码值进行与运算后,使得到的值尽可能有差异,有差异就减少了hash冲突的发生。
resize(2 * defaultInitSize);
}
/**
* 根据key获取的HASH值、数组长度减1,两者做'与'运算,计算出数组中的位置
*
* 这里使用与运算代替取模运算,位运算效率更高,注意:只有当defaultInitSize为
* 2^n次方时,使用 a&(b-1)替代a%b 才是有效的
*/
int index = hash(k) & (defaultInitSize -1);
//如果数组中此下标位置没有元素的话,就直接放到此位置上
if (table[index] == null) {
table[index] = new Entry(k, v, null);
//总存入元素数量+1
++entryUseSize;
}
else {
//遍历数组下边的链表
Entry<K,V> entry = table[index];
Entry<K,V> e = entry;
while(e != null){
if (k == e.getKey() || k.equals(e.getKey())) {
oldValue = e.getValue();
//key已存在,直接更新value
e.value = v;
return oldValue;
}
//获取数组此下标位置上链表的下个元素
e = e.next;
}
//JDK1.7中的链表头插法,直接占据数组下标位置
table[index] = new Entry<K,V>(k, v, entry);
//总存入元素数量+1
++entryUseSize;
}
return oldValue;
}
/**
* @Description: 根据key获取value值
* @param k
* @return
*@date: 2019年7月13日 下午6:34:49
*/
@Override
public V get(K k) {
//通过hash函数和数组元素容量做 【与】运算得到数组下标
int index = hash(k) & (defaultInitSize -1);
if (table[index] == null) {
return null;
}
else {
//获取到数组下标位置元素
Entry<K, V> entry = table[index];
Entry<K, V> e = entry;
do {
if (k.equals(e.getKey())) {
return e.getValue();
}
//获取数组下标位置对应链表中的下一个元素
e = e.next;
} while (entry != null);
}
return null;
}
/**
* @Description:扩容并重新将元素进行散列
* @param i 扩容后的大小
*@date: 2019年7月13日 下午5:06:06
*/
public void resize(int size){
Entry<K,V>[] newTable = new Entry[size];
//改变数组的初始大小
defaultInitSize = size ;
//将已存放键值对数量置为0
entryUseSize = 0 ;
//将已存的元算根据最新的数组的大小进行散列
rehash(newTable);
}
/**
* @Description: 重新进行散列
* @param newTable
*@date: 2019年7月13日 下午5:10:07
*/
public void rehash(Entry<K, V>[] newTable){
List<Entry<K, V>> entryList = new ArrayList<>();
for(Entry<K, V> entry : table){
if (entry != null) {
do {
//将原来数组中的元素放到list集合中
entryList.add(entry);
//如果此数组下标的位置存在链表的话,需要遍历下列表,将列表中的键值对数据取出来放到集合中
entry = entry.next;
} while (entry != null);
}
}
//将旧的数组引用覆盖,让引用指向堆中新开辟的数组
if (newTable.length > 0) {
table = newTable;
}
//所谓重新的散列hash,就是将元素重新放入到扩容后的集合中
for(Entry<K, V> entry : entryList){
//重新put
put(entry.getKey(), entry.getValue());
}
}
/**
* @Description: 根据key获取hashcod码值
* @param key
* @return
*@date: 2019年7月13日 下午5:52:22
*/
public int hash(K key){
int h;
// 使用key的hash值的高16位与其低十六位做异或操作,得到的hash值,可以降低hash碰撞
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* @Description: 判断是否存在此key
* @param k key键
* @return
*@date: 2019年7月23日 下午4:52:22
*/
public boolean containsKey(K k) {
boolean flag = false;
int index = hash(k) & (defaultInitSize -1);
if (table[index] == null) {
return flag;
}
else {
//获取到数组下标位置元素
Entry<K, V> entry = table[index];
Entry<K, V> e = entry;
do {
if (k.equals(e.getKey())) {
flag = true;
return flag;
}
//获取数组下标位置对应链表中的下一个元素
e = e.next;
} while (e != null);
}
return flag;
}
/**
* @Description: 获取map集合所有的key
* @return
*@date: 2019年7月23日 下午5:52:22
*/
@Override
public Set<K> keySet() {
if (entryUseSize == 0) {
return null;
}
Set<K> keySet = new HashSet<K>();
for(Entry<K, V> entry : table){
if (entry != null) {
do {
//将原来数组中的元素的key放到set集合中
keySet.add(entry.getKey());
//如果此数组下标的位置存在链表的话,需要遍历下列表,将列表中元素的key取出来放到集合中
entry = entry.next;
} while (entry != null);
}
}
return keySet;
}
/**
* 获取map集合所有的values
*/
@Override
public Set<V> valueSet() {
if (entryUseSize == 0) {
return null;
}
Set<V> valueSet = new HashSet<>();
for (Entry<K, V> entry : table) {
if (entry != null) {
do{
// 将获取到的value放入到set集合中
valueSet.add(entry.getValue());
// 遍历下标位置对应的单链表
entry = entry.next;
}while(entry != null);
}
}
return valueSet;
}
/**
* 获取map集合所有的Entry 键值对 对象
*/
public Set<MyMap.Entry<K, V>> entrySet(){
// 判断map集合是否为空
if (entryUseSize == 0) {
return null;
}
Set<MyMap.Entry<K, V>> entrySet = new HashSet<>();
for (Entry<K, V> entry : table) {
if (entry != null) {
do{
// 将获取到的Entry<K, V>放入到set集合中
entrySet.add(entry);
// 遍历下标位置对应的单链表
entry = entry.next;
}while(entry != null);
}
}
return entrySet;
}
//----------------------------------------内部类 Entry(存放key-value)---------------------------
/**
* @Title: Entry
* @Description: 实现了key-value简直对接口的java类
* @date: 2019年7月13日 下午6:12:16
*/
class Entry<K, V> implements MyMap.Entry<K, V>{
/**
* 键值对对象的key
*/
private K key;
/**
* 键值对对象的value
*/
private volatile V value;
/**
* 键值对对象指向下一个键值对对象的指针
*/
private Entry<K, V> next;
/**
* 无参构造
*/
public Entry() {
}
/**
* 有参构造
* @param key
* @param value
* @param next
*/
public Entry(K key, V value, Entry<K, V> next) {
super();
this.key = key;
this.value = value;
this.next = next;
}
/**
* 获取key
*/
@Override
public K getKey() {
return key;
}
/**
* 获取value
*/
@Override
public V getValue() {
return value;
}
/**
* toString 方法
*/
@Override
public String toString() {
return JSONObject.toJSONString(this);
}
}
}
测试方法:
import org.junit.Test;
/**
* @Title: TestMyMap
* @Description:
* @date: 2019年7月13日 下午6:49:25
*/
public class TestMyMap {
/**
* @Description:单元测试
*
*@date: 2019年7月23日 下午7:07:22
*/
@Test
public void test() {
MyMap<String, String> map = new MyHashMap<>();
for (int i = 0; i < 100; i++) {
//插入键值对
map.put("key" + i, "value" + i);
}
for (int i = 0; i < 100; i++) {
System.out.println("key" + i + ",value is:" + map.get("key" + i));
}
//根据key获取value
System.out.println("\n"+"此key:key88 的value是 "+map.get("key88"));
//判断key是否存在
System.out.println(map.containsKey("key885")+" 此key:key885 不存在!");
//获取map集合中所有的key
System.out.println(Arrays.toString(map.keySet().toArray()));
MyMap<String, String> mapOther = new MyHashMap<>();
Set<String> keySet = mapOther.keySet();
//获取map集合中所有的key
System.out.println((keySet == null)?null:Arrays.toString(mapOther.keySet().toArray()));
}
}
❤不要忘记留下你学习的足迹 [点赞 + 收藏 + 评论]嘿嘿ヾ
一切看文章不点赞都是“耍流氓”,嘿嘿ヾ(◍°∇°◍)ノ゙!开个玩笑,动一动你的小手,点赞就完事了,你每个人出一份力量(点赞 + 评论)就会让更多的学习者加入进来!非常感谢! ̄ω ̄=
个人原创Java技术文公众号,欢迎大家关注;关注后如果 不香 ,来捶我啊!嘿嘿。。。。。。
参考资料:手写实现一个HashMap