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 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[] tab; Node 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 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)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 e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node getNode(int hash, Object key) {
    Node[] tab; Node 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)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[] resize() {
    Node[] 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[] newTab = (Node[])new Node[newCap];
    table = newTab;

    // 如果旧数组不为空,将元素转移到新数组
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node 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)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node 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,面试,职场和发展)