HashMap浅析

HashMap应该是java中用的很频繁的数据结构,最近几天研究了下HashMap源码,了解的不算很多,讲讲我的体会

我看的是JDK1.8的HashMap,1.7跟1.8还是有些区别的

先看看HashMap源码中的关键变量:

 

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;    // 默认容量16
static final int MAXIMUM_CAPACITY = 1 << 30;    //最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;    //默认负载因子
transient Node<K,V>[] table;     //存储元素的Node数组,hashMap的元素就是放在这

final float loadFactor; //负载因子,参与 threshold的计算
int threshold; // 当size> threshold时,会进行扩容, threshold= capacity * loadFactor
transient int size; //元素个数

 

HashMap的容量是2的N次方,每次扩容新的table长度是 老的 一倍,

在size达到threshold的时候会进行扩容

HashMap是根据key通过 hash算法得到一个整数,然后再放入数组中(不是简单的直接放入哦)

看看保存元素的Node的结构

 

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;  //根据key计算出来的 hash值
    final K key;   
    V value;
   //next是用来在hash冲突的时候,利用链表的形式去保存冲突的值,第一个冲突的保存在next,如果还有
   //冲突,就保存在next元素的next里面,,以此类推,当然,jdk1.8调整了保存方式,冲突过多的时候,会保存在 红黑树里面
    Node<K,V> next; 
   
}

 

下面是HashMap一个构造函数:

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;
    this.threshold = tableSizeFor(initialCapacity);  
}

 

这里有个tableSizeFor方法

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;
}

这个方法用于求出大于等于 给定参数cap的 最小2次方,有点绕口,

比如你传入capacity 2, 大于等于capacity的最小2次方就是2

比如你传入capacity 3, 大于等于capacity的最小2次方就是4

 

可是问题来了,我传入的capacity如果是7,那这时候的threshold就是8了啊,岂不是说表的容量 还小于 扩容的阈值threshold ?

当然不是,这个时候其实HashMap并没有具体 实例化table, table是 在第一次put的时候创建的,

put发现table为null,就会调用resize方法扩容,resize里面重新计算了threshold和capacity

HashMap的resize 扩容方法  部分代码:

 

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {   //已经达到上限,不再扩容
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
      threshold = newThr; 
      Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
      table = newTab;

 

假设new HashMap<>(7),

 

 

然后构造方法将threshold经过计算后得到8,可是在resize方法做了啥事呢,

先将newCap=oldThr,也就是newCap为8了,

然后计算出新的threshold= newCap * loadfactory,计算结果为6了,所以,threshold不是为8,还是变成了 capacity * loadFactor

 

接下来看关键的 hash方法

 

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

 

 

这个方法跟jdkk1.7的不同了,不过效率可能更高了

先计算出key的hashCode然后  将计算出的 hashCode值无符号 右位移16位,然后 异或运算, 这个方法我也不大理解

这里有个问题了,hash方法返回的Int值可能很大,比如是3456吧,那我不可能建个 长度是3456的吧,比如hashMap默认容量16的时候,

要将这个数据放入 数组中怎么办?取余是其中一种方式

具体的存放的位置是根据这个表达式:

i = (n - 1) & hash  //n就是数组的length

这里没有直接使用 %  是为了提高效率

 

不过这个有可能会造成hash碰撞,所以对于碰撞的比较严重的,HashMap用红黑树来解决的,

 

看看put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //如果根据hash定位到 table中的 index位置上没有元素
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null); //插入元素到 table数组中
    else { //处理hash冲突的情况
        Node<K,V> e; K k;
        //如果之前 i位置的元素跟 要插入的元素,hash值,key都相等,将P赋值给e
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
//如果i位置的元素是 数节点,利用红黑树保存
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {    
            //冲突的元素都是放在 此位置的元素的next中,如果冲突多了,依次是next的next,,,,
            //当hash冲突 达到TREEIFY_THRESHOLD - 1个,
           //就用红黑树保存元素,若没有达到这个上限,就用链表保存
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //用于处理当put进来的key-value在表中已经存在,是否需要用新的value替换老的value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            //如果配置了onlyIfAbsent为false或者 之前位置的元素value为null,就用新的value替换
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //若达到上限,调用resize扩展
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

 

HashMap的get方法:

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 n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) { //hash值定位到table中的index位置,若此位置有值
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first; 
       //若不满足上面的条件(hash值相等,key相等),从链表中或者红黑树中查找
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

 

这里没有讲关于具体怎么用红黑树解决冲突,其实HashMap中有些变量和 方法都是跟红黑树相关的,

也就是用来解决冲突的,这里只是简单介绍了下大致的,也不知道写的有没有错,只是把我看的源码的理解介绍了下

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值