JDK源码阅读—IdentityHashMap

简介

IdentityHashMap是一个散列表,储存的元素为键值对(key-value),允许空值和空键,非线程安全,不能确保内部元素的顺序。与其他Map不同,IdentityHashMap对键使用引用相等的比较(==)而不是equals,所以它可以存放多个内容相同的键,只要这些键的引用不相同。

以下分析基于corretto-1.8.0_282版本。

继承关系

IdentityHashMap继承关系.png

  1. 实现Serializable接口,被序列化。
  2. 实现Cloneable接口,可以通过.clone()方法克隆出一个实例。
  3. 实现Map接口,提供了Map的相关操作。

属性

DEFAULT_CAPACITY

/**
 * 默认初始容量,必须是2的幂
 * 负载系数定义为2/3
 */
private static final int DEFAULT_CAPACITY = 32;

MINIMUM_CAPACITY

/**
 * 最小容量,当调用带参构造实例化时,若计算得到的初始容量小于
 * 此值,则使用最下容量作为初始容量,必须为2的幂
 */
private static final int MINIMUM_CAPACITY = 4;

MAXIMUM_CAPACITY

/**
 * 最大容量,必须是小于等于2的29次方的一个2的幂
 */
private static final int MAXIMUM_CAPACITY = 1 << 29;

table

/**
 * 桶数组,大小必须是2的幂
 * 键和值都放在数组中,例如键映射到索引i处,则值
 * 会放在索引i+1处
 */
transient Object[] table;

size

/**
 * 键值对个数
 */
int size;

modCount

/**
 * 修改次数,用作迭代时的快速失败检查
 */
transient int modCount;

NULL_KEY

/**
 * key为null会被替换为此值
 */
static final Object NULL_KEY = new Object();

构造方法

IdentityHashMap()

/**
 * 使用默认容量(32)实例化IdentityHashMap
 */
public IdentityHashMap() {
    init(DEFAULT_CAPACITY);
}

/**
 * 根据传入容量初始化数组
 * 因为键和值都放在数组中,所以数组大小是容量的两倍
 */
private void init(int initCapacity) {
    table = new Object[2 * initCapacity];
}

IdentityHashMap(int expectedMaxSize)

/**
 * 使用给定的最大大小实例化IdentityHashMap,会根据此参数计算出容量
 * 当储存的元素个数超过expectedMaxSize时,可能会进行扩容
 */
public IdentityHashMap(int expectedMaxSize) {
    if (expectedMaxSize < 0)
        throw new IllegalArgumentException("expectedMaxSize is negative: "
                                            + expectedMaxSize);
    init(capacity(expectedMaxSize));
}

/**
 * 返回给定预期最大大小的适当容量,必须为2的幂
 * 返回的cap需要满足expectedMaxSize<(2 * cap / 3)
 */
private static int capacity(int expectedMaxSize) {
    // 若expectedMaxSize大于(MAXIMUM_CAPACITY / 3) 返回MAXIMUM_CAPACITY
    // 若expectedMaxSize小于(2 * MINIMUM_CAPACITY / 3) 返回MINIMUM_CAPACITY
    // 否则返回小于等于expectedMaxSize的3倍的2的幂
    return
        (expectedMaxSize > MAXIMUM_CAPACITY / 3) ? MAXIMUM_CAPACITY :
        (expectedMaxSize <= 2 * MINIMUM_CAPACITY / 3) ? MINIMUM_CAPACITY :
        Integer.highestOneBit(expectedMaxSize + (expectedMaxSize << 1));
}

IdentityHashMap(Map<? extends K, ? extends V> m)

/**
 * 根据给定的Map实例化IdentityHashMap,容量由给定Map计算
 */
public IdentityHashMap(Map<? extends K, ? extends V> m) {
    // Allow for a bit of growth
    this((int) ((1 + m.size()) * 1.1));
    putAll(m);
}

方法

put(K key, V value)

/**
 * 将键值对插入Map,若key之前存在,则更新其值
 * 比较的是key的引用,使用==而不是equals
 */
public V put(K key, V value) {
    final Object k = maskNull(key);

    retryAfterResize: for (;;) {
        final Object[] tab = table;
        final int len = tab.length;

        // 计算key映射到数组的索引
        int i = hash(k, len);

        // 线性探测解决冲突
        // 遍历数组,步长为2,直到找到与key引用相等的位置或一个空位置
        for (Object item; (item = tab[i]) != null;
                i = nextKeyIndex(i, len)) {

            // 若此位置不为null且与给定的key引用相等,则更新其值
            // 引用不相等,则移动到后一个key,继续比较
            if (item == k) {
                @SuppressWarnings("unchecked")
                    V oldValue = (V) tab[i + 1];
                // 更新值
                tab[i + 1] = value;
                // 返回旧值
                return oldValue;
            }
        }

        // 执行到这里说明数组中没有与key引用相等的位置,执行插入操作
        final int s = size + 1;

        // 如果插入后元素个数超过了容量的2/3(负载系数),则先执行扩容操作
        if (s + (s << 1) > len && resize(len))
            // 扩容后需要重新计算key映射后的索引
            continue retryAfterResize;

        // 真正开始插入
        modCount++;

        // 索引i处储存key,i为找到的空位置,可能不是key映射的位置
        tab[i] = k;
        // 索引i+1处储存值
        tab[i + 1] = value;
        // 更新元素个数
        size = s;
        return null;
    }
}

/**
 * 当key为null时,将其替换为NULL_KEY
 */
private static Object maskNull(Object key) {
    return (key == null ? NULL_KEY : key);
}

/**
 * 计算hash映射到桶数组的索引
 */
private static int hash(Object x, int length) {
    // 获取对象的原始hashCode,此hashCode不受重写的hashCode()方法影响
    // 此hashCode每个对象唯一
    int h = System.identityHashCode(x);

    // 计算索引
    // 先将hashCode乘以-254,然后取对length的模
    // 不清楚为什么取了-254这个值
    // (h << 1) - (h << 8) = (h * 2) - (h * 2**8) = 2h - 256h = -254h
    // -254是偶数,乘完后结果也一定是个偶数,可以保证映射到数组的偶数位
    // 数组的偶数位保存key,奇数位保存value
    return ((h << 1) - (h << 8)) & (length - 1);
}

/**
 * 找到下一个key的位置,超过最大索引重置为0
 */
private static int nextKeyIndex(int i, int len) {
    return (i + 2 < len ? i + 2 : 0);
}

putAll(Map<? extends K, ? extends V> m)

/**
 * 将给定Map中的所有元素添加或更新到IdentityHashMap中
 */
public void putAll(Map<? extends K, ? extends V> m) {

    // 若给定的Map为空,则不执行任何操作
    int n = m.size();
    if (n == 0)
        return;

    // 若给定Map大小超过size,则根据给定Map的大小做一次扩容操作
    if (n > size)
        resize(capacity(n));

    // 遍历给定的Map,将其所有元素插入IdentityHashMap
    for (Entry<? extends K, ? extends V> e : m.entrySet())
        put(e.getKey(), e.getValue());
}

remove(Object key)

/**
 * 删除指定键的键值对
 */
public V remove(Object key) {

    // 计算key映射到数组的索引
    Object k = maskNull(key);
    Object[] tab = table;
    int len = tab.length;
    int i = hash(k, len);

    // 线性探测
    while (true) {
        Object item = tab[i];

        // 找到引用相同的位置,删除此处的键值对
        if (item == k) {
            modCount++;
            size--;
            @SuppressWarnings("unchecked")
                V oldValue = (V) tab[i + 1];

            // 解除引用
            tab[i + 1] = null;
            tab[i] = null;

            closeDeletion(i);
            return oldValue;
        }

        // key不存在
        if (item == null)
            return null;

        // 遍历下一个key
        i = nextKeyIndex(i, len);
    }
}

/**
 * 删除一个键值对后,需要将后面映射相同的键值对向前移动,使之保持连续
 */
private void closeDeletion(int d) {
    Object[] tab = table;
    int len = tab.length;

    // 遍历删除位置之后的键
    Object item;
    for (int i = nextKeyIndex(d, len); (item = tab[i]) != null;
            i = nextKeyIndex(i, len) ) {

        // 因为数组是循环使用的,所以判断比较多
        // (i < r && (r <= d || d <= i))
        // i < r 说明此hash在数组尾和数组头部都有分布,此时i已经遍历到数组头部
        // r <= d 说明被删除的索引d在数组尾,对应下面这种情况,r <= d <= len - 2
        // --------------
        // i         r d
        //
        // d <= i 说明被删除的索引d在数组头,对应下面这种情况,0 <= d <= i
        // --------------
        //   d i       r
        //
        // (r <= d && d <= i)说明此hash不存在跨越,对应下面这种情况,r <= d <= i
        // --------------
        //     r   d i
        // 符合上述条件说明i位置的键与d是相同的映射,需要将索引i处的键值对移动到索引d处
        int r = hash(item, len);
        if ((i < r && (r <= d || d <= i)) || (r <= d && d <= i)) {

            // 索引i处的键值移动到索引d处
            tab[d] = item;
            tab[d + 1] = tab[i + 1];
            tab[i] = null;
            tab[i + 1] = null;
            d = i;
        }
    }
}

clear()

/**
 * 清除所有元素
 */
public void clear() {
    modCount++;
    Object[] tab = table;
    // 遍历整个数组,将所有位置为null
    for (int i = 0; i < tab.length; i++)
        tab[i] = null;
    // 元素个数置为0
    size = 0;
}

get(Object key)

/**
 * 返回key对应的值,若key不存在,返回null
 */
@SuppressWarnings("unchecked")
public V get(Object key) {
    // 若key为null,替换为NULL_KEY
    Object k = maskNull(key);
    Object[] tab = table;
    int len = tab.length;
    // 计算key映射到数组的索引
    int i = hash(k, len);

    // 线性探测
    while (true) {
        Object item = tab[i];
        if (item == k)
            // 找到与key引用相等的键,返回对应的值
            return (V) tab[i + 1];
        if (item == null)
            // 找不到返回null
            return null;
        i = nextKeyIndex(i, len);
    }
}

containsKey(Object key)

/**
 * 查询是否包含给定的key
 */
public boolean containsKey(Object key) {

    // 计算key映射到数组的索引
    Object k = maskNull(key);
    Object[] tab = table;
    int len = tab.length;
    int i = hash(k, len);

    // 线性探测
    while (true) {
        Object item = tab[i];
        if (item == k)
            return true;
        if (item == null)
            return false;
        i = nextKeyIndex(i, len);
    }
}

containsValue(Object value)

/**
 * 查询是否包含给定的value
 */
public boolean containsValue(Object value) {
    Object[] tab = table;

    // 遍历数组储存值的位置
    for (int i = 1; i < tab.length; i += 2)

        // 若找到了相同的value,且对应的key不为null,则返回true
        if (tab[i] == value && tab[i - 1] != null)
            return true;

    return false;
}

forEach(BiConsumer<? super K, ? super V> action)

/**
 * 遍历所有元素
 */
@SuppressWarnings("unchecked")
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    int expectedModCount = modCount;

    Object[] t = table;

    // 遍历数组中所有的key
    for (int index = 0; index < t.length; index += 2) {
        Object k = t[index];

        // 若key不为null,则将此键值对传入action处理
        if (k != null) {
            action.accept((K) unmaskNull(k), (V) t[index + 1]);
        }

        // 快速失败检查
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
}

/**
 * 将NULL_KEY转换回null
 */
static final Object unmaskNull(Object key) {
    return (key == NULL_KEY ? null : key);
}

replaceAll(BiFunction<? super K, ? super V, ? extends V> function)

/**
 * 遍历所有元素,使用给定的function计算新的值进行替换
 */
@SuppressWarnings("unchecked")
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
    Objects.requireNonNull(function);
    int expectedModCount = modCount;

    Object[] t = table;

    // 遍历数组中所有的key
    for (int index = 0; index < t.length; index += 2) {
        Object k = t[index];

        // 若key不为null,则将此键值对传入function处理,使用返回值更新value
        if (k != null) {
            t[index + 1] = function.apply((K) unmaskNull(k), (V) t[index + 1]);
        }

        // 快速失败检查
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
}

总结

  1. IdentityHashMap键和值都存放在数组中,键存放在偶数位,值存放在奇数位,所以桶数组的长度是容量的2倍。
  2. IdentityHashMap使用线性探测解决哈希冲突。
  3. IdentityHashMap允许空键,通过将其替换为NULL_KEY来进行存放。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值