HashMap(JDK 1.7 )学习

HashMap

学习资料
https://www.cnblogs.com/skywang12345/p/3310835.html
https://blog.csdn.net/jeffleo/article/details/54946424
http://www.importnew.com/16301.html

HashMap数据结构

  • JDK7之前hashmap又叫散列链表:基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储。
    image
  • JDK8中,当同一个hash值(Table上元素)的链表节点数不小于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树。这就是JDK7与JDK8中HashMap实现的最大区别。
    image

==本文是基于JDK1.7的HashMap的学习==

HashMap常量

public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {
    private static final long serialVersionUID = 362498820763181265L;
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    static final int MAXIMUM_CAPACITY = 1073741824;
    static final float DEFAULT_LOAD_FACTOR = 0.75F;
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64; 
    transient Set<Entry<K, V>> entrySet;
    transient int size;
    transient int modCount;
    int threshold;
    final float loadFactor;
  • DEFAULT_INITIAL_CAPACITY: 初始容量,16
  • DEFAULT_LOAD_FACTOR: 默认加载因子:0.75。加载因子是控制HashMap自动扩容的一个因子。HashMap条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构)。比如,当前容量时16,1*0.75=12,当HashMap元素个数超过12时,就会自动扩容到32.
  • MAXIMUM_CAPACITY:1073741824 = Integer.MAX_VALUE = 2的31次方
  • size:HashMap的大小
  • threshold:HashMap的极限容量,扩容临界点(容量和加载因子的乘积)
  • loadFactor:负载因子loadFactor衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
  • Entry< K,V>[] table:Entry类型的数组,HashMap用这个来维护内部的数据结构,它的长度由容量决定
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        //用于指向下一个元素,组成链表结构
        Entry<K,V> next;
        int hash;

         /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            //创建新元素时,把next指向旧元素,维持链表结构
            next = n;
            key = k;
            hash = h;
        }
·····
}

HashMap构造方法

// 默认构造函数。
HashMap()

// 指定“容量大小”的构造函数
HashMap(int capacity)

// 指定“容量大小”和“加载因子”的构造函数
HashMap(int capacity, float loadFactor)

// 包含“子Map”的构造函数
HashMap(Map<? extends K, ? extends V> map)

这里主要学习下 HashMap(int capacity, float loadFactor) 构造方法,因为我看到HashMap对容量的规定是必须是2的幂次方,而这个控制算法很值得学习下

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;
    //控制容量必须是2的幂次方
    this.threshold = tableSizeFor(initialCapacity);
}
  • 容量控制算法
/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

在java中,”>>>”表示无符号右移位运算符,”m >>> n”表示m的二进制右移n位,高位补0(无符号)。如 13 >>> 2 相当于01101右移两位高位补0,得到00011

tableSizeFor(int cap)方法主要目的就是算出离cap最近的2的幂次方数(大于或等于cap)
主要是利用了cap的最高位1右移运算(>>>),然后或运算(|=),实现了,从最高非0位开始,后面全部是1,从而算出离cap最近的2的幂次方数。
比如:
int n = cap - 1 = 01xxxxxxxx
让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。
n >>> 1 得到 001xxxxxxx, ” |= “与运算就是 01xxxxxxxx | 001xxxxxxx = 011xxxxxxx ,最高非0位开始,2位都是1
n >>> 2 得到 00011xxxxx, ” |= “与运算就是 011xxxxxxx | 00011xxxxx = 01111xxxxx ,最高非0位开始,4位都是1
以此类推,int是32位,最终只需要把高16位右移到低16位,然后进行与运算,就能算出离cap最近的2的幂次方数:0111111111

那么问题来了,为什么要把容量控制在2的幂次方?,在后面的put方法会讲到。

存储put

  • 特点

    1. HashMap允许null做key值。HashMap将“key为null”的元素都放在table[0]位置
    2. 调用put方法时,如果已经存在一个相同的key, 则返回的是前一个key对应的value,同时该key的新value覆盖旧value;
      如果是新的一个key,则返回的是null;
  • 代码实现

public V put(K key, V value) {
        //如果key为空的情况,放在table[0]
        if (key == null)
            return putForNullKey(value); 
        int hash = hash(key);
        //计算该hash值在table中的下标
        int i = indexFor(hash, table.length);
        //对table[i]存放的链表进行遍历
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果已经存在一个相同的key, 则返回的是前一个key对应的value,同时该key的新value覆盖旧value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //把当前key,value添加到table[i]的链表中
        addEntry(hash, key, value, i);
        return null;
    }
void addEntry(int hash, K key, V value, int bucketIndex) { 
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        //创建Entry节点,存储新数据
        createEntry(hash, key, value, bucketIndex);
    }
void createEntry(int hash, K key, V value, int bucketIndex) {
        //先获取到bucketIndex位置的旧元素
        Entry<K,V> e = table[bucketIndex];
        //创建新节点,同时把新节点的next指向旧元素e,维持了链表结构
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
  • 根据hash计算位置
static int indexFor(int h, int length) {
    return h & (length-1);
}

 这里我们假设length为16(2^n)和15,h为5、6、7

hlenghth & (length-1)result
5160101 & 1111 = 01015
6160110 & 1111 = 01106
7160111 & 1111 = 01117

该方法仅有一条语句:h&(length -1),这句话除了上面的取模运算外还有一个非常重要的责任:均匀分布table数据和充分利用空间。
==当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。== 所以HashMap会限制size是2的幂次方。

获取元素get

理解了HashMap的数据结构,获取元素也就很好理解了

   public V get(Object key) {
        //如果key为null,求null键
        if (key == null)
            return getForNullKey(); 
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
    final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

HashMap 非线性安全

  1. put新增元素时
    上面讲HashMap插入元素(addEntry)时,会创建新Entry节点,同时把新节点的next指向旧元素e,维持了链表接口
void createEntry(int hash, K key, V value, int bucketIndex) { 
        Entry<K,V> e = table[bucketIndex];
        //创建新节点,同时把新节点的next指向旧元素e,维持了链表接口
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

在高并发时候,就会存在一个线程获取到的旧元素,而另外一个线程又同时插入新元素,更新了链表head头元素

  1. put更新节点值时
    上面同样说到过,如果已经存在一个相同的key, 则返回的是前一个key对应的value,同时该key的新value覆盖旧value
public V put(K key, V value) {
            ·······省略
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
            ······省略

高并发时,有两个线程同时进入了上面判断语句后,数据就会被混乱

  1. remove时
    同put原理。

  2. rehash时
    当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值