HashMap源码解析(JDK1.7)

HashMap源码解析(JDK1.7)

萌新的学习之路… 纯属以学习过程再现…

不存在那些大佬的 重点整理…

没有耐心的朋友~ 可以直接 阅读 HashMap 中的 一些小标题… 每个后面 都有小的总结… 可以直接看- -

有错误 请指出= =蟹蟹~



HashMap层次结构图

HashMap层次结构


接口
Map接口

package java.util;
public interface Map<K,V> {
    int size();		//获取大小
    boolean isEmpty();	//判断是否为空
    boolean containsKey(Object key);	//key - value  中 查询是否包含了key
    boolean containsValue(Object value);	//key - value  中 查询是否含有value
    V get(Object key);	//通过key 获得 value
    V put(K key, V value);	//设置一个key - value 对 如果key 存在 则会覆盖value .. 返回值为原来的值
    V remove(Object key);	//移除key - value 对
    void putAll(Map<? extends K, ? extends V> m);  //讲别的map都放进来
    void clear();	//清空
    Set<K> keySet();   	//获得key的集合
    Collection<V> values();	//获得value的集合
    Set<Map.Entry<K, V>> entrySet();	//获得内部存储结构的集合
    
    /**
    	这个就是Map 底层实际存储的单元...
    */
    interface Entry<K,V> {     
        K getKey();	//获得K
        V getValue();	//获得Value
        V setValue(V value);   //对这个单元设置value  
        boolean equals(Object o);
        int hashCode();
    }
    
    boolean equals(Object o);
    int hashCode();
}


抽象类
AbstractMap

在这个类中. . . 实现了基本的map方法…

在这个类中… 作者考虑的是map的 一个具体的获取情况… 没有实现具体的 存储策略

实现的都是一些 get 以及 contains

获得所有的 entry集合 还是抽象的方法

当然在这一级别的时候 … 还是没有具体诞生出 一个 数组 + 链表 这样的结构 , 只有真实的 entry这个存储单元

所以在这一抽象类中… containsValue 和 containsKey … 都是会遍历所有的存储单元…效率很低

transient volatile Set<K>        keySet = null;		//对key集合的缓存机制.. 由于 无法使用put .. 这个可以是key集合 的视图...不会发生改变 , 可能设计的目的 有向线程安全靠的趋势..包括volatile关键字
transient volatile Collection<V> values = null;

public abstract Set<Entry<K,V>> entrySet();

//都是先获取所有的entry集合..一个一个遍历.. 十分的慢
public boolean containsValue(Object value) {
    Iterator<Entry<K,V>> i = entrySet().iterator();
    if (value==null) {
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            if (e.getValue()==null)
                return true;
        }
    } else {
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            if (value.equals(e.getValue()))
                return true;
        }
    }
    return false;
}
public boolean containsKey(Object key) {
    Iterator<Map.Entry<K,V>> i = entrySet().iterator();
    if (key==null) {
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            if (e.getKey()==null)
                return true;
        }
    } else {
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            if (key.equals(e.getKey()))
                return true;
        }
    }
    return false;
}

// 该类中 对 entry 有2种实现的策略

  1. SimpleEntry

    private static boolean eq(Object o1, Object o2) {
        return o1 == null ? o2 == null : o1.equals(o2);
    }
    
    public static class SimpleEntry<K,V>
        implements Entry<K,V>, java.io.Serializable
    {	//真实的存储单元
        private static final long serialVersionUID = -8499721149061103585L;
    
        private final K key;
        private V value;
        
        //省略掉了 实例化方法..get set方法.. 以及tostring 方法
        
        /**向map这种数据结构...要关注的东西 一个重点就是equals方法 和hashcode */
        public boolean equals(Object o) {	//这边是直接用eq方法 使用对象 的equals方法
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            return eq(key, e.getKey()) && eq(value, e.getValue());
        }
    
        //这边使用的hash 是 key的hash ^ value的hash
        public int hashCode() {
            return (key   == null ? 0 :   key.hashCode()) ^
                   (value == null ? 0 : value.hashCode());
        }
    
    }
    
  2. SimpleImmutableEntry

    这种实现与上面那个一模一样…就是取消掉了set方法… 所有的字段成为private final 方法… 变成了一个不可变对象… 要了解 不可变对象 的… 可以自行百度…


HashMap

对于学习java的一个基础的数据结构… 重点在于学习 它的数据结构是怎么样的… 需不需要扩容? 如何扩容?

先从 它的基础字段 着手 … 一些常量 . 以及成员变量\

成员变量 / 常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 默认的容量大小是 16
static final int MAXIMUM_CAPACITY = 1 << 30;		//最大容量 2^30次 
static final float DEFAULT_LOAD_FACTOR = 0.75f;		//默认负载因子为 0.75 ..算是一个阈值
static final Entry<?,?>[] EMPTY_TABLE = {};			//看到这个东西.. 猜测一下.. 看了之前的ArrayList 可能是初始化的时候 一种标记..

//直接把空的标记上去了 .. table..表格. 出现了第一个结构.. 数组..
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

transient int size;	   //元素大小
int threshold;	       //暂时不太清楚是什么..接着看下去
final float loadFactor;	//负载因子... final修饰.. 应该会在实例化方法中出现
transient int modCount;		//修改次数.. 用作迭代器里面的值.. 防止结构发生变化

//这个常量莫名其妙的- -后面看看
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

实例化方法

这里就列出 不同的… 一些构造器传递的方法 就不拿出来了

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 = initialCapacity;		//表面看上去 第一个参数应该是容量大小...但是为什么表示起来叫做threshold呢?
    init();		//居然还提供了一个构造函数...
}

void init() {	//空方法..但是 不是public 方法- - 好像我们无法覆盖这个方法..
    //可能是给这个包下的其他map 使用的吧
}

public HashMap(Map<? extends K, ? extends V> m) {		//入参是其他的Map
    //注意..看这个东西的计算方式... 另外一个 map的元素个数 / 负载因子.. + 1 和 10 取最大值
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    
    inflateTable(threshold);		//计算出的阈值进行一些变换

    putAllForCreate(m);	//把m全部都存进来
}

private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize	//找到一个 2^n形式的数字 且 恰好大于等于toSize
    int capacity = roundUpToPowerOf2(toSize);

    //这个阈值..有点东西.. 需要乘 负载因子... 和 11取最小数
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];	//实际的容量..都是2^n次形式..
    //也就是说 如果传入的初始参数为 15 ... 最后的扩容大小 是16.... 阈值是16 * 0.75 = 12 
    initHashSeedAsNeeded(capacity);	//这个方法后面解析..
}
private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
        ? MAXIMUM_CAPACITY
        : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

// Integer的类
/**
由于要获得一个2^n形式的数..因此我们可以想到使用二进制数来表示.. 又要大于等于toSize
如果仅仅只需要大于的话  比如   
	00000000 00000000 00000000 00010010 
	我们可以让整体数字向右边移动一位 然后把所有最高位的数归零
	00000000 00000000 00000000 00100000 (这个数)
	
	因为要等于.. 也就是说 有这个例子
	00000000 00000000 00000000 00010000
	如果按上面的移动方式 就会造成 多了一位
	00000000 00000000 00000000 00100000
	所以要先减掉1

说清楚了上面.. 这个方法要做的事情就明白了... 就是要保留最高位的1 其他都归零
但是它实现的方式真是太妙了...
具体原理就是从最高位的1 开始 把低位都置为1, 然后整体左移动把所有低位1 清空
使用1,2,4,8,16 类似迭代的操作..真的炫酷- -....原本1位1位清空的操作..变成了只需要执行6次..操作
比如一个数
	00000000 00000000 00000000 10011010
其实不必要考虑 低位是什么...
	00000000 00000000 00000000 1xxxxxxx
1.与上左移1位
	00000000 00000000 00000000 11xxxxxx
2.与上左移2位
	00000000 00000000 00000000 1111xxxx
3.与上左移4位
	00000000 00000000 00000000 11111111
其实已经完成了.. 后面的 是需要具有更高位的1 才需要.. 一共也就32位.. 执行到16..任意的一个最高位的1都能取到
最后再 - 左移一位的数..
00000000 00000000 00000000 10000000		全部清空..
public static int highestOneBit(int i) {
     // HD, Figure 3-1
     i |= (i >>  1);
     i |= (i >>  2);
     i |= (i >>  4);
     i |= (i >>  8);
     i |= (i >> 16);
     return i - (i >>> 1);
}
这个方法是真的巧妙
*/

小总结

  1. 实例方法中… 我们可以看出… 如果指定了一个 容量 进行初始化… 最终的容量 并非我们所指定的大小… 它的实际容量是 一个2的次方数 且大于等于我们所指定的…也就是说 指定11 或者 12 或者 13 的初始容量…最后都是一样的实际容量
  2. 如果不指定一个容量…那么默认的容量大小为10 , 默认负载因子 0.75…参与运算 也就是16 但是 阈值(threshold) 为 10 (一开始…) , 且在第一次put的时候 才会进行初始化容量… 出现了实际大小为16的数组…以及一个 (threshold) 为12 ## threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); ## 到这里…还看不出 阈值 有什么用… (我们接着看下去)

put 方法

这是map中的核心方法… 应该没问题吧… 在这之前… 我们先来看 hashmap 到底是用怎么样的存储结构来存储… 我们先来看一下 它实际的存储单元 entry

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;			
    V value;				
    Entry<K,V> next;		//注意这里.... 链表的结构出现了! 而且是单项链表.. 指后不指前
    int hash;				//还有hash作为缓存
    
    //基础的get/set方法都忽略了 以及tostring
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))	//如果不是节点直接的比较- -直接false
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        // 还是相同的配方.. 比较 key 和 value的 equals方法
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

    //熟悉的配方...key的hash ^ value的hash
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

   	//说是在entry被修改的时候..会被调用.. 不过hashmap中是空实现.. 在linkedHashMap中有实现
    void recordAccess(HashMap<K,V> m) {
    }

    //说是在entry被移除的时候..会被调用.. 不过hashmap中是空实现.. 在linkedHashMap中有实现
    void recordRemoval(HashMap<K,V> m) {
    }
    
}

然后结合之前的数组 也就是长这样的…

结构

// 有了这个概念之后… 我们再来看 put方法


public V put(K key, V value) {
    if (table == EMPTY_TABLE) {			
        inflateTable(threshold);		//用默认方法初始化 .. 第一次 put 才需要真正的初始化
    }
    if (key == null)		//注意!  map 是可以使用 null 作为key 的.. 但是只有一个
        return putForNullKey(value);
    int hash = hash(key);	//计算出当前key的hash值
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {	
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

private V putForNullKey(V value) {  //null节点 存储在 0 的位置
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {		//如果曾经 是有的 null 这个key 的话... 
            V oldValue = e.value;  
            e.value = value;
            e.recordAccess(this);		//修改 会调用e的方法.. 虽然还没有实现..
            return oldValue;	//返回以前的
        }
    }
    //如果没有这个key
    modCount++;		//因为是个添加的方法.. 结构会发生变化 .. 所以modCount 会自增
    addEntry(0, null, value, 0);	
    return null;
}

//添加一个 entry ... 注意.. 这里很重要的东西来了... 
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {	//只有当前元素大于等于阈值..且当前不为null的时候..才需要扩容
        resize(2 * table.length);	//扩容机制.. 很直接..扩大到原来的2倍
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}
//创造一个新的节点... 使用的依然是头插法
//作者可能感性的认为...刚刚插入的节点很可能被马上需要..
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {	//如果已经到达最大的容量 2^30次  那么就不扩容了
        threshold = Integer.MAX_VALUE;	//提升阈值.. 不允许发生扩容..
        return;
    }

    Entry[] newTable = new Entry[newCapacity];		//不在原来的基础上扩大..这里也需要看.. 是开辟一个新的空间...
    transfer(newTable, initHashSeedAsNeeded(newCapacity));	//这里又出现了这个方法..initHash...
    //这个方法上面也遗留的.. 下面进行扩展
    table = newTable;
    
    //扩容结束后..重新制定以下阈值..
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0; //hashSeed 默认为0 也就是 这个值 是 false
    
    //这个判断语句 前面是VM 是否开启...true ... 后者 需要 设置JVM参数...
    boolean useAltHashing = sun.misc.VM.isBooted() &&
        (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); 
    
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {	//如果要跳入 这个判断语句内 ... 现在需要做到的是 userAltHashing 为 true
        hashSeed = useAltHashing	//useAltHashing 为true 的时候 才会获得一个 新的hash种子
            ? sun.misc.Hashing.randomHashSeed(this)	
            : 0;
    }
    return switching;
}

/**
	结论..也就是说 只有在设置了JVM 参数的情况 下... 还会出现这个 hash种子...才会发生变化...否则 这个方法 基本都会是 false; .. 这里就设计到扩容内的问题了 transfer ..方法
**/
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {		//这个东西 在一般情况下 .. 都是false ..也就是说 不会出现 再次hash的情况
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity); // 重新hash 一下...变换一下
            
            //头插法..! 也是重点.. 每次插入在链表头部..往后移动
            //头插会造成..顺序反转..这也是重点.. 线程不安全的一个重要原因
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

/**
    这也是重要的一个地方..
    hashmap 是如何 根据hashcode 区分每个元素应该放那个位置.
    首先..我们知道map的容量 一定是一个 2^n 数.. 这个数 - 1 的二进制长什么样呢
    无非就是00000....001...1111.111 高位全0  低位全1的情况
    hashcode & 上这个值..意味着什么.. 这里的 length - 1 可以看成是一个类似掩码的样子... 
    舍弃高位... 取低位...根据低位的不同 进行hash..
    也就是在扩容的时候.. 因为 * 2的影响.. 进行位置索引的时候 会发生不同..
    而且就最高位发生了变化..在样本足够大的情况下... 接近1/2的概率..
    也就是说..原本在 index=4 的位置 ... 要么继续在4  要么 在 容量的一般 + 4的位置
**/
static int indexFor(int h, int length) {
    return h & (length-1);
}

// 不配置参数的情况下 hashSeed 为 0
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);	//string 里面还有个 hash32的方法- - 太神奇了..没看过
    }
    h ^= k.hashCode();		// 通常情况下.. 都是自己再hash - -
    
    h ^= (h >>> 20) ^ (h >>> 12);
    
    // 为啥要这么进行hash - -我也不清楚...反正就是增加散列度
    return h ^ (h >>> 7) ^ (h >>> 4); 
}

// hashMap 内置的 一个类..
private static class Holder {
    static final int ALTERNATIVE_HASHING_THRESHOLD;	//默认不加参数的时候.. 这个值是ALTERNATIVE_HASHING_THRESHOLD_DEFAULT 也就是 正数最大值...

    static {
        String altThreshold = java.security.AccessController.doPrivileged(
            new sun.security.action.GetPropertyAction(
                "jdk.map.althashing.threshold"));
        //只有 配置了 这个 JVM 参数..才会有反应..

        int threshold;
        try {
            threshold = (null != altThreshold)
                ? Integer.parseInt(altThreshold)
                : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;

            // disable alternative hashing if -1
            if (threshold == -1) {
                threshold = Integer.MAX_VALUE;
            }

            if (threshold < 0) {
                throw new IllegalArgumentException("value must be positive integer.");
            }
        } catch(IllegalArgumentException failed) {
            throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
        }

        ALTERNATIVE_HASHING_THRESHOLD = threshold;
    }
}

小总结 :

  1. null是可以作为key的,而且index 为 0

  2. put方法在1.7的HashMap中使用的是头插法… 如图所示

头插法

  1. 只有达到了阈值的要求 且 当前put的key 不是null 的时候… 才会发生扩容 . 扩容机制 是 扩容到 原来的2倍

  2. 在不配置JVM的参数情况下… hashSeed 一直为 0 且不会出现重hash的情况(也就是每个entry的key的hash不会发生改变)

  3. 这里补充一下… 为什么HashMap 会发生多线程的问题… 如果 不 put的话… 只是get …也就是 当做是 不可变对象 , 那是没有多线程问题… 抛开 变量能不能可见的角度… 在扩容的时候 会出现这种情况… 如图

问题1

问题2

出现了环状…就会导致 下一次 get的时候 计算出这个位置 将会陷入一个死循环


get 方法
/***
    其实get 方法就没啥好讲的了... 就是用相同的方式 计算出 key的hash...再确定index...
    然后再数组对应的位置 查询链表 , 找到位置.. 找不到就返回null
**/
public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}

小技巧总结…

  1. 如果在初始化的时候… 把负载因子调到 大于1的情况… 就不会发送扩容了… 大小被定死…
  2. 刚刚插入的值…找起来比较快 (狗头~)
  3. 等有时间- -再摸一摸 jdk 1.8 的hashmap … 数组 + 链表 + 红黑树的结构 , 尾插法 , index计算 也会不一样 听说…

结束
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值