java学习系列3(集合hashmap)

来源:互联网 发布:大数据用户画像培训 编辑:程序博客网 时间:2024/05/18 00:20

HashMap原理详解

HashMap

一 定义和创建

  HashMap实现了Map接口,继承AbstractMap类。AbstractMap中包含了map的基本功能。

     (1) 初始大小

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

从源码可以看出大小是16(1左移动4位1000 = 16)

static final int MAXIMUM_CAPACITY = 1 << 30;

最大长度是2的30次方1073741824 基本能满足绝大部分需求的使用。

 

static final float DEFAULT_LOAD_FACTOR = 0.75f;

默认的负载因子是0.75 ,默认的负载因子一般不需要改动。如果更加关注内存空间,而不怎么关注速度,可以调大负载因子,反之,如果更加关注HashMap读写速度,而不太关注内存空间,则可以调小负载因子。

 

二 put方法,HASHMAP写入值,get方法读取值

从1.7之前的hashMap是用数组table+链表实现的,而在1.8jdk中队hashMap做了优化,通过数组,链表,红黑树(二叉树的一种)来实现,链表数组长度超过8时,转成红黑树从而提高了HashMap的效率。

 (1)1.7中 hashMap的put方法详解:

复制代码
public V put(K key, V value)   {    // 如果 key 为 null,调用 putForNullKey 方法进行处理   if (key == null)        return putForNullKey(value);    // 根据 key 的 keyCode 计算 Hash 值,hash函数为一个数学方法用位运算继续计算hash值  int hash = hash(key.hashCode());    //  查找hash值在table中的索引(此处的indexfor方式是一个数学方法, 返回值为 hash&table.length-1 ,通过位运算,保证此函数的返回值总是小于等于table.length,这样能确保i值在table数组的索引之内)     int i = indexFor(hash, table.length);   // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素   // table的i处存放的Entry中next值记录了链表情况,可能有多个Entry链,所以此时对链表循环处理 for (Entry<K,V> e = table[i]; e != null; e = e.next)    {        Object k;        // 1.找到key值的hash值与插入值的hash值相同的key,并且key的值要与插入的key值相同,     // 2.两者同时满足才能判断插入的是同一个key( 不同key值的hash值可能相等(hash碰撞),所以此处要增加key值自身的判断)     if (e.hash == hash && ((k = e.key) == key            || key.equals(k)))        {            //key值相同时直接替换value值,跳出函数         V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }    // 如果 i 索引处的 Entry 为 null 或者key的hash值相同而key不同  ,则需要新增Entry modCount++;    // 将 key、value 添加到 i 索引处   addEntry(hash, key, value, i);    return null;   }   
复制代码

下面通过viso图来简要描述流程

 

addEntry这个函数这里在简单的说一下,先上代码
复制代码
//table数组节点中新增 Entryvoid addEntry(int hash, K key, V value, int bucketIndex)   {       // 获取指定 bucketIndex 索引处的 Entry       Entry<K,V> e = table[bucketIndex];     ////将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry      //此处分为两种情况    //1.table中i出无元素,则 e的值为null,新插入的Entry的next为null,无链表    //2.table中i处存在元素,新插入的元素会覆盖数组中的位置,同时新创建的Entry中next指向老元素的,形成链表。     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);       // 如果 Map 中的 key-value 对的数量超过了极限      if (size++ >= threshold)           // 把 table 对象的长度扩充到 2 倍。          resize(2 * table.length);    //}  
复制代码

在table的指定位置添加Entry元素,如果已经有entry元素,用新增的Entry替代老元素,但在entry中的next记录原元素的位置,实现链表查询。

到这里基本上put方法的原理基本上就清晰明了,下面看一下get方法

复制代码
public V get(Object key)   {    // 如果 key 是 null,调用 getForNullKey 取出对应的 value    if (key == null)        return getForNullKey();    // 根据该 key 的 hashCode 值计算它的 hash 码   int hash = hash(key.hashCode());    // 直接取出 table 数组中指定索引处的值,   for (Entry<K,V> e = table[indexFor(hash, table.length)];        e != null;        // 搜索该 Entry 链的下一个 Entr        e = e.next)         // {        Object k;        // 如果该 Entry 的 key 与被搜索 key 相同       if (e.hash == hash && ((k = e.key) == key            || key.equals(k)))            return e.value;    }    return null;   } 
复制代码

先通过hash方法查到在数组中的位置,查找出entry并通过entry的next做遍历,查到key值对应的value

所以,HashMap的整体结构如下

  

  简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,急需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。



三 jdk1.8中hashMap的区别(简述)

复制代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {                Node<K,V>[] tab;         Node<K,V> p;         int n, i;        //如果table空,初始化table数组        if ((tab = table) == null || (n = tab.length) == 0)            n = (tab = resize()).length;                //查到i出元素为空 ,则在i位置新增Node元素        //注:此处i = (n - 1) & hash此方法即是jdk1.7中的indexFor方法,通过数学位运算,取的一个在table的长度范围内的值)        if ((p = tab[i = (n - 1) & hash]) == null) //没有查到key ,新增节点            tab[i] = newNode(hash, key, value, null);        else {            Node<K,V> e; K k;            //索引处的key值和key的hash值匹配,则直接把p节点指针传给临时变量e            if (p.hash == hash &&                ((k = p.key) == key || (key != null && key.equals(k))))                e = p;            else if (p instanceof TreeNode)//如果p节点是红黑树,特殊处理                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);            else {//遍历链表,处理key值的hash冲突情况                for (int binCount = 0; ; ++binCount) {                    if ((e = p.next) == null) {//在链表结尾处新增节点                        p.next = newNode(hash, key, value, null);//链表原末尾元素next指向先创建的元素                        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))))//检查key值和hash值是否同时相同                        break;                    p = e;                }            }            if (e != null) { // existing mapping for key  将新value值存放到节点处                V oldValue = e.value;                if (!onlyIfAbsent || oldValue == null)                    e.value = value;                afterNodeAccess(e);                return oldValue;            }        }        ++modCount;        if (++size > threshold)            resize();        afterNodeInsertion(evict);        return null;    }
复制代码

主要新增了红黑树方法, 在key值产生冲突事,如果链表的长度超过8则转化成红黑树来实现,避免同一节点太长的链表影响效率。

原创粉丝点击