手写java HashMap部分核心功能
嗯,先介绍一下HashMap的运用场景和其内部使用的结构 废话不多说,我们开始
HashMap数据结构是 数组、链表、红黑树(此章代码并没涉及)
大学中科班的同学应该知道上述这三类,是属于一门叫做数据结构和算法中涉及到的内容
数组:一段地址连续的存储单元
上述这这一张图片也是比较经典的数组存储的状态,从上上面的图片可以看出
数组的优点:查询速度很快(下标知道,使用数学表达式即可找到目标)
缺点:添加/删除数据的速度很慢(涉及到数据位移的情况,若是中间某一个数据进行插入,后面所有数据下标都需要+1,以此来满足
对于元素的添加操作)
链表:存储一个数据和下一个数据的存储地址
从上述图片的特点我们可以看出部分链表的特点和优点
链表的优点:随机存储和删除数据效率很高(不存在考虑数组当中的下标和移动的问题)
缺点:查询效率较低(链表当中不存在下标的缘故,最优的情况是在第一个节点就找了我们需要的节点O1,最差的情况就是最后一个数据中
找到我们需要的数据On)
注:此处的O1、On指的是数据结构当中的时间复杂度
下面我们进行今天的重要内容HashMap的部分功能实现
1、HashMap的使用
HashMap<Integer,String> hashmap = new HashMap<>();
hashmap.put(1,"zhangsan");
//put方法代表向HashMap中添加数据
//添加我们指定的数据Integer、String类型
//这也可以说是向HashMap集合当中添加key,value值
hashmap.get(1);
//而get功能呢 顾名思义就是根据key值来获取与其对应的value值
2、关于添加的数据是添加在那段内存当中?(添加)
首先我们需要知道在jdk包中的Object类中有一个叫做hashCode的这么一个类(返回一个hashCode值)
int hash = "name".hashCode();//第一步得到其hash值
int index = hash%7;//第二步做取余的运算 而7相当于是存储的长度
//假设最后的index的值为2,那么就要把数据存储到这个索引为2的位置上
3、查找数据
int hashCode = "name".hashCode();//获取name的hashCode值
int index = hash%7;//取余数
String value = data[2];//取出第二个位置上的value值取出来
可能发生的问题:
1、哈希冲突:这个名字具体指的是,最后得到的两个数据的index的值相同的,映射到了同一个位置上
答:数组中存放的是一个节点的数据结构,节点拥有next属性,若hash冲突了,单链表进行存放(挂载到同一个位置上,利用链表的特点),取的方式也是同理,遍历链表查询
2、若数组存储满了应该如何操作:
答:和ArrayList(动态数组:可以根据存储的大小进行自动扩容)进行扩容,重新映射
3、由于直接使用hashCode值的话,hash冲突的概率会很大
答:在JDK包中HashMap上有一个hash的方法,再在上面对其hash值进行修改运算方式(使用一个不容易重复的运算方式即可)
后面有一个或者两个目标的为挂载的对象(在hash冲突之后,key值相同,value值不同)
手写HashMap原码
上述内容简单的描述了HashMap的一些情况,接下来就用最少的代码对其进行实现
下面先创建一个关于Entry的类
public static class Entry<K,V>{
int hash;//key对应哈希值
K key;//存放的key
V value;//存放的value
Entry<K,V> next;
/*
在hash值相同的时候,会出现同一个位置上面已近存在元素了
故,后进来的新元素作为指向之前存放的元素,作为表头
总结:添加新元素时,添加在表头
*/
public Entry(int hash, K key, V value, Entry<K, V> next) {//构造方法
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
上面定义了Entry类的基本定义有了,下面看下 HashMap 类中需要哪些属性? HashMap 类的定义如下图
private static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认容量
private static final float DEFAULT_FACTOR = 0.75f;
//默认加载因子
private Entry[] table;
//存放元素的数组
private int size;
//数组中元素个数
下面是关于hashmap的无参构造
public hashmap_1222() {
table = new Entry[DEFAULT_INITIAL_CAPACITY];
//默认创建数组大小为16
size = 0;
//元素个数为0
}
put方法(添加) ——详细请看注释
public V put(K key,V value)
{
if(key ==null)
{
throw new RuntimeException("key is null");
}
int hash = hash(key.hashCode());
int index = indexFor(hash,table.length);
//用key和数组大小做一次映射,得到其位置
Entry<K,V> entry = table[index];
//不为空 相同key的情况
while(entry !=null)
{
/**
* 先比较hash值是否相同,再比较对象是否相同,或者比较equals方法
* 如果相同,说明存在一个同样的key,这个时候需要把旧的值转换为新的值,同时返回(旧)value
*/
if((entry.hash ==hash) &&(key==entry.key ||key.equals(key)))
{
V oldValue = entry.value;
entry.value = value;
return oldValue;
}
entry = entry.next;
}
//没有value,有key,同时和进来的key相同(保存新的value)
Entry<K,V> next = table[index];
/**
* next可能为空,也可能不为空,不管最后是否为空,
* next都需要作为新元素的下一个节点
* 然后新元素保存在index上
*/
table[index] = new Entry<>(hash,key,value,next);
//当数组容量达到总容量75%,需要对其进行扩容处理(前面提到的负载因子)
if(size++ >= (table.length*DEFAULT_FACTOR))
{
resize();
}
return null;
}
下面的hash方法是照搬HashMap原码的(由于我是1.8的版本的,不同版本上的hash方法会有一些出入)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
static int indexFor(int h ,int length)
{
/**
* 逻辑与比取余的方式运算更好
*/
h = h > 0 ? h:-h;//避免出现负数
return h % length;
}
扩容(容量到达75%进行扩容)
private void resize()
{
//新建一个数组(这里设定为原来数组的两倍)
int newCapacity = table.length * 2;
Entry [] newTable = new Entry[newCapacity];
Entry[]src = table;
/**
* 遍历旧数组,重新映射到新数组上面
*/
for (int i = 0; i < src.length; i++) {
Entry<K,V> e = src[i];
//获取旧数组中的元素
//释放旧的元素
src[i]=null;
//由于e是一个节点,属于链表,可能有多个节点,循环遍历
while(e!=null)//没有数据,无法进行遍历
{
//把e的下一个节点数据进行保存
Entry<K,V> next = e.next;
//e在当前节点进行新的数组的映射
int i1 = indexFor(e.hash,newCapacity);
//newTable的位置上可能为null,可能不为null
//不管是否为null 都需要作为e的下一个节点
e.next = newTable[i];
//e的下一个节点同理
e = next;
}
//所有节点映射到新的数组上,新数组赋值给table
table = newTable;
}
}
get方法——详细请看注释
public V get(K key)
{
if(key ==null)
{
throw new RuntimeException("key is null");
}
//对于key求其hash值
int hash = hash(key.hashCode());
//用hash值进行映射,得到应该去那个位置上去取数据
int index = indexFor(hash, table.length);
/**
* 把index位置元素保存下来进行遍历
* 因为e是一个链表,我们需要进行遍历
* 找到和key相等的那个Entry,并返回value
*/
Entry<K,V> e = table[index];
while(e!=null)
{
//比较hash值是否相同
if(hash == e.hash &&(key == e.key||key.equals(e.key)))
{
return e.value;
}
//若不相同,则找一下个
e = e.next;
}
return null;
}
下面对其进行测试: