源码冲浪之手撕HashMap1.7

HashMap简介

HashMap是我们最常用到的集合之一,是java非常典型的数据结构。学习它的源码是非常只有必要的,我们所要了解的并不仅仅是“HashMap不是线程安全的,HashTable是线程安全的,通过synchronized实现的。HashMap取值非常快”等等。
了解hashmap必须要先对hashmap的存储结构有个了解
在这里插入图片描述
它是属于数组及链表相结合的存储结构。如上图 x轴为数组,y轴为链表。
数组存储方式在内存地址是连续大小固定,一旦分配无法被其他引用占用,查询迅速,时间复杂度O(1),插入删除比较慢,时间复杂度为O(n)。
而链表存储方式则与数组相反,属于非连续性,大小非固定,插入及删除块,查询速度慢。
所以HashMap相对中庸。

HashMap的一些常见问题

HashMap的数据结构是啥?数据结构上存储的数据对象结构是啥?

HashMap是一个存储数据对象<封装了K,V属性的对象>的集合,而这个集合是数组+链表类型的数据结构。

根据源码来分析hashMap内部的精髓 hash算法如何保证散列均匀冲突的解决方式?

谈到hash
通常我们jdk的equals在比较的时候就会使用hash算法,此算法会定位到对象的存储位置
具体hash的原理是:
hash函数:找到存储过程
被重写的hashCode(key)
index=h=Hash(int hashCode)
(key.hashCode)&&length -1
length 2^n
通过h就可以找到数组下标的位置
例子如下:
2^4=16
length-1 =15 二进制为 01111
h返回的是 10101
数组上存储的位置为: 00101 【上下都是1才是1】
好处:
1 )散列的范围被低位限制—》散列位置一定在我们的索引范围(即length-1)之内。
2 )低位的0如果越多 代表我们散列的结果越固定。【想象一个若是非length-1就会发生
10000 低位0较多,导致散列结果几乎就是一致】,导致冲突越多,导致数组位置的利用率不高。

HashMap并发闭环问题?

扩容方法会新建一个数组,复制原数组到新的数组,由于下标挂着链表,扩容之后会导致环形链表的出现,JDK1.8已经解决了这个问题了。

手撕HashMap

首先定义最基础的map接口

package com.hikvision.rabbitmq.map;

/**
 * @ClassName Map
 * @Description TODO
 * @Autuor lulinfeng
 * @Date 2020/8/18
 * @Version 1.0
 */
public interface Map<K, V> {
    V get(K k);

    V put(K k, V v);
}

然后定义散列实体

package com.hikvision.rabbitmq.map;

/**
 * @ClassName Entry
 * @Description TODO
 * @Autuor lulinfeng
 * @Date 2020/8/18
 * @Version 1.0
 */
public class Entry<K, V> {
    K k;
    V v;
    Entry<K, V> next;

    public Entry(K k, V v, Entry<K, V> next) {
        this.k = k;
        this.v = v;
        this.next = next;
    }

    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;
    }
}

然后开始书写我们自定义的HashMap
首先我们定义最基础的四个元素

    // 定义数组大小 16
    // 结合着下面的扩容因子来解释一波:假如数组用了 4 usesize/defaulLenth =4/16=0.25 即使用率<0.75,不会扩容
    private static int defaulLenth = 1 << 4;
    // 扩容标准 所使用的useSize / 数组长度 >0.75
    // defaulAddSizeFactor 过大 造成扩容概率变低 存储小 但是就是存与取的效率降低
    // 0.9 有限的数组长度空间位置内会形成链表 在存与取值中都必须进行大量的遍历和判断(逻辑)
    // 过小 内存使用比较多,使用率不高,造成浪费
    private static double defaulAddSizeFactor = 0.75;
    // 使用数组位置的总数
    private int useSize;
    // 定义Map 骨架 只要 数组之一 数组
    private Entry<K, V>[] table = null;

然后用门面模式,这样可以传参来控制散列大小和扩容因子

// Spring 门面模式运用
    public HashMap() {
        this(defaulLenth, defaulAddSizeFactor);
    }

    public HashMap(int length, double AddSizeFactor) {
        if (length < 0) {
            throw new IllegalArgumentException("参数不能为负数" + length);
        }
        if (defaulAddSizeFactor <= 0 || Double.isNaN(defaulAddSizeFactor)) {
            throw new IllegalArgumentException("扩容标准必须是大于0的数字" + defaulAddSizeFactor);
        }
        defaulLenth = length;
        defaulAddSizeFactor = AddSizeFactor;
        table = new Entry[defaulLenth];
    }

PUT方法

然后我们开始书写put方法
put的最简单的逻辑即:
1)判断是否需要扩容
2)判断是数组对应index是否存在链表,若无塞值,若有挂载链表上

 @Override
    public V put(K k, V v) {
        // 存储是判断是否需要扩容
        if (useSize > defaulAddSizeFactor * defaulLenth) {
            up2Size();
        }
        // 获取数组下标
        int index = getIndex(k, table.length);
        Entry<K, V> entry = table[index];
        // 判断这个entry是否为空,为空意味着未被散列到
        if (entry == null) {
            table[index] = new Entry(k, v, null);
            useSize++;
        } else if (entry != null) {
            // 形成了链表结构
            table[index] = new Entry(k, v, entry);
        }
        return table[index].getV();
    }

获取数组下标【即为啥必须要是2的n次方】

这边我们首先看下获取数组下标的方法

/**
     * 寻找数组的下标
     **/
    private int getIndex(K k, int length) {
        int m = length - 1;
        int index = hash(k.hashCode()) & m;
        return index;
    }

采取数组大小-1 01111111 & k的hash 这样比较稳定
看下自定义hash算法

 /**
     * 自定义hash算法
     **/
    private int hash(int hashCode) {
        hashCode = hashCode ^ (hashCode >>> 20) ^ (hashCode >>> 12);
        return hashCode ^ (hashCode >>> 7) ^ (hashCode >>> 4);
    }

扩容方法

说到扩容,无非就是两点:
1)将新建一个2倍大小的数组
2)将旧的散列赋值到新的散列中

/**
     * 扩容
     **/
    private void up2Size() {
        // 如何扩容,无非就是新建一个2倍空间的数组
        Entry<K, V>[] newTable = new Entry[2 * defaulLenth];
        // 老数组的内容拿到新数组中
        againHash(newTable);
    }

具体复制

 /**
     * 复制旧散列到新散列
     **/
    private void againHash(Entry<K,V>[] newTable) {
        List<Entry<K, V>> entryList = new ArrayList<Entry<K, V>>();
        // for循环 即老数组内容被全部遍历到了entryList中
        for (int i = 0; i < table.length; i++) {
            if (table[i] == null) {
                continue;
            }
            // 继续找存到数组上的entry对象
            foundEntryByNext(table[i], entryList);
        }
        // 设置entryList
        if (entryList.size() > 0) {
            useSize = 0;
            defaulLenth = 2 * defaulLenth;
            for (Entry<K, V> entry : entryList) {
                if (entry.next != null) {
                    entry.next = null;
                }
                put(entry.getK(), entry.getV());
            }
        }
    }

这边要注意数组上还可能挂着链表

 /**
     * 查询链表
     **/
    private void foundEntryByNext(Entry<K,V> entry, List<Entry<K,V>> entryList) {
        // 形成了链表结构
        if (entry != null && entry.next != null) {
            entryList.add(entry);
            // 递归,不断地一层层取存entry
            foundEntryByNext(entry.next, entryList);
        } else {
            // 没有链表的情况
            entryList.add(entry);
        }
    }

GET方法

get相对简单

 public V get(K k) {
        int index = getIndex(k, table.length);
        if (table[index] == null) {
            throw new NullPointerException();
        }
        return findByValueByEqualKey(k, table[index]);
    }

采取拉链法来获取数据即k相等,且equals也相等

/**
     * 拉链法查询
     **/
    private V findByValueByEqualKey(K k, Entry<K,V> entry) {
        if (k == entry.getK() || k.equals(entry.getK())) {
            return entry.getV();
        } else if (entry.next != null) {
            return findByValueByEqualKey(k, entry.next);
        }
        return null;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值