手撕HashMap源码1

目录

简单说一下什么是哈希表

哈希冲突的常见解决办法

1.开放寻址法

        1.1线性的地址检测

        1.2二次检测

        1.3双重散列

2.链表法

 HashMap的源码讲解

常见属性

初始化对象的几个构造函数

实际的扩容分析

 几个常见问题

 手写一个简单的哈希表


简单说一下什么是哈希表

其实就是在数组上面,然后不按照固定的索引去存放数据,这也是利用数组支持按照索引下标进行随机性访问的特性。其中,索引的随机生成并且保证它的唯一性,就是我们哈希表的重要任务

那么这个哈希值怎么算出来呢,一般我们会通过一个散列函数去进行计算得到,比如在HashMap的源码里面有下面这个散列函数

上面这个哈希函数可以自行设计,它的设计思想就是先拿到key的原始哈希值,这个是系统给我们实现的函数函数,在Java中,每个类都继承自Object类,而Object类中有一个默认的hashCode()方法。这默认的hashCode()方法是根据对象的内存地址计算的,因此相同内容的对象在内存中不同的位置,其hashCode()值也会不同。

总之,它能先初步计算出一个哈希值,在把这个hashCode的高16位和低16位进行混合,使得高位和低位的信息更好地交叉,进一步增加混淆性。减少了哈希冲突,哈希冲突也就是不同的对象进来你生成的哈希值一样。

那这里提一点,如果是相同的对象,你肯定要生成相同的哈希值,哈希函数也不能太复杂,不然也会耗费大量的计算时间,尽可能让算出来的哈希值随机且均匀的分布,这样也能减少冲突

一些常见的设计就比如:处余法,平方取中间数字,稍微了解一下

哈希冲突的常见解决办法

一般来说,分为两类方法来解决散列冲突:开放寻址法,链表法

1.开放寻址法

        1.1线性的地址检测

        核心:一旦出现了散列冲突,我们就重新去寻址一个空的散列地址,比如从当前位置,依         次往后面找,看看是否有空闲,直到找到空闲位置为止

        1.2二次检测

        和上面差不多的思想,只不过上面是一个地址一个地址移动往下面检测,这里把检测不上变成了原来的二次方,也就是一次可以跨很多位置进行检测

        1.3双重散列

        先用一个哈希函数计算出一个存储位置,如果位置白占用,在用第二个散列函数,直到找到空闲位置为止

2.链表法

        我们首先把数组下标的每一个位置我们可以称之为“桶(bucket)”,每一个桶通常会对应一条链表,所有的散列值相同的元素我们放到这个桶对应的链表里面

 HashMap就是采用这样的方式存储数据的

 HashMap的源码讲解

整个HashMap采用了哈希表 + 单链表 + 红黑树结构来进行设计 

常见属性

 /**
     * The default initial capacity - MUST be a power of two.
     * 默认的初始化容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     * 最大的hash表的容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;


/**
     * The load factor used when none specified in constructor.
     * 加载因子,可以这样理解
     * 当表里面的数据达到了75%的时候,就要触发扩容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;


 /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     * 进行树化的阈值,默认某个桶结点数大于8进行树化
     * 但是还是有个条件是,整个桶的所有结点满足最小的树化容量(MIN_TREEIFY_CAPACITY = 64)
     */
    static final int TREEIFY_THRESHOLD = 8;


 /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     * 链化的阈值,当结点数小于6时,红黑树会退化成单链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

/**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     * 树化一个条件:表的最小容量数目
     */
    static final int MIN_TREEIFY_CAPACITY = 64;


/**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     * 每一个结点
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

这里先来说jdk1.8的源码

初始化对象的几个构造函数

/**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        //上面几个都是一些常规的安全检查
        this.loadFactor = loadFactor;//赋值你的负载因子
        //这个函数返回的阈值是一个最接近当前初始容量的二次方数
        this.threshold = tableSizeFor(initialCapacity);//返回一个阈值
        //需要注意一点,调用这个构造方法的时候
        //还没有给我们返回容量,也就是这个表还没有被初始化
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     * 还是调用上面那个构造函数
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     * 给了一个默认的负载因子
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     * 一样给一个默认的负载因子
     * 但是这里会调用一个putMapEntries方法进行数据的追加
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

/**
     * Implements Map.putAll and Map constructor.
     *
     * @param m the map
     * @param evict false when initially constructing this map, else
     * true (relayed to method afterNodeInsertion).
     * 当使用HashMap(map)初始化一个对象的时候,会进入这个构造方法
     * 这个方法会计算出一个阈值,这个阈值一般和你传入的map大小是有关系的
     * 然后在
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            //表为null的时候,计算一个扩容阈值
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                        (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold) //原始阈值肯定为0,进来
                    threshold = tableSizeFor(t);//变成当前值最近的二次方,这个数组大小和s大小有关系
            }
            else if (s > threshold)
                resize();//表如果不为空,容量大于了阈值,就进行扩容

            //循环添加数据
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                //内部还是调用了putVal开始添加数据
                putVal(hash(key), key, value, false, evict);
            }
            //上面第一次进来,也会先算阈值,然后进行循环调用putVal进行数据的添加
        }

    }

这里就得引入一个问题,就是哈希表在什么时候进行初始化,上面看了一下构造函数,在初始化的时候new HashMap()与new HashMap(capacity,factor)他们其实都没有创建一张哈希表,只是给我们创建了一个负载因子(空参构造只会创建负载因子)和阈值,他们初始化哈希表都是在调用put方法添加数据的时候进行的,那我们在看new HashMap(Map)这个构造函数,它虽然也会给我们先算一个阈值出来,但是后面会调用一个for循环调用putVal方法进行数据添加,这个时候就会初始化一张表

如果说非要问,哈希表是在什么时候进行初始化的?

我们可以这样回答,空参构造和带容量的构造函数初始化一个对象的时候,没有初始化表,是在调用put方法的时候进行初始化的,而我们利用map集合进行初始化一个对象的时候,它是会给我们初始化表的,但是本质也是调用了putVal方法

构造对象看完了,下面就来看put和putVal这两个初始化一张表的方法

其实put方法内部就是调用了putVal进行一个数据的添加,下面我们看一下表的初始化过程

这里面有putVal方法的详细注释

下面我引入了完整的put方法

我们可以这样来看

第一次put数据会先进入下面这部分代码里面

然后去调用扩容方法reszie,那么进入到resize之后,会直接干到if (oldTab != null)的部分,我把这部分代码贴过来

/**
     *
     *扩容的函数
     * @return the table
     * 先来分析一下他会进入扩容的一些情况
     * 1.第一次put的时候,会调用putVal方法,然后内部,会调用resize()方法进行初始化一个哈希表
     *阈值对于HashMap(initialCapacity, loadFactor)来说会直接根据capacity算出一个阈值,new一个构造对象的时候就会算出来
     *对于HashMap()来说,是在第一次初始化算阈值,也就是在第一次put的时候计算阈值
     *
     *
     */
    final Node<K,V>[] resize() {
        //oldTab:引用扩容前的哈希表
        //这个表对于所有构造函数第一次put进来的时候来讲是null
        Node<K,V>[] oldTab = table;

        //为null长度自然就为0,否则返回之前表的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //拿到当前对象的阈值
        int oldThr = threshold;
        //新长度,新的阈值
        int newCap, newThr = 0;


        //这一块计算新的长度与阈值,分为不同的块来看
        //原来的表有长度,那么肯定不是new HashMap(),因为它初始化的时候,不可能有长度
        //那么也不是new HashMap(cap,factor)这个,这个只会根据cap计算一个2次方的阈值,也没有长度
        //考虑为第一次为初始化map的时候,new HashMap(map),这个也不会有长度,但是会根据map的大小
        //计算出来一个阈值,这个阈值也是2的次方数目,然后就在添加数据进行表的初始化,也没有长度
        if (oldCap > 0) {//第一把初始化时构造函数的时候,都不可能走进oldCap > 0 这条线

            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

        //这里就是oldCap等于0的情况,但是它又是一个有阈值的情况
        //什么情况在初始化的时候会有阈值呢?那么肯定就是new HashMap(cap,factor), new HashMap(map)都有
        //那么他们就会走这初始化,把原来的阈值变为新的长度,但是newThr在这还没有初始化
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

        //下面就是oldCap等于0,oldThr=0
        //就是空参new HashMap(),初始化的时候无长度,无阈值
        else {               // zero initial threshold signifies using defaults
            //问题:HashMap无参构造调用的时候,什么时候会初始化一张哈希表
            //在第一次put的时候,会初始化一表,他会走oldCapacity=0与oldThreshold=0的情况
            //把长度变为默认长度16,把阈值变为默认负载因子*默认容量=0.75*16=12
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }


        //新的阈值为0,经过上面,只有new HashMap(cap,factor)与new HashMap(map)新的阈值为0
        if (newThr == 0) {
            //这里对于HashMap(map),它的负载因子也是0.75
            float ft = (float)newCap * loadFactor;//算新的阈值 = 老的阈值(因为之前是老的阈值给到了新的长度)*他们传入的负载因子
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }

        //然后把当前对象的阈值赋值上去,什么new初始化都是在这里赋值新的阈值
        threshold = newThr;


        //上面做完了,该初始化的都初始化完了
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//建立一个新的哈希表,如果是初始化,就是第一次在这里建立一张表
        table = newTab;//然后赋值给table;
        //以上初始化到这一步就全部结束掉,因为老的表是没有数据的
        //对所有的new HashMap(....)都是一样的结果,也就是在put里面第一次调用resize()走到这就结束了
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

 下面是完整的puVal方法注释

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab:引用当前hashMap的散列表
        //p: 当前散列表的元素
        //n: 散列表数组的长度
        //i: 根据上面的hash进一步计算出的数据存放在哈希表的位置
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        //不管用什么构造函数进行初始化一个HashMap
        //他们都没有初始化一张hash表
        //所以都会进入到这个里面,进行扩容,并且把长度赋值给n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;




        //按照分析
        //所有的HashMap构造函数都会在上面一步进行一个扩容
        //与其说是扩容,不如说是初始化一张哈希表
        //然后在进入下面的判断
        //上面哈希表就已经被初始化出来了
        //i = (n - 1) & hash 利用之前hash码算一个哈希位置出来
        //并把当前元素赋值给p
        //如果当前值为NULL
        if ((p = tab[i = (n - 1) & hash]) == null)//这个里面的n在每一次扩容之前都不会变,换句话,如果每次的key一样,并且没有主动去修改此对象的hashCode,大多返回的i都一样
            //直接把值放进去
            tab[i] = newNode(hash, key, value, null);


        //如果这个位置已经有数据了
        //注意一个问题是位置有数据,并不代表key就相等
        //这里很可能就是不同的key生成后几位相同hash码
        //从而算出来的位置是一样的
        //这个else大体的判定条件是!= null
        else {
            //e: 找到一个与插入key相等的结点,这里也就是key相等的结点
            //k: 一个临时的k
            Node<K,V> e; K k;

            //上面if判断的时候就已经把当前元素赋值给了p
            //当前元素hash码相等 && key相等
            //其实这里完全可以理解为key相等,计算的hash码就是相等的,自然i索引位置相等
            //这里也就是在第一个位置,也就是表的桶位,进行了判定
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//key相等的索引结点赋值给e,后面需要去把e的value替换掉

            //上面没进去说明key不一样
            //然后我们判断下面当前结点是不是一棵红黑树
            //如果是红黑树,就按照红黑树的方式插入进去
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

            //上面没有相等的key替换,也没有被树化
            //那就是链表处理
            else {
                //走一个循环插入
                //jdk1.8是尾插,所以要走指针到链表的最后一个位置
                for (int binCount = 0; ; ++binCount) {
                    //如果当前结点的.next等于null的时候,表明已经走到了链表的最后一个结点
                    if ((e = p.next) == null) {
                        //直接把这个结点连接到当前结点的next后面
                        p.next = newNode(hash, key, value, null);
                        //binCount记录了这个链表走了多少次
                        //TREEIFY_THRESHOLD=8这是树化的条件
                        //当binCount = 7的时候,也就是达到了树化的条件,循环了八次
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

                    //如果在循环的过程中找到key相等的位置,就需要替换值
                    //因为链表里面可能也存在相等的key啊
                    //直接跳出这个循环就行
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;//这就是让p与e交替往下面轮替
                }
            }

            //替换e结点
            if (e != null) { // existing mapping for key
                V oldValue = e.value;//保留老值,等会返回
                //onlyIfAbsent表示存在某个key就不插入这里默认是false
                //不为false,进入,表示已经存在某个key值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }


        ++modCount;//表的改变次数,包含了添加操作,删除操作等改变
        //哈希表的长度大于了阈值,就要进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

实际的扩容分析

扩容会走下面这条路

如果表里面的数量大于了阈值,就开始扩容,下面是resize扩容部分的代码分析

 final Node<K,V>[] resize() {
       //拿到老表
        Node<K,V>[] oldTab = table;

        //老表这里,肯定会拿到长度
        //这里说一下如果是空参构造,在上面初始化put之后长度就是16(值默认)
        //如果是带容量的构造函数,在上面初始化后容量是2次方的值,至于具体是多少
        //这个要根据你传入的initialCapacity决定,因为他会先算一个阈值,然后在下次
        //初始化表的时候,把这个阈值当做容量传递过去,然后在根据你的容量和传入的负载因子
        //算出一个新的阈值
        //而对于利用map集合进行初始化一个HashMap的时候,它的长度取决于map集合
        //的长度,因为他是利用map集合的长度来算出了一个2次方的值作为阈值,下次初始化
        //一张表的时候,在把这个阈值拿进来作为表的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //拿到当前对象的阈值,也是有的
        int oldThr = threshold;
        //新长度,新的阈值初始化为0
        int newCap, newThr = 0;


        //这里就不是初始化动作了
        //这里全部都是扩容,所以都走这条if路线,容量全部都是大于0的
        if (oldCap > 0) {
            //直接封顶了,也就是扩容不了,直接返回老表
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //重点在这个位置
            //新的表的长度一定会扩展为原来表长度的2倍
            //新的阈值这里分了一个情况:只有老表的长度大于了默认的容量
            //才会把新的阈值扩展为原来的2倍
            //那么对于空参构造来说,这里肯定就是新的长度与阈值全都变为原来的2倍
            //因为它的初始化长度就是16默认值
            //那么什么情况下会出现oldCap小于DEFAULT_INITIAL_CAPACITY这个容量呢
            //那么就是当用另外两个构造函数初始化一个哈希表的时候
            //你传入的capacity与map集合的大小非常小的时候
            //这里拿capacity来说,当它的值是8以下的时候
            //他会先算出来一个初始阈值,这个初始阈值在第一次初始化哈希表的时候
            //就会给这张表作为初始容量,假设是8以下,比如4,那么初始容量算出来就是8
            //阈值就是,假设负载因子传入的是0.5,阈值=8*0.5=4也就是olcCap=8,oldThr=4
            //下一次进来扩容长度变为16,因为老的长度是8<16,所以它的阈值扩容不会直接走
            //什么下面的两倍,而是跳到下面我标注(1)的位置
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

        
        else if (oldThr > 0) 
            newCap = oldThr;

        
        else {               

            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }


        //(1)
        //如果到这个位置
        //那么肯定是oldCapacity长度<16
        if (newThr == 0) {
            //它按照新长度和它自己的负载因子
            //上面我们假设了负载因子是0.5,新长度是16 
            //那么乘以新的负载因子就是16*0.5= 8;
            //我们会注意到阈值也还是变为了原来的2倍
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }

        threshold = newThr;


        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建一张新表
        table = newTab;//然后赋值给table;
        //扩容老表肯定不等于null的,所以必走下面这条路
        if (oldTab != null) {
            //遍历整个表,我们重点在于对原来表的数据的迁移
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;//中间的移动变量
                //当这个位置有值了,让e结点去指向这个位置
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;//把当前位置的数据变为null
                    //如果当前位置下面没有结点
                    //说明了一个问题就是当前结点是一个唯一的key
                    //也就是说按照之前的hash算出来的位置没有重复值
                    //那么直接算出一个位置,放到新表里面
                    //这个位置不可能重复
                    if (e.next == null)
                        //算出新的位置然后放值
                        newTab[e.hash & (newCap - 1)] = e;

                    //如果有结点,然后就判断有没有被树化
                    else if (e instanceof TreeNode)
                        //如果是一棵红黑树,那么就按照红黑树处理
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                    //也不是红黑树,那么就按照链表处理
                    //需要将这个桶的链表进行迁移
                    else {
                        //这里分为迁移到低位链表还是高位链表
                        //低位链表原来的链表长度之内
                        //高位链表扩展出来的链表长度之内
                        Node<K,V> loHead = null, loTail = null;//低位链表
                        Node<K,V> hiHead = null, hiTail = null;//高位链表
                        Node<K,V> next;//中间的移动指针

                        do {
                            next = e.next;
                            //如果最高位为0的数据,依旧放在原来的表的位置
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    //这个只会进来一次
                                    loHead = e;//当前loHead是表头,到时候挂到桶位置上的表头
                                else
                                    loTail.next = e;
                                loTail = e;//loTail会指向e的上一个结点
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;//同理
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);


                        if (loTail != null) {
                            //这里刚好是这个链表的最后一个数据
                            //它的next直接就是null
                            //假如这里不是当前链表的最后一个数据,他可能下面还有数据
                            //因为他已经要搬到新位置上去
                            //所以直接把这里的next直接挂为null
                            loTail.next = null;//低位表最后一个位置next挂为null
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            //同理
                            hiTail.next = null;
                            //扩展到了新的位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }

                }

            }
        }
        return newTab;
    }

简单说一下如果表的长度达到了最大值处理方式

这里还得重点来说一下关于链表位置的迁移计算

那么上面就知道了e.hash & oldCap结果如果为0的情况,那就是最高位一定为0的情况,最高位如果为1,它永远不会出现为0的情况,那么继续往下面看

 下面我们就可以拿到当前的结点的hash值我们去计算一下位置

很明显,高位为0的结点的hash值,它的位置只与后面四位有关,换句话说说,上面的hash值不管是与老的长度位比较还是新的长度位比较,结果都一样,所以他们全都放在低位链表的位置,换句话,这几个结点的位置全都不动

那么我们现在去看一下扩展到高位的链表

 这里也就说明了,为什么高位链表要扩展到这个位置的原因

 几个常见问题

 

 手写一个简单的哈希表

package com.pxx.test.hashmap.myhashmap1;

public class MyHashMap<K, V> {
    //默认容量
    private static final int DEFAULT_CAPACITY = 16;

    //容量的最大值
    private static int MAXIMUM_CAPACITY = 1 << 30;

    //默认负载因子
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //哈希表的容量
    private int capacity;

    //负载因子
    private float loadFactor;

    //阈值
    private int threshold;

    //当前哈希映射中键值对的数量
    private int size;

    //存储键值对的数组,每一个元素都是一个链表头结点
    private Node<K, V>[] table;

    public MyHashMap() {
        //使用默认的容量和负载因子创建哈希映射实例
        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    /**
     * //带参,使用指定容量和负载因子创建哈希映射实例
     * @param capacity
     * @param loadFactor
     */
    public MyHashMap(int capacity, float loadFactor) {
        this.capacity = capacity;
        this.loadFactor = loadFactor;
        this.table = new Node[capacity];//这里已经把数据表给初始化了
        //把阈值在初始化的时候也给上
        this.threshold = (int) (capacity * loadFactor);
    }

    /**
     * 内部静态节点类,表示哈希映射中的结点
     * 这个结点的设计除了键值以外
     * 应该还包含一个指向下一个node的指针,
     * 因为当某个索引算出来
     * 但是又被占用了之后,后面就要接链表了,不停的往下指向
     */
    private static class Node<K, V> {
        K key;
        V value;
        Node<K, V> next;//每个结点后面可能变成一个链表,所以必须有一个next指针
        int hash;//给一个hash值用于后面进行扩容操作

        public Node(int hash , K key, V value, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

    //这里提供一个哈希函数,计算哈希索引的值
    private int hash(K key) {
        return key.hashCode();
    }

    //完善添加方法
    //这里面采用了链接的方法解决了冲突
    //如果有相同的键进来,就要被后面的值进行更新
    public V  put(K key, V value) {
        int index = hash(key);//拿到哈希值
        //拿到表的长度
        int n = table.length;
        //当前这个结点就是数组的桶,也就是链表的头结点
        Node<K, V> node = table[index & (n - 1)];//从这个当前哈希索引里面拿一个节点看看
        //如果不等于null
        //表明这个位置有值,我们就要插入到链表里面去
        //那么是头插还是尾插呢
        //在JDK源码里面1.8是尾插 1.8之前是头插
        //那我们这里实现尾插
        if (node != null) {
            while (node.next != null) {
                //还必须知道如果存在相同的键,那么就需要把值进行更新
                //但是如果更新了我们需要返回老值
                if (node.key.equals(key)) {
                    V oldVal = node.value;
                    //然后替换值
                    node.value = value;
                   return oldVal;
                }
                node = node.next;//最后跳出循环会移动到最后一个指针的位置
            }

            Node<K,V> newNode = new Node<>(index,key, value, null);
            node.next = newNode;
        } else {
            //直接放到这个位置就行了
            table[index] = new Node<>(index, key, value, null);
        }

        //除了替换老值,不进入size的计算之内
        //其他的都要进入到size的计算之内
        size++;
        //看看是否达到了负载因子
        //默认负载因子
        //private static final float DEFAULT_LOAD_FACTOR = 0.75f;
        //这里的负载因子默认是75%,比如容量是16 * 0.75 = 12
        //如果达到了12,这个哈希表的容量也就达到了一个阈值
        //负载因子用于控制哈希表的填充程度
        //当负载因子设置得较小时,哈希表会更早地进行扩容,以减少冲突和提高性能
        if (size > threshold) {
            resize();
        }
        return null;//没有新的值
    }

    //我限定的是只要进入这个方法就是扩容
    //表的初始化动作没有在这里进行
    private Node<K,V>[] resize() {
        Node<K, V>[] oldTab = table;
        //拿到老表的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //拿到阈值
        int oldThr = threshold;
        //新长度,新阈值
        int newCap = 0, newThr = 0;

        if (oldCap > 0) {
            //容量如果是最大值,封顶
            //阈值给个最大值,返回老表
            if (oldCap >= MAXIMUM_CAPACITY) {
                //直接把阈值给到最大
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }

            //这里我们直接扩展两倍
            newCap = oldCap << 1;
            newThr = oldThr << 1;
        }

        //新的阈值赋值
        threshold = newThr;
        //创建新表,注意强制类型转换,编译器无法强制类型转换成泛型数组
        Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
        table = newTab;

        if (oldTab != null) {
            Node<K, V> e;//给一个中间结点,等会用来移动
            //遍历老表
            for (int j = 0; j < oldCap; ++j) {
                e = oldTab[j];
                //当前结点有值
                if (e != null) {
                    //把老表的位置释放掉
                    oldTab[j] = null;

                    //下面去判断是直接放值,还是进行链化迁移
                    //没有一个重复的索引位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else {
                        //考虑为链表的迁移
                        Node<K, V> loHead = null, loTail = null;
                        Node<K, V> hiHead = null, hiTail = null;
                        Node<K, V> next;

                        do {
                            next = e.next;
                            //最高位如果为0,放原来的位置
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;//用loTail不断去拼接这个链表
                                loTail = e;//loTail不停往下面走,然后会卡在其中某一个位置
                            } else {
                                //最高位不等于0的时候,考虑为1的时候
                                //会迁移到原来的位置 + 最高位数
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                            //然后整体把e向下移动
                            e = next;
                        } while (e != null);//直到结束大家都链好了各自的数据

                        //上面循环完了之后,要把链表挂到相应的位置上去
                        //这里就是真正的迁移

                        if (loTail != null) {
                            //这里刚好是这个链表的最后一个数据
                            //它的next直接就是null
                            //假如这里不是当前链表的最后一个数据,他可能下面还有数据
                            //因为他已经要搬到新位置上去
                            //所以直接把这里的next直接挂为null
                            loTail.next = null;//低位表最后一个位置next挂为null
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            //同理
                            hiTail.next = null;
                            //扩展到了新的位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

    
}

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值