前几天字节二面,面试官要求自己手写一个HashMap,当时就有些被惊到了,虽然读过HashMap的源码,也能侃侃而谈,但是说的和做的难度还是不一样的,当时吹了一会HashMap之后,面试官淡淡一笑,说:“好的,那写一下吧,,,,”。看来还是逃不过啊,最后写的不太好,所以说,大家以后还是要动起手来啊!
首先HashMap的底层就是一个数组,数组里面保存了Key和Value,以及一个next指针,如果没有哈希冲突的话,next指针就是null,如果有哈希冲突,那么就会形成链表,指向下一个元素。这里我们首先要了解,新版本的HashMap底层已经不只是数组+链表了,而是数组+链表+红黑树,并且已经由原来的“头插法”,改成了“尾插法”。这里我们暂时不讨论新版本的实现,而是使用旧版本的HashMap实现,也就是数组+链表+“头插法”。
下面首先给出代码:
首先创建一个接口,就是定义一个规范,接口里面主要定义了put和get方法。以及Entry类型的getKey和getValue方法(Entry类型实际上就是数组类型)。
package Hash;
//接口,相当于规范,里面定义了map和entry
//entry里面完成getKey和getValue,map里面完成put和get
public interface MyMap<K,V>{
public V put(K key,V value);
public V get(K key);
interface Entry<K,V>{
public K getKey();
public V getValue();
}
}
然后,就是去实现这个接口。(当然,我们也可以不用这样实现接口的方法,就直接写一个类来完成)
package Hash;
import java.util.ArrayList;
import java.util.List;
//手撕hashmap
public class MyHashMap <K,V> implements MyMap<K,V>{
//内部的属性
private static final int DEFAULT_INITIAL_CAPACITY=1<<4;//默认大小
private static final float DEFAULT_LOAD_FACTOR=0.75f;
private int capacity;
private float load_factor=0.75f;
private int entryUseSize;//已经使用的entry的数量
private Entry<K,V>[] table =null;//entry类型的数组
//构造函数
public MyHashMap(){ this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);}
public MyHashMap(int initial_capacity,float load_factor){
if(initial_capacity<0)
throw new IllegalArgumentException("!!!");
if(load_factor<=0||Float.isNaN(load_factor))
throw new IllegalArgumentException("!!");
this.capacity=initial_capacity;
this.load_factor=load_factor;
table=new Entry[this.capacity];
}
class Entry<K, V> implements MyMap.Entry<K, V> {
private K key;
private V value;
private Entry<K, V> next;
Entry(K k, V v, Entry<K, V> next) {
this.key = k;
this.value = v;
this.next = next;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
@Override
public V put(K key,V value){
V oldNum=null;//需要返回的旧的数
if(this.entryUseSize>=this.capacity*this.load_factor)//需要扩容
resize(2*this.capacity);//扩容以及rehash
int index=hash(key)&(this.capacity-1);//利用hash函数再散列一次
//如果是空直接放进去
if(table[index]==null)
{
table[index]=new Entry<>(key,value,null);
++this.entryUseSize;
}
else{//需要遍历链表
Entry<K,V> entry=table[index];
Entry<K,V> e=entry;
while(e!=null)
{
if(e.getKey()==key||key.equals(e.getKey()))
{
oldNum=e.value;
e.value=value;
return oldNum;
}
e=e.next;
}
//如果没有,使用头插法
table[index]=new Entry<K,V> (key,value,entry);
++this.entryUseSize;
}
return oldNum;
}
@Override
public V get(K key) {
int index=hash(key)&(this.capacity-1);
if(table[index]==null)
return null;
else{
Entry<K,V> entry=table[index];
do{
if(key==entry.getKey()||key.equals(entry.getKey()))
return entry.getValue();
entry=entry.next;
}while(entry!=null);
}
return null;
}
//根据hashcode来计算散列值
private int hash(K key){
int h=0;
return (key==null)?0:(h=key.hashCode())^(h>>16);
}
//重新划分数组大小,然后把原来的里面的元素都放入新数组,参数newSize表示新的容量
private void resize(int newSize){
Entry<K,V>[] newTable=new Entry[newSize];
this.capacity=newSize;
this.entryUseSize=0;
rehash(newTable);
}
//把原来的数全放到新的输入数组里面
private void rehash(Entry<K,V>[] newTable){
List<Entry<K,V>> list=new ArrayList<Entry<K,V>>();
for(Entry<K,V> entry:this.table)//遍历数组里面的每个元素
{
if(entry!=null){
do{//遍历链表里面的每个元素
list.add(entry);
entry=entry.next;
}while(entry!=null);
}
}
if(newTable.length>0){
this.table=newTable;//指向新的数组
}
for(Entry<K,V> entry:list){
//调用put函数,把刚才放入list里面的数据放到新的table里
put(entry.getKey(),entry.getValue());
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
MyHashMap<String,String> hash=new MyHashMap<String,String>();
for(int i=0;i<100;i++)
{
hash.put("nihao"+i, "buhao"+i);
}
for(int i=0;i<100;i++)
{
System.out.println(hash.get("nihao"+i));
}
}
}
最后的输出:
这个里面当然最主要的就是put和get方法,注释都帮大家准备好了,简单概括一下吧:
1、put函数,首先判断需不需要扩容,判断的标准就是,当前的容量*负载因子是不是大于当前已经使用的容量,如果大于了,就要执行resize方法,这个方法会创建一个新的数组,新数组的容量是原来的2倍大小。然后把原来链表里面的元素放到一个容器里面,最后把容器里面的数倒出来,在put到新的数组里面。
如果不需要扩容,那么会利用hash方法,进行一次再散列,关于再散列的原因,以及理论这里不细讲了,想知道的话可以看看我的另外一篇博客:hash方法。然后判断当前的Entry数组元素之前有没有被占据,没有直接把这个“坑”占掉,否则,进行链表的遍历,找到了直接修改,没找到,就把元素放在原来头结点的前面(头插法),最后返回旧元素的数值。
2、get方法:依旧是首先利用hash方法对hashcode的值进行二次哈希,然后计算出数组的下标,后面的其实和put差不多,就不细讲了。