瞅瞅源码之HashMap[jdk1.8]

本文详细解析了HashMap的工作原理,包括其内部结构、初始化过程、增删查改操作的实现细节,以及JDK8引入的红黑树优化策略。探讨了如何减少碰撞、合理设置容量和负载因子,以提高性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap

JAVADOC

基于实现Map接口的哈希表。
这个实现提供了所有可选的映射操作,并允许null值和null键
(HashMap类大致相当于Hashtable,只是它是不同步的,并且允许为空。)

该类不保证映射的顺序;特别是,它不能保证顺序在一段时间内保持不变。

这个实现为基本操作(get和put)提供了固定时间的性能,假设散列函数正确地将元素分散到各个桶中。

集合视图的迭代需要与HashMap实例的“容量”(桶的数量)及其大小(键值映射的数量)成比例的时间。因此,如果迭代性能很重要,那么++不要将初始容量设置得太高(或负载因子太低)是非常重要的++。

一个HashMap的实例有两个影响其性能的参数:初始容量(initial capacity)和负载因数(load factor)。

capacity 是哈希表中的桶数,初始容量就是创建哈希表时的容量。

load factor是哈希表在容量自动增加之前允许获得的满容量的度量。

当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表是rehashed(即重新构建内部数据结构)此时哈希表的新桶数大约是旧桶数的两倍

一般来说,默认的负载因子(.75)在时间和空间成本之间提供了很好的权衡

更高的值减少了空间开销,但是增加了查找成本(反映在HashMap类的大多数操作中,包括get和put)。

在++设置map的初始容量时,应该考虑map中条目的期望数量及其负载因子,从而最小化rehash操作的数量++。

如果初始容量大于条目的最大数量除以负载因子,则不会发生重排操作。

如果要将许多映射存储在HashMap实例中,那么使用足够大的容量创建映射将比根据需要执行自动散列来增长表更有效。

注意,使用多个具有相同{@code hashCode()}的键肯定会降低任何散列表的性能。为了减轻影响,当键是{@link Comparable}时,这个类可以使用键之间的比较顺序来帮助打破联系。

注意,这个实现不是同步的

如果多个线程同时访问一个散列映射,并且至少有一个线程在结构上修改了映射,则必须在外部同步。 (结构修改是指增加或删除一个或多个映射的操作;仅仅更改与一个实例已经包含的键相关联的值并不是结构修改。)

这通常是通过对一些自然封装了映射的对象进行同步来实现的。

如果不存在这样的对象,那么应该使用{@link Collections#synchronizedMap Collections.synchronizedMap}方法。

这最好在创建时完成,以防止意外的不同步访问map:

Map m = Collections.synchronizedMap(new HashMap(…));

这个类的所有“集合视图方法”返回的迭代器是快速失败的:

如果在迭代器创建后的任何时候,以任何方式(除了通过迭代器自己的删除方法)对映射进行结构修改,迭代器将抛出{@link ConcurrentModificationException}。
因此,在面对并发修改时,迭代器会快速而干净地失败,而不是在将来某个不确定的时间冒任意的、不确定的行为的风险。

注意,不能保证迭代器的快速故障行为,因为通常来说,在存在非同步并发修改的情况下,不可能做出任何严格的保证。

故障快速迭代器在最大努力的基础上抛出ConcurrentModificationException。
因此,编写一个依赖于这个异常的正确性的程序是错误的:迭代器的快速故障行为应该只用于检测bug。

NO BB , SHOW CODE

STATIC AREA

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/*
* 使用树而不是列表的容器计数阈值。(树型阈值)
* 当向至少有这么多节点的bin中添加元素时,bin将被转换为树。
* 该值必须大于2,并且应该至少为8,以便与树节点移除时关于收缩后转换回普通桶的假设相吻合。
* 树型阀值就是当链表长度超过这个值时,将 Node 的数据结构修改为红黑树,以便优化查找时间,
* 默认值为8
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 在调整大小操作期间取消(拆分)存储的存储库计数阈值。
* 应小于TREEIFY_THRESHOLD,且最多6个网格进行收缩检测下去除。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 可以对容器进行treeified的最小表容量。
* (否则,如果一个bin中有太多节点,就会重新调整表的大小。)
* 至少4 * TREEIFY_THRESHOLD,以避免调整大小和treeification阀值之间的冲突。
* 要是还不明白,看 Q&A
*/
static final int MIN_TREEIFY_CAPACITY = 64;

//NODE是啥?就是这个
static class Node<K, V> implements Entry<K, V> {
        final int hash;
        final K key;
        V value;
        Node<K, V> next;//指向下一个node,形成node chain
    ...
}

//高位右移16位然后与操作,性能目前最佳
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

Fields

transient Node<K, V>[] table;
transient Set<Entry<K, V>> entrySet;
transient int size;
/*
* 此HashMap在结构上修改的次数。
* 结构修改是指改变HashMap中映射的数量,或者修改其内部结构(例如,重新散列)
* 此字段用于使HashMap的集合视图上的迭代器快速失效。(见ConcurrentModificationException)。
*/
transient int modCount;
/**
* 要调整大小的下一个大小值(容量*负载因子)。
* 序列化后,javadoc描述为true。
* 此外,如果还没有分配表数组,则该字段保留初始数组容量,或者为零,表示DEFAULT_INITIAL_CAPACITY。
*/
int threshold;
/*
* 注意:这里用了final做修饰,也就是负载因子一旦初始化之后就不可更改
*/
final float loadFactor;

Constructs

//HashMap是lazy load 的,
//这点通过构造函数中只会初始化几个设置参数,并没有初始化table可以看出来
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

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);
        }
        //initialCapacity 不可能无限的大
        if (initialCapacity > MAXIMUM_CAPACITY) {
            initialCapacity = MAXIMUM_CAPACITY;
        }
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        }
        //设置负载因子
        this.loadFactor = loadFactor;
        //设置下一次扩容需要设置的大小值,
        //这个tableSizeFor一定会返回一个不小于当前方法值的最小的2的n次方
        this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            //这里得if else 分支,只是判断了要新添加的map的大小是否超出当前map的阈值大小,
            //其实只能算是防御性校验,因为单纯判断m和当前阈值的大小不能完全反映出当前map是否需要扩容,因为并没有考虑当前map已经含有的数据
            //真实的扩容其实还是以for循环中putVal方法当中的判断  if (++size > threshold) {resize();}
            if (table == null) {
                float ft = ((float) s / loadFactor) + 1.0F;
                int t = ((ft < (float) MAXIMUM_CAPACITY) ? (int) ft : MAXIMUM_CAPACITY);
                if (t > threshold) {
                    threshold = tableSizeFor(t);
                }
            } else if (s > threshold) {//不管当前map中有多少数据,如果参数中的m的大小直接超出阈值,其实就可以直接扩容了
                resize();
            }
            //遍历添加
            for (Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
}

Public Method

put
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) {
        Node<K, V>[] tab;
        Node<K, V> p;       //table[i]位置的元素
        int n, i;           //n: table长度 ; i: 当前要设置的元素要归属的桶位置,也就是table的一个下标
        if ((tab = table) == null || (n = tab.length) == 0) {
            //初始化table,通过resize方法
            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)))) {
                //如果发现这个位置的元素key,和打算存储的元素的key一致,那么e指向p
                e = p; //注意 : 这里修改了e , p也会同步修改 ,其实e p 指向的是同一个内存地址,也就是实际指向一份数据
            } else if (p instanceof TreeNode) {
                //如果发现p已经是一个树节点元素了,那么尝试使用树节点的方式存储键值对
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            } else {
                //如果p不是一个树节点,那么必然就是一个链表节点了
                for (int binCount = 0; ; ++binCount) {//这种方式的写法,binCount的大小就是当前hash桶下链条的长度
                    if ((e = p.next) == null) {
                        //如果p的下一个节点是空的,那么直接创建一个节点让p.next指向此节点
                        p.next = newNode(hash, key, value, null);
                        //如果当前链条长度大于等于树化阈值,需要扩容或者将当前hash链条上的所有数据转化为树形结构
                        if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1st
                            treeifyBin(tab, hash);
                        }
                        //跳出循环
                        break;
                    }
                    //如果p的下个节点的key和打算存储的元素key一致,跳出循环
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        break;
                    }
                    //e作为p的下一个节点,如果不为空又不是和存储key一致,那么把将指针p重新指向原先的e,然后继续循环
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key : key 现有的映射
                V oldValue = e.value;
                //因为上边有三个分支可能对e的值做设定,所以这里加了这么个操作,保证value的值一定是方法参数中传递的value值
                if (!onlyIfAbsent || oldValue == null) {
                    e.value = value;
                }
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold) {
            //放进去一个元素,table中数量加一之后如果大于阈值,则进行扩容操作
            resize();
        }
        afterNodeInsertion(evict);
        return null;
}
get
    @Override
    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 tableLength; //
        K k;
        if ((tab = table) != null && (tableLength = tab.length) > 0 && (first = tab[(tableLength - 1) & hash]) != null) {
            //检查是否要找的是第一个节点,注意这里的验证方式 (哈希值 && (内存地址 || equals))
            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 while 的形式
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k)))) {
                        return e;
                    }
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
remove
    @Override
    public V remove(Object key) {
        Node<K, V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
    }
    
    final Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
        Node<K, V>[] tab;
        Node<K, V> firstHashNodeInHashArray;
        int tableLength, index;
        if ((tab = table) != null && (tableLength = tab.length) > 0 && (firstHashNodeInHashArray = tab[index = (tableLength - 1) & hash]) != null) {
            Node<K, V> removeNode = null, e;
            K k;
            V v;
            if (firstHashNodeInHashArray.hash == hash &&
                    ((k = firstHashNodeInHashArray.key) == key || (key != null && key.equals(k)))) {
                //判断方法参数中的hash位置的第一个元素是不是就是要移除的node
                removeNode = firstHashNodeInHashArray;
            } else if ((e = firstHashNodeInHashArray.next) != null) {
                //是否树节点
                if (firstHashNodeInHashArray instanceof TreeNode) {
                    removeNode = ((TreeNode<K, V>) firstHashNodeInHashArray).getTreeNode(hash, key);
                } else {
                    //链表节点,do while 比较
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            removeNode = e;
                            break;
                        }
                        firstHashNodeInHashArray = e;
                    } while ((e = e.next) != null);
                }
            }
            if (removeNode != null && (!matchValue || (v = removeNode.value) == value || (value != null && value.equals(v)))) {
                if (removeNode instanceof TreeNode) {
                    ((TreeNode<K, V>) removeNode).removeTreeNode(this, tab, movable);
                } else if (removeNode == firstHashNodeInHashArray) {
                    tab[index] = removeNode.next;
                } else {
                    firstHashNodeInHashArray.next = removeNode.next;
                }
                ++modCount;
                --size;
                afterNodeRemoval(removeNode);
                return removeNode;
            }
        }
        return null;
    }

JDK8 新增的方法

  • public V getOrDefault(Object key, V defaultValue) : key不存在返回defaultValue
  • public V putIfAbsent(K key, V value) : 如果key不存在,那么保存
  • public boolean remove(Object key, Object value) : remove(Object key)的基础上补充对value的校验,如果value校验失败,那么参数中的key不会删除
  • public boolean replace(K key, V oldValue, V newValue)
  • public V replace(K key, V value) : 注意这里返回的是oldValue
  • public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) : 如果key存在map中返回对应的value,否则执行function
  • public V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) : 如果key存在map中则执行function.apply(key, oldValue),function得到的v会设置成key的新value且返回
  • public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) : 如果key存在map中,remappingFunction.apply(key, oldValue)且将结果设置为key的新value ; 如果不存在就将key存储到map中,value就是function的结果
  • public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) : 这个方法还是别用了=_=||
  • public void forEach(BiConsumer<? super K, ? super V> action) : 这个就不用说了,需要注意不要触发CME
  • public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) : e.value = function.apply(e.key, e.value);



Q&A

HashMap : 如何有效减少碰撞
  • 扰动函数 : 使元素位置分布均匀,减少碰撞几率
  • 使用final对象,并采用合适的equals()和hashCode()方法
HashMap : 扩容的问题
  • 多线程环境下,调整大小存在条件竞争,容易造成死锁
  • 需要将老的map中元素重新移植到新的map中,rehashing就是一个比较耗时的过程
HashMap需要满足什么条件才会将链条转为红黑树
  1. 当前链条长度大于树形阈值
  2. 当前table容量大于MIN_TREEIFY_CAPACITY(64)
  3. 如果2不满足,说明当前链条的hash值碰撞率过高,需要扩容降低
为啥不直接使用hashCode做散列

hashCode是一个int值,范围在-21亿到+21亿,总更加起来有40多亿的数字,HashMap默认的容量才16,用这么大的数值范围做散列明显不现实,碰撞几率太小了

HashMap中为什么要求初始化容量大小是2的n次方

是为了hash方法中使用与以及位移运算代替取模,以获得更好的性能。(h = key.hashCode()) ^ (h >>> 16)

Collections.synchronizedMap(map)是如何保证线程安全的呢
    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
        return new SynchronizedMap<>(m);
    }
    
    /**
     * @serial include
     */
    private static class SynchronizedMap<K,V>
        implements Map<K,V>, Serializable {
        private static final long serialVersionUID = 1978198479659022715L;

        private final Map<K,V> m;     // Backing Map
        final Object      mutex;        // Object on which to synchronize

        SynchronizedMap(Map<K,V> m) {
            this.m = Objects.requireNonNull(m);
            mutex = this;
        }

        SynchronizedMap(Map<K,V> m, Object mutex) {
            this.m = m;
            this.mutex = mutex;
        }

        public int size() {
            synchronized (mutex) {return m.size();}
        }
        ...
    }

可以看到,SynchronizedMap是Collections的一个静态内部类,实现了Map接口,所有map的操作都使用了synchronized锁住了当前对象,所以是线程安全的。

传统 HashMap的缺点
  1. JDK 1.8 以前 HashMap的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
  2. 当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有n个元素,遍历的时间复杂度就是O(n),完全失去了它的优势。
  3. 针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高级摸鱼工程师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值