Java源码之IdentityHashMap源码分析

1.声明

当前内容主要用于本人学习和复习之用,主要包含IdentityHashMap的源码分析

2.查看IdentityHashMap的继承和实现

public class IdentityHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, java.io.Serializable, Cloneable

该IdentityHashMap继承了AbstractMap,实现了MapSerializableCloneable

3.查看IdentityHashMap的字段属性

// 默认容量为32
private static final int DEFAULT_CAPACITY = 32;

// 默认最小的容量为4
private static final int MINIMUM_CAPACITY = 4;

// 默认的最大的容量为2的29次方
private static final int MAXIMUM_CAPACITY = 1 << 29;

// 用于存储数据的table,长度必须时2的次方
transient Object[] table; // non-private to simplify nested class access

// key-value映射的长度
int size;

// 修改记录器
transient int modCount;

// 表示key中的null值,使用NULL_KEY表示
static final Object NULL_KEY = new Object();

分析发现:

  1. 当前的IdentityHashMap只有一个Object的数组,没有key和value如何实现映射的?
  2. 存放的null的key使用NULL_KEY代替(不存放null只作为key)

4.查看IdentityHashMap的构造函数

// 默认使用初始化容量位64长度
public IdentityHashMap() {
    init(DEFAULT_CAPACITY);
}

// 传递容量参数,需要通过计算获取出来
public IdentityHashMap(int expectedMaxSize) {
    if (expectedMaxSize < 0)
        throw new IllegalArgumentException("expectedMaxSize is negative: "
                                           + expectedMaxSize);
    init(capacity(expectedMaxSize));//传递小于2时初始化为8,传递2以上就创建6倍的长度
}

// 计算出需要的实际容量
private static int capacity(int expectedMaxSize) {
    // assert expectedMaxSize >= 0;
    return
        (expectedMaxSize > MAXIMUM_CAPACITY / 3) ? MAXIMUM_CAPACITY :
    (expectedMaxSize <= 2 * MINIMUM_CAPACITY / 3) ? MINIMUM_CAPACITY :
    Integer.highestOneBit(expectedMaxSize + (expectedMaxSize << 1));
}

// 传递map集合数据的时候,直接初始化为当前map集合的容量的+1的1.1倍
public IdentityHashMap(Map<? extends K, ? extends V> m) {
    // Allow for a bit of growth
    this((int) ((1 + m.size()) * 1.1));//变为当前的2.2倍+2
    putAll(m);
}

// 初始化Object容量
private void init(int initCapacity) {
    // 直接创建指定参数的两倍长度的数组
    table = new Object[2 * initCapacity];
}

   

从这里分析处出来

  1. 默认无参的构造函数默认创建64长度的数组
  2. 带实际初始化容量的参数,如果小于等于2那么长度就是8,否者就是初始化参数的6倍
  3. 带map集合的参数,那么为传递map实际数量的2.2倍+2(表示空闲最少2个)

5.查看putAll方法

public void putAll(Map<? extends K, ? extends V> m) {
    // 获取实际的map长度
    int n = m.size();
    if (n == 0)
        return;//没有数据直接返回
    // 如果出现传递的map长度大于当前IdentityHahsMap的容量就直接扩容
    if (n > size)
        resize(capacity(n)); // conservatively pre-expand(2.2倍+2)保证能存放
	// 实际上调用的方法就是通过循环当时使用put函数添加数据
    for (Entry<? extends K, ? extends V> e : m.entrySet())
        put(e.getKey(), e.getValue());
}

分析发现putAll方法

  1. 首先获取map集合的key-value的对数,如果没有直接什么都不做
  2. 然后判断是否超出了当前IdentityHashMap的长度,(放不下数据)超出直接扩容
  3. 通过entrySet方式从map集合中取出所有的数据,通过put方法添加到数组中

6.查看put方法

public V put(K key, V value) {
    final Object k = maskNull(key);//如果存放null值则直接放入NULL_KEY

    retryAfterResize: for (;;) {
        final Object[] tab = table;//获取当前的表
        final int len = tab.length;// 获取当前的table的长度
        int i = hash(k, len);  // 计算当前的key 的hash值
		// 如果hash出现原来的位置上有值,出现hash冲突
        for (Object item; (item = tab[i]) != null;
             i = nextKeyIndex(i, len)) {
            if (item == k) {
                @SuppressWarnings("unchecked")
                V oldValue = (V) tab[i + 1];
                tab[i + 1] = value;
                return oldValue;
            }
        }
		// 否者认为没有hash冲突(获取存储的实际位置
        
        final int s = size + 1;
        // Use optimized form of 3 * s.
        // Next capacity is len, 2 * current capacity.
        // 如果存放值的下标*3大于当前的length长度(就是表示放不下了每次存放2个数据,就把原来的长度扩容两倍
        if (s + (s << 1) > len && resize(len))
            continue retryAfterResize; // 重新计算hash存放的位置

        modCount++;
        tab[i] = k;
        tab[i + 1] = value;// 进行赋值(即key存放在0位置,value放在1位置)
        size = s; // 大小变成原来size+1
        return null;
    }
}

使用图解方式分析添加数据操作

  1. 第一添加数据

Java源码之IdentityHashMap源码分析_第1张图片

  1. 第二次添加数据,hash冲突的数据(直接向后面找到不冲突的,每次移动两个下标)

Java源码之IdentityHashMap源码分析_第2张图片

  1. 第三次添加hash冲突的数据,key也相同

Java源码之IdentityHashMap源码分析_第3张图片

如果出现需要扩容那么自动扩容后重新计算hash放入值(其size表示每次存放的数据的key和value),但是每次都是使用index存放key,使用index+1存放value

所以该IdentityHashMap是可以存放null的key的(包装成空Object)

7.查看nextKeyIndex方法

private static int nextKeyIndex(int i, int len) {
    return (i + 2 < len ? i + 2 : 0);
    // 实际为((i+2)
}
// 假设i=0,并且len=8.那么存放的下标就是2
// 但是注意永远不会出现超出len的情况,所以冲突就放在该冲突的位置的后面两位

分析发现,该算法就是如果该位置有值,那么跳到后面两位存放,如果存在则继续向后面两位

所以上面的put方法就是通过冲突位置向后找2位,直到找到空闲的位置存放即可

8.查看resize方法

private boolean resize(int newCapacity) {
    // assert (newCapacity & -newCapacity) == newCapacity; // power of 2
    int newLength = newCapacity * 2;// 直接扩容为原来的两倍

    Object[] oldTable = table;//获取原表的数据
    int oldLength = oldTable.length;//获取原表的长度
    
    // 如果原来的长度超出了最大长度,并且实际存放的个数已近快到到限制
    if (oldLength == 2 * MAXIMUM_CAPACITY) { // can't expand any further
        // 实际的数组长度就是size*2(keyvalue就是相邻的数组位置)
        if (size == MAXIMUM_CAPACITY - 1)
            throw new IllegalStateException("Capacity exhausted.");
        return false;
    }
    // 如果老长度大于新长度,那么认为不需要扩容
    if (oldLength >= newLength)
        return false;
	
    // 否则认为老长度小于新长度,直接创建新长度的数组
    Object[] newTable = new Object[newLength];
	// 每次跨越两个获取key
    for (int j = 0; j < oldLength; j += 2) {
        // 获取老表中的key
        Object key = oldTable[j];
        //如果存在数据(就表示存在数据)
        if (key != null) {
            // 从当前老表下标的后一位取出实际存放的值
            Object value = oldTable[j+1];
            // 置空老表的数据key
            oldTable[j] = null;
            // 置空老表的数据key对应的value
            oldTable[j+1] = null;
            
            // 重新计算该key在新表中存放的下标
            int i = hash(key, newLength);
            // 如果获取的下标中没有数据,直接存放
            while (newTable[i] != null)
                // 迭代获取空闲的位置
                i = nextKeyIndex(i, newLength);
            // 获取存放数据的的下标直接存放
            newTable[i] = key;
            newTable[i + 1] = value;
        }
    }
    table = newTable;
    return true;
}

分析发现

  1. 发现resize方法会将原表的长度执行*2的操作获取新表
  2. 然后通过每次跨越2个下标方式找出所有的key,以及下标+1位置key对应的value
  3. 然后计算该key在新表的位置,最后也是通过存放key和value(key的index+1)

所以这里的resize实际上就是扩容为原来长度的两倍!

9.查看remove方法

public V remove(Object key) {
    // 现获取包装的null的key,因为存放的不是null而是空Object
    Object k = maskNull(key);
    // 获取表
    Object[] tab = table;
    int len = tab.length;
    int i = hash(k, len);
    // 计算该key存放的位置

    // 循环遍历操作
    while (true) {
        // 直接获取该索引的值(key)
        Object item = tab[i];
        // 如果两个key完全一致(即内存地址也一致),表示找到了数据
        if (item == k) {
            // 修改器自增
            modCount++;
            size--;// 容量减少
            @SuppressWarnings("unchecked")
            // 获取key对应的value
            V oldValue = (V) tab[i + 1];
            // 清空数据
            tab[i + 1] = null;
            tab[i] = null;
            closeDeletion(i);
            return oldValue;
        }
        // 如果该索引没有数据,那么直接返回null(表示没有该key)
        if (item == null)
            return null;
        // 开始向后面移动两位
        i = nextKeyIndex(i, len);
    }
}

分析closeDeletion(i)方法

// 传递的是删除的key的下标
private void closeDeletion(int d) {
    // 获取原表(删除数据后的)的内容
    Object[] tab = table;
    int len = tab.length;//获取表长度

    // Look for items to swap into newly vacated slot
    // starting at index immediately following deletion,
    // and continuing until a null slot is seen, indicating
    // the end of a run of possibly-colliding keys.
    Object item;
    // 通过循环迭代该删除下标+2的以及后面的数据,每次迭代+2,并且找到不空的数据
    for (int i = nextKeyIndex(d, len); (item = tab[i]) != null;
         i = nextKeyIndex(i, len) ) {
        // The following test triggers if the item at slot i (which
        // hashes to be at slot r) should take the spot vacated by d.
        // If so, we swap it in, and then continue with d now at the
        // newly vacated i.  This process will terminate when we hit
        // the null slot at the end of this run.
        // The test is messy because we are using a circular table.
        // 计算找到的key的hash(认为是当前key下标的右边的key)
        int r = hash(item, len);
        // 开始迭代将该找到的key和value放在原来的hash位置和hash+1,并且实现向后面迭代,将其向前移动2位
        if ((i < r && (r <= d || d <= i)) || (r <= d && d <= i)) {
            tab[d] = item;
            tab[d + 1] = tab[i + 1];
            tab[i] = null;
            tab[i + 1] = null;
            d = i;
        }
    }
}

分析发现

  1. 该方法就是一个整理方法,用于整理该删除下标的后面+2的数据
  2. 判断该数据时候要放在原来删除的地方,如果是则认为hash是一致的,继续放在后面+2位置
  3. 实现了循环迭代找出hash一致的数据(追加到index+2位置,然后index像后面迭代+2),并整理了当前的table

10.查看remove方法

public void clear() {
    modCount++; // 修改器自增
    Object[] tab = table;//获取当前的表
    for (int i = 0; i < tab.length; i++)
        tab[i] = null;//全部置空
    size = 0;//长度置零
}

11.总结

1.IdentityHashMap使用数组方式实现key-value的mapping的映射(采用key=index和value=index+1方式存放)

2.IndentityHashMap默认容量是64,使用map的构造函数时,默认为原来的2.2倍+2

3.添加数据就是通过计算hash值然后计算出存放的位置,如果出现冲突那么就往后面+2的位置存放

以上纯属个人见解,如有问题请联系本人!

你可能感兴趣的:(Java)