面试手写HashMap,手撕HashMap

最困难的事情就是认识自己!

个人博客,欢迎访问!

前言:

           现在面试时,面试官经常会问到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

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值