【java】面试经典之hashmap解读,并简单手写一个hashmap

本文详细解析了HashMap的底层数据结构,包括hash函数、数组和单链表的运用,探讨了hash函数的设计原则,如何处理hash冲突,以及HashMap的扩容机制。同时,提供了自定义HashMap的实现代码,帮助读者更深入地理解其工作原理。

HashMap是在面试中经常会问的一点,很多时候我们仅仅只是知道HashMap他是允许键值对都是Null,并且是非线程安全的,如果在多线程的环境下使用,是很容易出现问题的。 这是我们通常在面试中会说的,但是有时候问到底层的源码分析的时候,为什么允许为Null,为什么不安全,这些问题的时候,如果没有分析过源码的话,好像很难回答, 这样的话我们来研究一下这个源码。看看原因吧。

  • 底层数据结构

第一,如图所示,HashMap有3个要素:hash函数+数组+单链表
第二,对于hash函数而言,需要考虑些什么?
要快,对于给定的Key,要能够快速计算出在数组中的index。那么什么运算够快呢?显然是位运算!
要均匀分布,要较少碰撞。说白了,我们希望通过hash函数,让数据均匀分布在数组中,不希望大量数据发生碰撞,导致链表过长。那么怎么办到呢?也是利用位运算,通过对数据的二进制的位进行移动,让hash函数得到的数据散列开来,从而减低了碰撞的概率。
如果发生了碰撞怎么办?上面的图其实已经说明了JDK的HashMap是如何处理hash冲突的,就是通过单链表解决的。那么除了这个方法,还有其他思路么?比如说,如果发生冲突,那么记下这个冲突的位置为index,然后在加上固定步长,即index+step,找到这个位置,看一下是否仍然冲突,如果继续冲突,那么按照这个思路,继续加上固定步长。其实这就是所谓的线性探测来解决Hash冲突的方法!

  • 定义一个Map接口

定义一个接口,对外暴露快速存取的方法。


```java
public interface Map<K,V> {
    //存值
    public V put(K k, V v);
    //取值
    public V get(K k);
}
  • 定义一个Entry链表

HashMap的要素之一,单链表的体现就在这里!

public class Entry<K, V> {
    K k;
    V v;
    Entry<K, V> next;
    public Entry() {
    }
    /**
     * 在next指向下一个节点
     */
    public Entry(K k, V v, Entry<K, V> next) {
        this.k = k;
        this.v = v;
        this.next = next;
    }
    public K getKey() {
        return k;
    }
    public V getValue() {
        return v;
    }
}
  • 实现Map接口
  • 重写有参/无参构造方法

仔细观察下,你会发现,其实这里使用到了“门面模式”。这里的2个构造方法其实指向的是同一个,但是对外却暴露了2个“门面”!

public class MyHashMap<K, V> implements Map<K, V> {

    //数组默认长度
    private final static int DEFAULT_INIT_CAPACITY = 1 << 4;
    //默认负载因子
    private final static float DEFAULT_LOAD_FACTOR = 0.75f;
    //初始定义的长度
    private int defaultInitCapacity;
    //初始定义的负载因子
    private float defaultLoadFactor;
    //实际的数组长度
    private int userSize;

    //entry数组
    Entry<K, V>[] tables;

    //无参构造
    public MyHashMap() {
        this(DEFAULT_INIT_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    //有参构造
    public MyHashMap(int defaultInitCapacity, Float defaultLoadFactor) {
        if (defaultInitCapacity < 0) {
            throw new IllegalArgumentException("初始长度不能是0" + defaultInitCapacity);
        }
        if (defaultLoadFactor <= 0 || Float.isNaN(defaultLoadFactor)) {
            throw new IllegalArgumentException("负载因子错了!" + defaultLoadFactor);
        }
        this.defaultInitCapacity = defaultInitCapacity;
        this.defaultLoadFactor = defaultLoadFactor;

        this.tables = new Entry[this.defaultInitCapacity];
    }
}
  • 存值put

第一,要考虑是否扩容? HashMap中的Entry的数量(数组以及单链表中的所有Entry)是否达到阀值?
第二,如果扩容,意味着新生成一个Entry[],不仅如此还得重新散列。
第三,要根据Key计算出在Entry[]中的位置,定位后,如果Entry[]中的元素为null,那么可以放入其中,如果不为空,那么得遍历单链表,要么更新value,要么形成一个新的Entry“挤压”单链表!

 //存值
    @Override
    public V put(K k, V v) {
        //判断是否需要扩容
        if (this.userSize > this.defaultInitCapacity * this.defaultLoadFactor) {
            this.resize();
        }
        //计算出当前下标
        int index = this.getIndex(k, this.defaultInitCapacity);
        //获取下标上的entry
        Entry<K, V> entry = this.tables[index];
        //创建一个新的newEntry
        Entry<K, V> newEntry = new Entry<K, V>(k, v, null);
        //判断当前下标是否被使用,如果没有就将newEntry填入
        if (entry == null) {
            this.tables[index] = newEntry;
            //使用后,使用长度+1
            this.userSize++;
        } else {
            Entry<K, V> e = entry;
            //判断当前key是否等于传入的k
            if (e.getKey() == k) {
                //如果相等,就将传入v赋值给e.v
                e.v = v;
            } else {
                //如果不相等,就循环遍历entry的下一个entry.next
                while (e.next != null) {
                    //判断如果下一个entry的key是否等于传入的k,如果相等就赋值v
                    if (e.next.getKey() == k || e.next.getKey().equals(k)) {
                        e.next.v = v;
                    } else {
                        e = e.next;
                    }
                }
                //判断上边循环后entry.next是否等于null,如果等于null
                //将newEntry设置到entry.next的位置
                if (e.next == null) {
                    e.next = newEntry;
                }
            }
        }
        // 返回newEntry.getValue()
        return newEntry.getValue();
    }
  • 取值get

get很简单,只需要注意在遍历单链表的过程中使用== or equals来判断下即可。

    //取值
    @Override
    public V get(K k) {
        // 获取当前下标
        int index = this.getIndex(k, this.tables.length);
        // 得到下标上的entry
        Entry<K, V> entry = this.tables[index];
        // entry非空校验
        if (entry == null) {
            throw new NullPointerException("空空如也");
        }

        // 循环entry != null
        while (entry != null) {
            // key相等就返回
            if (entry.getKey() == k || entry.getKey().equals(k)) {
                return entry.getValue();
            } else {
                // 如果不相等,将next赋值给entry继续循环
                entry = entry.next;
            }
        }
        // 找不到就返回null
        return null;
    }
  • 计算hashcode

我这里参考了JDK的HashMap的hash函数的实现,这里也再次说明了:要想散列均匀,就得进行二进制的位运算!

    //计算hashcode
    private int hash(int hashCode) {
        hashCode = hashCode ^ ((hashCode >>> 20) ^ (hashCode >>> 12));
        return hashCode ^ ((hashCode >>> 7) ^ (hashCode >>> 4));
    }
  • 扩容resize

这里可以看出,对于HashMap而言,如果频繁进行resize/rehash操作,是会影响性能的。
resize/rehash的过程,就是数组变大,原来数组中的entry元素一个个的put到新数组的过程,需要注意的是一些状态变量的改变。

    //扩容
    private void resize() {
        // 创建一个新的entry数组 ,长度为 defualtLength*2
        Entry<K, V>[] newEntryTable = new Entry[this.defaultInitCapacity * 2];

        // 创建一个list 用来存放entry
        List<Entry<K, V>> entryList = new ArrayList<Entry<K, V>>();

        // 1.先将历史的数据保存
        // 循环entry数组
        for (int i = 0; i < tables.length; i++) {
            Entry<K, V> entry = tables[i];
            // 判断数组下标上的entry!=null
            while (entry != null) {
                entryList.add(entry);
                // 将enrty存到list中,entry = entry.next;
                entry = entry.next;
            }
        }

        // 非空校验list
        if (entryList != null && entryList.size() > 0) {
            // 重新设置默认长度
            this.defaultInitCapacity = this.defaultInitCapacity * 2;
            // 使用长度重置为0
            this.userSize = 0;
            // 将newtable赋值给table
            this.tables = newEntryTable;
            // 2.将保存好的数据存到新的容器中
            for (Entry<K, V> entry : entryList) {
                // 循环list中的entry,并将其next置位null
                if (entry.next != null) {
                    entry.next = null;
                }
                // 调用put方法,传入entry.k entry.v
                put(entry.getKey(), entry.getValue());
            }
        }
    }
  • 测试代码
public class Test1 {

    public static void main(String[] args) {

        Map<String,Integer> map = new MyHashMap<>();
        for(int i=0;i<10000;i++){
            map.put(""+i, i);
        }

        for(int i=0;i<10000;i++){
            System.out.println("key:"+i+"value:"+map.get(""+i));
        }
    }
    
}
  • 测试结果
    在这里插入图片描述
    全部代码一览
package hashmap;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: wangpeng
 * @Date: 2020-8-10 23:01
 */
public class MyHashMap<K, V> implements Map<K, V> {

    //数组默认长度
    private final static int DEFAULT_INIT_CAPACITY = 1 << 4;
    //默认负载因子
    private final static float DEFAULT_LOAD_FACTOR = 0.75f;
    //初始定义的长度
    private int defaultInitCapacity;
    //初始定义的负载因子
    private float defaultLoadFactor;
    //实际的数组长度
    private int userSize;

    //entry数组
    Entry<K, V>[] tables;

    //无参构造
    public MyHashMap() {
        this(DEFAULT_INIT_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    //有参构造
    public MyHashMap(int defaultInitCapacity, Float defaultLoadFactor) {
        if (defaultInitCapacity < 0) {
            throw new IllegalArgumentException("初始长度不能是0" + defaultInitCapacity);
        }
        if (defaultLoadFactor <= 0 || Float.isNaN(defaultLoadFactor)) {
            throw new IllegalArgumentException("负载因子错了!" + defaultLoadFactor);
        }
        this.defaultInitCapacity = defaultInitCapacity;
        this.defaultLoadFactor = defaultLoadFactor;

        this.tables = new Entry[this.defaultInitCapacity];
    }

    //计算hashcode
    private int hash(int hashCode) {
        hashCode = hashCode ^ ((hashCode >>> 20) ^ (hashCode >>> 12));
        return hashCode ^ ((hashCode >>> 7) ^ (hashCode >>> 4));
    }

    //获取保存位置的数组下标
    private int getIndex(K k, int length) {
        //减1后,下标永远不会大于16
        int m = length - 1;
        int index = hash((k.hashCode())) & m;
        return index > 0 ? index : -index;
    }

    //存值
    @Override
    public V put(K k, V v) {
        //判断是否需要扩容
        if (this.userSize > this.defaultInitCapacity * this.defaultLoadFactor) {
            this.resize();
        }
        //计算出当前下标
        int index = this.getIndex(k, this.defaultInitCapacity);
        //获取下标上的entry
        Entry<K, V> entry = this.tables[index];
        //创建一个新的newEntry
        Entry<K, V> newEntry = new Entry<K, V>(k, v, null);
        //判断当前下标是否被使用,如果没有就将newEntry填入
        if (entry == null) {
            this.tables[index] = newEntry;
            //使用后,使用长度+1
            this.userSize++;
        } else {
            Entry<K, V> e = entry;
            //判断当前key是否等于传入的k
            if (e.getKey() == k) {
                //如果相等,就将传入v赋值给e.v
                e.v = v;
            } else {
                //如果不相等,就循环遍历entry的下一个entry.next
                while (e.next != null) {
                    //判断如果下一个entry的key是否等于传入的k,如果相等就赋值v
                    if (e.next.getKey() == k || e.next.getKey().equals(k)) {
                        e.next.v = v;
                    } else {
                        e = e.next;
                    }
                }
                //判断上边循环后entry.next是否等于null,如果等于null
                //将newEntry设置到entry.next的位置
                if (e.next == null) {
                    e.next = newEntry;
                }
            }
        }
        // 返回newEntry.getValue()
        return newEntry.getValue();
    }

    //取值
    @Override
    public V get(K k) {
        // 获取当前下标
        int index = this.getIndex(k, this.tables.length);
        // 得到下标上的entry
        Entry<K, V> entry = this.tables[index];
        // entry非空校验
        if (entry == null) {
            throw new NullPointerException("空空如也");
        }

        // 循环entry != null
        while (entry != null) {
            // key相等就返回
            if (entry.getKey() == k || entry.getKey().equals(k)) {
                return entry.getValue();
            } else {
                // 如果不相等,将next赋值给entry继续循环
                entry = entry.next;
            }
        }
        // 找不到就返回null
        return null;
    }

    //扩容
    private void resize() {

        // 创建一个新的entry数组 ,长度为 defualtLength*2
        Entry<K, V>[] newEntryTable = new Entry[this.defaultInitCapacity * 2];

        // 创建一个list 用来存放entry
        List<Entry<K, V>> entryList = new ArrayList<Entry<K, V>>();

        // 1.先将历史的数据保存
        // 循环entry数组
        for (int i = 0; i < tables.length; i++) {
            Entry<K, V> entry = tables[i];
            // 判断数组下标上的entry!=null
            while (entry != null) {
                entryList.add(entry);
                // 将enrty存到list中,entry = entry.next;
                entry = entry.next;
            }
        }

        // 非空校验list
        if (entryList != null && entryList.size() > 0) {
            // 重新设置默认长度
            this.defaultInitCapacity = this.defaultInitCapacity * 2;
            // 使用长度重置为0
            this.userSize = 0;
            // 将newtable赋值给table
            this.tables = newEntryTable;
            // 2.将保存好的数据存到新的容器中
            for (Entry<K, V> entry : entryList) {
                // 循环list中的entry,并将其next置位null
                if (entry.next != null) {
                    entry.next = null;
                }
                // 调用put方法,传入entry.k entry.v
                put(entry.getKey(), entry.getValue());
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值