通过自定义一个HashMap来学习HashMap的数据结构

写在最前:首先要搞清楚HashMap的数据结构是怎样的,它是用来解决什么问题的,以及该数据结构中体现javabean结构的成员变量,有参/无参构造,成员方法等是如何定义的。

本文所写的数据结构模拟的是jdk7,数组+链表。jdk8的红黑树只是优化链表,后续更新。

可以参考下我之前的文章:《HashMap源码分析(jdk8)》

我们先思考几个问题:

1. 有哪些成员变量?各自的默认值是什么?

2. 有哪些构造方法?

3. 有哪些成员方法?

4. 数组是如何定义的?

5. 链表是如何定义的?

6. 什么是hash冲突,它什么时候发生?该怎么解决?

7. map的put方法,如何判定key是否重复?

8. 何时扩容?

9. hashmap如何优化?

10. 欢迎评论区补充,一起探讨

我们先通过自定义一个HashMap来了解这种数据结构,再来回答这些问题。

1. 定义MyMap接口

/**
 * @author yog
 *
 * 自定义map接口
 * @param <K> key
 * @param <V> value
 */
public interface MyMap<K,V> {

    /**
     * put
     * @param k key
     * @param v value
     * @return value
     */
    V put(K k,V v);

    /**
     * get
     * @param k key
     * @return alue
     */
    V get(K k);

    /**
     * 内部接口,存放key-value的entry桶
     * @param <K> key
     * @param <V> value
     */
    interface Entry<K,V>{

        /**
         * getKey 获取key
         * @return key
         */
        K getKey();

        /**
         * getValue 获取value
         * @return value
         */
        V getValue();
    }
}

暂定一个get和一个put方法。

2. 定义MyHashMap,实现MyMap接口

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

    @Override
    public Object put(Object o, Object o2) {
        return null;
    }

    @Override
    public Object get(Object o) {
        return null;
    }
}

2.1 成员变量

    /**
     * 定义数组初始化容量 32
     */
    private static final int DEFAULT_INITIAL_CAPATITY = 1 << 4;

    /**
     * 定义加载因子 0.75f
     */
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 默认初始化容量
     */
    private transient int defaultInitSize;

    /**
     * 默认加载因子
     */
    private final float defaultLoadFactor;

    /**
     * entry数组
     */
    private Entry<K,V>[] table;

    /**
     * map中entry数量
     */
    private int entryUseSize;

2.2 构造方法

    //这里两个构造方法其实指向同一个构造方法,但对外暴露两个,"门面模式"
    MyHashMap(){
        this(DEFAULT_INITIAL_CAPATITY,DEFAULT_LOAD_FACTOR);
    }

    private MyHashMap(int defaultInitialCapatity, float defaultLoadFactor) {
        if(defaultInitialCapatity < 0){
            throw new IllegalArgumentException("非法初始化容量异常:" + defaultInitialCapatity);
        }

        if (defaultLoadFactor <= 0 || Float.isNaN(defaultLoadFactor)){
            throw new IllegalArgumentException("非法初始化加载因子异常" + defaultLoadFactor);
        }

        this.defaultInitSize = defaultInitialCapatity;
        this.defaultLoadFactor = defaultLoadFactor;

        table = new Entry[this.defaultInitSize];
    }

2.3 定义Entry数组

    /**
     * @author yog
     * @param <K> key
     * @param <V> value
     *
     * 内部类
     */
    static class Entry<K,V> implements MyMap.Entry<K,V>{

        private K key;

        private V value;

        private Entry<K,V> next;

        public Entry(){}

        private Entry(K key, V value, Entry<K,V> next){
            this.key = key;
            this.value = value;
            this.next = next;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }
    }

2.4 put方法

    @Override
    public V put(K k, V v) {
        V oldValue = null;

        //是否需要扩容
        if(entryUseSize >= defaultInitSize * defaultLoadFactor ){
            //扩容完毕,重新散列
            resize(2 * defaultInitSize);
        }

        //求hash值,计算在数组中的位置
        int index = hash(k) & (defaultInitSize - 1);
        if(null == table[index]){
            table[index] = new Entry<K,V>(k,v,null);
            ++entryUseSize;
        }else {
            //需要遍历单链表
            Entry<K,V> entry = table[index];
            Entry<K,V> e = entry;
            while (null != e){
                if(k == e.getKey() || k.equals(e.getKey())){
                    oldValue = e.value;
                    e.value = v;
                    return oldValue;
                }
                e = e.next;
            }
            table[index] = new Entry<K,V>(k,v,entry);
            ++entryUseSize;
        }
        return oldValue;
    }

2.5 resize扩容

    //参考jdk的hashap的hash运算
    private int hash(K k){
        int hashCode = k.hashCode();
        hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12);
        return hashCode ^ (hashCode >>> 7) * (hashCode >>> 4);
    }

    //从这里可以看到resize/rehash的操作是影响性能的,需要数组的重新put操作。但要注意状态变量的变化
    private void resize(int i){
        Entry[] newTable = new Entry[i];
        //改变数组的大小
        defaultInitSize = i;
        entryUseSize = 0;
        rehash(newTable);
    }

    private void rehash(Entry<K,V>[] newTable) {
        //将旧table遍历,放入新集合中
        List<Entry<K,V>> entries = new ArrayList<>();
        for (Entry<K, V> entry : table) {
            if(null != entry){
                do{
                    entries.add(entry);
                    entry = entry.next;
                }while (null != entry);
            }
        }

        //覆盖旧的引用
        if(newTable.length > 0){
            table = newTable;
        }

        //重新hash:重新put entry到hashMap
        for (Entry<K, V> entry : entries) {
            put(entry.getKey(),entry.getValue());
        }
    }

2.6 get方法

    @Override
    public V get(K k) {

        int index = hash(k) & (defaultInitSize - 1);
        if(null == table[index]){
            return null;
        }else {
            Entry<K, V> entry = table[index];
            do {
                if(k == entry.getKey() || k.equals(entry.getKey())){
                    return entry.getValue();
                }
                entry = entry.next;
            }while (null != entry);
        }

        return null;
    }

3. 测试

public class MyHashMapTest {

    public static void main(String[] args) {
        MyMap<Integer,String> myMap = new MyHashMap<>();
        for (int i = 0; i < 100; i++) {
            myMap.put(i,"value" + i);
        }

        for (int i = 0; i < 100; i++) {
            System.out.println("key : " + i + " , value : " + myMap.get(i));
        }
    }

}

测试结果:

4. 分析HashMap数据结构

4.1 成员变量

  • 数组默认的初始化容量DEFAULT_INITIAL_CAPATITY为32(必须是2的次幂),默认的加载因子DEFAULT_LOAD_FACTOR为0.75f。初始化容量决定hashmap中初始化时存放的元素数量,加载因子决定hashmap何时进行扩容。当HashMap中元素数量超过 初始化容量*加载因子 时,就进行扩容resize()操作,如达到 16*0.75=12时,就进行扩容,默认扩容为原来的两倍,16*2=32。
  • Entry数组:它是一个静态的内部类。从Entry的构造函数可以看出,每次创建新的Entry对象,就会把链表的头结点作为next拼接到新的entry对象上。也就是说每次插入新的map数据时,就会生成一个bucket在链表头部。由此可以得出单链表的实现方式。
  • 在jdk8之后,采用了红黑树来优化单链表。当哈希冲突比较多的时候,因为链表的长度很大 , 链表是不利于查询的,有利于删除和插入,所以引进了红黑树,每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置。若桶(hashmap的table数组)中链表元素超过8,会自动转化成红黑树;若桶中元素小于等于6时,树结构还原成链表。红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果小于等于6,6/2=3,虽然速度也很快,但是转化为树和生成树的时间并不会太短。中间的差值7为了防止链表和树频繁的转换,试想如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
  • 扩容resize:扩容后需要再哈希rehash。put操作时也要判断是否需要扩容。rehash操作时,先用一个新的数组接受原数组,重新put entry到新数组中。
  • hash:参考jdk的hash运算。
  • rehash操作/resize操作可以看出是需要开销的,所以主要优化的地方也在这里。如果业务中能判断初始化容量,可以提前设置好初始化参数。而默认加载因子0.75f是平衡了时间和空间等因素; 负载因子越小桶的数量越多,读写的时间复杂度越低(极限情况O(1), 哈希碰撞的可能性越小); 负载因子越大桶的数量越少,读写的时间复杂度越高(极限情况O(n), 哈希碰撞可能性越高)。 0.1,0.9,2,3等都是合法值。

4.2 构造方法

  • 无参构造:它并没有实际的操作,而是调用有参构造。源码中它的作用是初始化一些参数。
  • 有参构造:初始化参数。初始化加载因子和初始化容量。

4.3 成员方法

4.3.1 put方法

  1. 首先根据当前数组容量,来判断是否需要扩容;如果需要扩容,需要进行rehash操作。
  2. 之后根据put时的key来计算hash值,计算在数组中的位置;如果hash值在数组中不存在,就直接将元素设置到数组中,并且容量加一;否则遍历该索引位置上的单链表,如果单链表已存在value,就替换,并将旧值返回;如果不存在就set该元素到链表头位置,同时容量加一。

4.3.2 get方法

      首先计算key的hash值,如果hash值为null,说明数组中不存在该key,就返回null;如果hash值存在,通过该hash值得到数组中的Entry位置,并且判断该位置上的元素的key和请求参数key是否相同(需要==和equals同时比较),如果相同则返回该value,否则返回null。

 

代码复制粘贴完即可运行。欢迎评论指正!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值