我这里就不带大家学基础和看源码了,建议大家先去看看基础知识再来跟着我自定义一个
1.创建Node节点类
在hashmap中,所有的节点实际上是一个个的Node节点,下面我就直接用代码加注释的方法来解释了。
class Node<K,V>{
//当前节点计算出来的hash值
//通过hash值对数组的长度取模,可以获取该Node节点在数组中的位置
int hash; //hash值
K k; //键
V v; //值
Node<K,V> next;//如果hash冲突,生成链表的话,则next指向下一个节点,没有下一个节点返回null
/*构造方法*/
public Node(int hash, K k, V v, Node<K, V> next) {
this.hash = hash;
this.k = k;
this.v = v;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"hash=" + hash +
", k=" + k +
", v=" + v +
", next=" + next +
'}';
}
//所有属性的set和get方法,不用多说
public int getHash() {return hash;}
public void setHash(int hash) {this.hash = hash;}
public K getK() {return k;}
public void setK(K k) {this.k = k;}
public V getV() {return v;}
public void setV(V v) {this.v = v;}
public Node<K, V> getNext() {return next;}
public void setNext(Node<K, V> next) {this.next = next;}
}
//最后这个Node类会作为我们后面定义的HashMap的内部类来使用
2.HashMap的参数设置
/*map的底层就是一个Node数组,每一个元素就是一个Node,如果这个元素有hash冲突,那么node转为链表*/
private Node<K,V> [] table;
/*数组的初始大小*/
private final int DEFAULT_INITIAL_CAPACITY = 1<<4; //1<<4 1左移四位 = 1000 就是16
private int size = 0;//table的实际长度(存了多少Node)
/**
* 负载因子:减少生成链表的机会,因为一旦table存满了,则必会增加生成链表的机会
* 所以在table还没有存满前,就要扩容
* */
private static float DEFAULT_FACTOR = 0.75f; //实际上就是一个百分数75%
/**
* 阈值:数组容量*负载因子
* */
private int threshold;
/*构造方法:初始化一个Node数组和阈值*/
public MyHashMap(){
table = new Node[DEFAULT_INITIAL_CAPACITY];
threshold = (int) (DEFAULT_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
public int size(){
return this.size;
}
这里有两个概念需要拿出来单独讲
2.1.位运算
在我们的Java编程中,这种容器还有其他什么的框架,虽然会使得我们的编码过程变得十分快捷和方便。但是,代价就是太吃内存了,这是因为我们在封装或者使用泛型等操作时,涉及的运算实在是太多了。因此,位运算就是其中的一个解决办法之一。
位运算是一种直接操作二进制数字的运算方式,通过对数字的二进制表示进行操作来实现各种计算。位运算相比于算术运算具有许多优点,这里我们只需要掌握两点:1.速度快,2.节省空间。
2.2.负载因子和阈值
在hashmap中,有一个重要的概念,那就是扩容。在不考虑红黑树的情况下,当添加节点时节点个数超过数组容量时,需要对数组扩容。
而hashmap的整体设计思想就是要减少hash冲突,减少产生链表的机会。我们在不断添加节点的过程中,随着数组存储的节点实际数量越来越多,发生hash冲突的几率是越来越大的。所以,我们规定,当节点实际个数为数组容量大小的75%(负载因子)时,就对数组进行扩容。
其实,简单点说就是,数组还没有存满前(达到阈值),就对其进行扩容,减少hash冲突的机会。
3.索引和hash值的计算
/**
* 利用该函数完成对key生成hashcode的操作
* Object中原来就有hashCode(),但是这种自带的函数生成的hash值比较相近
* 比如 a,b,c,d,e的哈希值就是它们的ASCLL值:97,98,99,100,101
* 解决办法:1.先调用hashCode()方法 2.hash值是32位整数,将它的高低16位进行混洗
* @param key
* @return 节点hash值
* 目的:使hash值更加离散
*/
static final int hash(Object key){
int h = key.hashCode();
return (key==null)?0:(h^(h>>16));//!!又是位运算
//h ^ (h >>> 16):将原始的 h 与其右移16位的结果进行异或运算。
}
/**
* @param n 就是数组的长度
* @param key 索引就是key的hash值对数组长度取余
* @return 返回在Node数组中的索引
*/
static int index(int n,Object key){
//hash(key)%n;
return (n-1)%hash(key);//等同于求余,此余数就是key在数组中的索引
}
4.hashmap的get方法
public V get(K k){
// 1.根据k获得数组下标
int index = index(table.length,k);
// 2.根据下标到table表中取出Node
Node<K,V> node = table[index];
// 3.Node==null,说明map中还没有这个key,直接返回空
if (node ==null) return null;
// 4.Node!=null 说明数组中有这个key
// 4.1.如果Node.next==null,说明这不是链表,只有唯一的节点,返回节点V
if (node.getNext()==null) return node.getV();
// 4.2.如果Node.next!=null,说明是一个链表,迭代找到K相等的节点,返回节点的V
else {
if (node.getK()==k) return node.getV();
Node<K,V> next = node.next;
while(next!=null){
if (next.getK()==k){
return next.getV();
}
next = next.next;
}
return null;
}
}
5.hashmap的put方法
public void put(K k,V v){
//1.想根据K计算数组索引
int index = index(table.length,k);
//2.取出这个索引的node
Node<K,V> node = table[index];
//3.如果node==null,说明没有冲突,即这个索引位置没有存东西
if (node ==null){
table[index] = new Node<>(hash(k),k,v,null);
size++;
}
//4.如果node!=null,有冲突:
else {
// 4. 如果 node != null,有冲突:
Node<K, V> current = node;
while (current != null) {//这里相当于遍历索引()
//4.1如果k值相同,则替换value值即可
if (current.getK().equals(k)) {
current.setV(v);
return;
}
if (current.next == null) {//说明索引处只有一个节点,没有产生链表
// 4.2. k 与 current 中的 k 不同,表示不同 key 但是 hash 值相同,生成链表新的节点
current.next = new Node<>(hash(k), k, v, null);
size++;
break;
}
current = current.next;//遍历下一个链表节点
}
}
// System.out.println("元素个数:"+size+",阈值:"+threshold+",容量:"+table.length);
if (size>threshold){
resize();//扩容这个数组,要将这个数组中的原来的数据移动;
}
}
6.扩容方法
为了解决JDK1.7中的死循环问题, 在jDK1.8中新增加了红黑树,即在数组长度大于64,同时链表长度大于8的情况下,链表将转化为红黑树。我们这里就先不考虑这个问题。
private void resize(){
Node [] tableOld = table;
Node[] tableNew = new Node[tableOld.length<<1];//扩容,原来的容量长度左移一位,相当于乘以2
for (int i =0;i<tableOld.length;i++){
if (tableOld[i]!=null){
//原来node的hash值,对新数组计算一次索引下标(因此,扩容后,节点的索引可能会与在旧数组的位置不一样)
int newIndex = index(tableNew.length,tableOld[i].hash);
tableNew[newIndex] = tableOld[i];
}
}
table = tableNew;//更新数组
//更新阈值
threshold = (int) (table.length*DEFAULT_FACTOR);
}
7.最后的完整代码
package 自定义HashMap;
import java.util.HashMap;
public class MyHashMap<K,V> {
/*map的底层就是一个Node数组,每一个元素就是一个Node,如果这个元素有hash冲突,那么node转为链表*/
private Node<K,V> [] table;
/*数组的初始大小*/
private final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //1<<4 1左移四位 = 1000 就是16
private int size = 0;//table的实际长度(存了多少Node)
/**
* 负载因子:减少生成链表的机会,因为一旦table存满了,,则必会增加生成链表的机会
* 所以在table还没有存满前,就要扩容
* */
private static final float DEFAULT_FACTOR = 0.75f; //实际上就是一个百分数75%
/**
* 阈值:数组容量+负载因子
* */
private int threshold;
/*构造方法:初始化一个Node数组和阈值*/
public MyHashMap(){
table = new Node[DEFAULT_INITIAL_CAPACITY];
threshold = (int) (DEFAULT_FACTOR * table.length);
}
public int size(){
return this.size;
}
/**
* 根据Key取值
* @param k
* @return
*/
public V get(K k){
// 1.根据k获得数组下标
int index = index(table.length,k);
// 2.根据下标到table表中取出Node
Node<K,V> node = table[index];
// 3.Node==null,说明map中还没有这个key,直接返回空
if (node ==null) return null;
// 4.Node!=null 说明数组中有这个key
// 4.1.如果Node.next==null,说明这不是链表,只有唯一的节点,返回节点V
if (node.getNext()==null) return node.getV();
// 4.2.如果Node.next!=null,说明是一个链表,迭代找到K相等的节点,返回节点的V
else {
if (node.getK()==k) return node.getV();
Node<K,V> next = node.next;
while(next!=null){
if (next.getK()==k){
return next.getV();
}
next = next.next;
}
return null;
}
}
/**
* 存
* @param k
* @param v
*/
public void put(K k,V v){
//1.想根据K计算数组索引
int index = index(table.length,k);
//2.取出这个索引的node
Node<K,V> node = table[index];
//3.如果node==null,说明没有冲突,即这个索引位置没有存东西
if (node ==null){
table[index] = new Node<>(hash(k),k,v,null);
size++;
}
//4.如果node!=null,有冲突:
else {
// 4. 如果 node != null,有冲突:
Node<K, V> current = node;
while (current != null) {//这里相当于遍历索引()
//4.1如果k值相同,则替换value值即可
if (current.getK().equals(k)) {
current.setV(v);
return;
}
if (current.next == null) {//说明索引处只有一个节点,没有产生链表
// 4.2. k 与 current 中的 k 不同,表示不同 key 但是 hash 值相同,生成链表新的节点
current.next = new Node<>(hash(k), k, v, null);
size++;
break;
}
current = current.next;//遍历下一个链表节点
}
}
// System.out.println("元素个数:"+size+",阈值:"+threshold+",容量:"+table.length);
if (size>threshold){
resize();//扩容这个数组,要将这个数组中的原来的数据移动;
}
}
/**
* 数组扩容函数
* */
private void resize(){
//扩容,原来的容量长度左移一位,相当于乘以2
Node [] tableOld = table;
Node[] tableNew = new Node[tableOld.length<<1];
for (int i =0;i<tableOld.length;i++){
if (tableOld[i]!=null){
//原来node的hash值,对新数组计算一次索引下标(因此,扩容后,节点的索引可能会与在久数组的位置不一样)
int newIndex = index(tableNew.length,tableOld[i].k);
tableNew[newIndex] = tableOld[i];
}
}
table = tableNew;//更新数组
//更新阈值
threshold = (int) (table.length*DEFAULT_FACTOR);
}
/**
* 利用该函数完成对key生成hashcode的操作
* Object中原来就有hashCode(),但是这种自带的函数生成的hash值比较相近
* 比如 a,b,c,d,e的哈希值就是它们的ASCLL值:97,98,99,100,101
* 解决办法:1.先调用hashCode()方法 2.hash值是32位整数,将它的高低16位进行混洗
* @param key
* @return
* 目的:使hash值更加离散
*/
static final int hash(Object key){
int h = key.hashCode();
return (key==null)?0:(h^(h>>16));
}
/**
* @param n 就是数组的长度
* @param key 索引就是key的hash值对数组长度取余
* @return 返回在Node数组中的索引
*/
static int index(int n,Object key){
//hash(key)%n;
return (n-1)%hash(key);//等同于求余,此余数就是key在数组中的索引
}
/**
* 每一个node就是数组里的一个节点
* @param <K>
* @param <V>
*/
class Node<K,V>{
//当前节点计算出来的hash值
//通过hash值模数组的长度,可以获取该Node节点在数组中的位置
int hash; //hash值
K k; //键
V v; //值
Node<K,V> next;//如果hash冲突,生辰链表的话,则next指向下一个节点,没有下一个节点返回null
/*构造方法*/
public Node(int hash, K k, V v, Node<K, V> next) {
this.hash = hash;
this.k = k;
this.v = v;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"hash=" + hash +
", k=" + k +
", v=" + v +
", next=" + next +
'}';
}
//所有属性的set和get方法,不用多说
public int getHash() {return hash;}
public void setHash(int hash) {this.hash = hash;}
public K getK() {return k;}
public void setK(K k) {this.k = k;}
public V getV() {return v;}
public void setV(V v) {this.v = v;}
public Node<K, V> getNext() {return next;}
public void setNext(Node<K, V> next) {this.next = next;}
}
}
8.测试
public static void main(String[] args) {
MyHashMap<Integer,String> hashMap = new MyHashMap<>();
hashMap.put(1,"张1");
hashMap.put(2,"李2");
hashMap.put(3,"王3");
hashMap.put(1,"赵1");
hashMap.put(1,"陈1");
System.out.println(hashMap.size());
System.out.println(hashMap.get(1));
System.out.println(hashMap.get(2));
System.out.println(hashMap.get(3));
}
测试结果:
3
陈1
李2
王3
9.小结
这里,我只是对基础的set和get方法进行了自定义,还有只使用了链表进行扩容,没有运用红黑树的知识。实际上的hashmap还有更多的方法,这就需要我们细读源码了,在后续我有时间的话会更新的。当然,官方源码肯定比我写的详细且规范。如有错误和其他意见,还请在评论区留言指正哦。