HashMap源码解读,面试常考知识点速通!

前言

大家好,我是chowley,前几天面试的时候被问到了HashMap的内容,

今天我就来做一个详细的总结,帮助大家快速上手HashMap的关键点!

HashMap

在Java集合框架中,HashMap是一种常见且重要的数据结构,广泛应用于各种场景。了解其内部实现原理,不仅有助于大家更好地使用,也可以帮助我们了解面试中问题的关键点。

1. 基本概念

1.1 键值对映射

HashMap是一种通过键值对映射关系存储数据的容器。每个键对应一个值,通过键可以快速查找对应的值。

1.2 内部结构

HashMap的内部结构包括一个数组和一些链表/红黑树。数组的每个元素称为“桶”,每个桶存储着一个链表(或红黑树)的头节点。键值对通过哈希算法映射到具体的桶。

2. 哈希算法

2.1 哈希码获取

HashMap中的键通过哈希算法映射到具体的桶。键的哈希码通过调用键的hashCode方法获得。

2.2 桶的位置计算

通过一系列位运算操作,HashMap计算键的哈希码在数组中的位置,确定存储的桶。

3. 冲突解决

由于哈希算法可能导致不同的键映射到同一个桶,产生哈希冲突。HashMap通过链表和红黑树来解决冲突。当桶中的链表长度达到一定阈值时,链表会转换成红黑树,以提高查询效率。

4. 常用方法源码分析

4.1 构造方法

    // 无参构造方法
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 使用默认负载因子
    }

    // 接受另一个Map作为参数
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    // 指定初始容量大小
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    // 指定初始容量大小和负载因子
    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;
        // 初始容量暂时存放到 threshold ,在resize中再赋值给 newCap 进行table初始化
        this.threshold = tableSizeFor(initialCapacity);
    }

HashMap的构造方法主要设置了默认的初始容量、最大容量、负载因子等参数,以及初始化了一些阈值。

4.2 put方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;

    // 如果数组为空或长度为0,则进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    // 计算桶的位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

    // 如果桶中已经有元素
    else {
        Node<K,V> e; K k;

        // 判断桶中第一个元素的哈希值、键是否与当前元素相同
        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) {
                // 如果链表中没有找到相同的键,则将新节点插入到链表的末尾
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);

                    // 如果链表长度达到阈值,将链表转化为树形结构
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }

                // 如果找到相同的键,则更新值
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;

                p = e;
            }
        }

        // 如果找到相同的键,更新值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }

    // 结构性修改计数器增加
    ++modCount;

    // 如果元素个数超过阈值,进行扩容
    if (++size > threshold)
        resize();

    afterNodeInsertion(evict);
    return null;
}

put方法根据键的哈希值计算桶的位置,然后根据当前桶中的元素情况,选择合适的方式进行插入,包括链表插入和红黑树插入。如果插入后元素个数超过阈值,触发扩容操作。

4.3 get方法

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

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 &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;

        // 如果桶中是树形结构,调用树形查找方法
        if ((e = first.next) 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;
}

get方法根据键的哈希值计算桶的位置,然后根据桶中的元素情况,选择合适的方式进行查找,包括链表查找和红黑树查找。

4.4 resize方法

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;

    // 如果旧数组不为空,进行扩容操作
    if (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
    }
    // 如果旧数组为空,使用默认容量和阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    // 如果新的阈值为0,计算新的阈值
    if (newThr == 0) {
        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;

    // 如果旧数组不为空,将元素转移到新数组
    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;
}

resize方法用于对HashMap进行扩容。它根据当前的数组容量和扩容阈值,计算新的容量和新的扩容阈值,并创建新的数组。然后,将旧数组中的元素根据其哈希值重新分配到新数组中。

总结

通过对HashMap源码的简要解读,我们可以了解到其内部实现采用数组+链表/红黑树的方式,通过哈希算法将键映射到具体的桶中。在插入元素时,会根据桶中的结构选择合适的方式进行插入。

同时,HashMap具有动态扩容机制,当元素个数超过一定阈值时,会触发扩容操作。深入了解这些源码细节有助于更好地理解HashMap的工作原理。

好了,以上就是本文的全部内容,如有问题欢迎留言讨论。

我是chowley,一个专注互联网技术和软件质量保障领域的博主,我们下次再见!

欢迎点赞、评论、收藏,it's important for me.

欢迎点赞、评论、收藏,it's important for me.

欢迎点赞、评论、收藏,it's important for me.

QALog 文章被收录于专栏

记录了chowley的一些质量管理博文

全部评论

相关推荐

07-16 19:10
门头沟学院 Java
点赞 评论 收藏
分享
XSS(跨站脚本攻击)指的是攻击者通过注入恶意脚本代码来攻击用户的网站或应用程序。攻击者通常会在用户输入的内容中注入脚本代码或恶意链接,并使其他用户在浏览该网页时执行该代码,从而窃取用户信息、篡改页面内容或进行其他恶意行为。为了防止&nbsp;XSS&nbsp;攻击,可以采取以下措施:https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mg58Em&amp;amp;uuid=bb7c8339fae245258616366baf13e19e输入验证和过滤:对用户输入的数据进行验证和过滤,确保只接受符合预期格式和内容的数据。可以使用合适的库或框架进行输入验证,如限制特殊字符、转义特殊字符等。输出编码:在将用户输入内容(包括用户输入的文本、URL&nbsp;或&nbsp;HTML)输出到页面时,将其进行合适的编码处理,以防止脚本代码被解析执行。常用的编码方式有&nbsp;HTML&nbsp;实体编码和&nbsp;URL&nbsp;编码。使用&nbsp;HTTP-only&nbsp;Cookie:将敏感信息(如用户身份验证的&nbsp;Cookie)标记为&nbsp;HTTP-only,这样脚本无法通过&nbsp;JavaScript&nbsp;访问该&nbsp;Cookie,减少&nbsp;XSS&nbsp;攻击的威力。使用内容安全策略(CSP):通过配置内容安全策略,限制页面加载资源的来源,只加载可信的资源,以防止恶意脚本的注入。定期更新和维护:及时更新和修复应用程序的漏洞和安全问题,以提高应用程序的安全性。教育用户:提高用户的安全意识,教育用户不要轻信可疑的链接或提供个人敏感信息,从而降低受到&nbsp;XSS&nbsp;攻击的风险。
点赞 评论 收藏
分享
1 6 评论
分享
牛客网
牛客企业服务