java基础篇之HashMap

总结

  • 哈希表的主干是数组
  • 复写时有返回值
java7 HashMap

在这里插入图片描述

  • 整体来说,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
  • capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍(ConcurrentHashMap )
  • loadFactor:负载因子,默认为 0.75。
  • threshold:扩容的阈值,等于 capacity * loadFactor
java7 put源码
public V put(K key, V value) {
    // 当插入第一个元素的时候,需要先初始化数组大小
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 如果 key 为 null,感兴趣的可以往里看,最终会将这个 entry 放到 table[0] 中
    if (key == null)
        return putForNullKey(value);
    // 1. 求 key 的 hash 值
    int hash = hash(key);
    // 2. 找到对应的数组下标
    int i = indexFor(hash, table.length);
    // 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,
    //    如果有,直接覆盖,put 方法返回旧值就结束了
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    // 4. 不存在重复的 key,将此 entry 添加到链表中,细节后面说
    addEntry(hash, key, value, i);
    return null;
}

计算具体数组位置 详见java 与 运算

static int indexFor(int hash, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return hash & (length-1);
}

addEntry 添加节点到链表中,主要判断是否需要扩容

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 扩容,后面会介绍一下
        resize(2 * table.length);
        // 扩容以后,重新计算 hash 值
        hash = (null != key) ? hash(key) : 0;
        // 重新计算扩容后的新的下标
        bucketIndex = indexFor(hash, table.length);
    }
    // 往下看
    createEntry(hash, key, value, bucketIndex);
}
// 这个很简单,其实就是将新值放到链表的表头,然后 size++
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

扩容resize,双倍扩容,并迁移,比如原16位的数组
table[0] --> table[0] + table[16]
table[1] --> table[1] + table[17]

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    // 新的数组
    Entry[] newTable = new Entry[newCapacity];
    // 将原来数组中的值迁移到新的更大的数组中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

相对而言,java7 get方法比较简单
1.根据 key 计算 hash 值。
2.找到相应的数组下标:hash & (length - 1)。
3.遍历该数组位置处的链表,直到找到相等(==或equals)的 key

为什么扩容是2的次幂
if ((p = tab[i = (n - 1) & hash]) == null)
  • 这里返回的是key的hashcode跟初始容量-1做与运算
  • 首先length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;
  • 其次,length为2的整数次幂的话,为偶数。这样length-1为奇数,奇数的最后一位为1,这样便保证了h&(length-1)的最后一位为0,也可能为1(这取决于h的值),即与后的结果可能为偶数也可能是奇数。这样便可以保证散列的均匀性,
  • 而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间。所以,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列
java8 HashMap

在这里插入图片描述

图来自https://javadoop.com/post/hashmap

  • java8与java7不同之处,利用了红黑树,所以其构成为 数组+链表+红黑树
  • java7中,根据hash值可以快速找到数据的下标,但是如果链表很长的话,需要一个一个比较才能找到,时间复杂度取决于链表的长度,为O(n)
  • java8中,当链表中元素到达8个,会将链表转换为红黑树,时间复杂度降低为 O(logN)
  • java7使用Entry,java8使用Node,Node只用于链表,红黑树使用TreeNode
  • 初始值16,默认加载因子0.75,put过程如下:
  1. 第一次put时(node数组为空),通过resize()从null初始化到16,定位到具体的数组下标,如果没有值,初始化node,直接放入value即可,新插入的值,判断是否超过阈值
  2. 如果该位置有值,比较该位置的第一个key与当前key是否相等,如果相等,到第3步,如果不相等,判断该节点是否为红黑树:
    是——调用红黑树的插入方法
    否——插入到链表最后 – 如果新插入的值是第8个,触发转换红黑树操作;如果在链表中找到相同的key,也到第3步
  3. 对于相同的key,覆盖旧值,并且返回
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links 
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
		//Entry 又继承自 HashMap.Node……
java8 put源码
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第四个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度
    // 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 ,自定义的容量,则会得到大于该数字的第一个2的次幂,比如15->16,44->64
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
    if ((p = tab[i = (n - 1) & hash]) == null) //& 二进制运算
        tab[i] = newNode(hash, key, value, null);

    else {// 数组该位置有数据
        Node<K,V> e; K k;
        // 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果该节点是代表红黑树的节点,调用红黑树的插值方法,本文不展开说红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 到这里,说明数组该位置上是一个链表
            for (int binCount = 0; ; ++binCount) {
                // 插入到链表的最后面(Java7 是插入到链表的最前面)
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // TREEIFY_THRESHOLD final类型,为 8,如果新插入的值是链表中的第 8 个
                    // 会触发下面的 treeifyBin,也就是将链表转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果在该链表中找到了"相等"的 key(== 或 equals)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
                    break;
                p = e;
            }
        }
        // e!=null 说明存在旧值的key与要插入的key"相等"
        // 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //++modCount用于检测遍历map时候,map是否被其他线程修改
    ++modCount;
    // 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
java8 get源码

get操作相对简单,过程如下:
1.计算key的哈希值,定位到数组下标
2.如果该数组的第一个key就是我们要找的,GG,否则继续
3.如果是红黑树node,走红黑树的get方法
4.遍历链表,找到为止

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 判断第一个节点是不是就是需要的
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 判断是否是红黑树
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);

            // 链表遍历
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
时间复杂度
  • O(1): 表示算法的运行时间为常量
  • O(n): 表示该算法是线性算法
  • O(1)就是最低的时空复杂度了,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。 哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(不考虑冲突的话)
为什么hashmap能保证O(1)

Hashtable的时间复杂度最好是O(1)但是最差是 O(n) 最差的时候也就是hashtable中所有的值的hash值都一样,都分配在一个entry里面,当然这个概率跟中彩票的概率相差不大

注意

HashMap,在使用put的时候,如果添加的是对象的话,所存储的都是对象的引用(地址)

疑问

在这里插入图片描述

参考:https://javadoop.com/post/hashmap
https://blog.csdn.net/dam454450872/article/details/80376661

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值