zoukankan      html  css  js  c++  java
  • Java HashMap原理

    HashMap存储结构

    HashMap中数据的存储是由数组与链表一起实现的

    数组寻址非常容易,其时间复杂度为O(1),但是当要插入或删除数据时,时间复杂度就会变为O(n)。链表插入和删除操作的内存复杂度为O(1),但是寻址操作的复杂度却是O(n)。HashMap结合两者的优点,即寻址,插入删除都快。

    HashMap中定义了一个Entry类的数组table,    Entry<K,V>[] table;    数组table中存储的是Entry对象(也是Entry链表的头结点)。

    Entry对象中保存的是Key-Value对。

    其中数组table被称作buckets,每个数组节点则是一个bucket(可以构成一个链表)

    capacity指的是buckets的容量,也即数组的大小,默认capacity大小如下

        /**
         * The default initial capacity - MUST be a power of two.
         */
        static final int DEFAULT_INITIAL_CAPACITY = 16;

    load factor是衡量buckets填满程度的比例,默认load factor大小如下

        /**
         * The load factor used when none specified in constructor.
         */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;

    当buckets中entry数量大于capacity*loadfactor时就要把capacity扩充为原来的两倍。

    HashMap中方法

    添加键值对put(K key, V value)方法

    实现流程如下:

    进行key是否为null的判断,如果key==null ,放置在数组table的0号位置,即table[0]。

    若key不为null,由key的hashCode计算相应的hash值,进而得到该key对应的数组table的下标i。

    判断table[i] 是否为null(不包含任何键值对),若是则创建该键值对的Entry对象,添加至table[i]。

    若table[i]不为null,遍历table[i]链表中的每个Entry对象,若该链表中已包含key,则覆盖其value。若该链表中不包含key,则创建该键值对的Entry对象并添加至链表头部。

    实现代码如下:

    public V put(K key, V value) {
        // HashMap允许存放null键和null值。
        // 当key为null时,调用putForNullKey方法,将value放置在数组table第一个位置。
        if (key == null)
            return putForNullKey(value);
    
    
        // 根据key的hashCode计算hash值。
        int hash = hash(key.hashCode());
        // 由hash值求得key对应table的下标。
        int i = indexFor(hash, table.length);
    
    
        // 如果 i 索引处的 Entry 不为 null,遍历table[i]链表的每一个Entry对象
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 如果链表中已有键值key,则新的value覆盖原来的,并返回原来的value
            // 判断为同一个键值的方法是,hash值相同,且key相同
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
    
        
        modCount++;
        // 若table[i]为null,则创建key,value的Entry对象,并添加至table[i]处
        // 若table[i]链表中没有键值key,则创建key,value的Entry对象,并添加至table[i]处
        addEntry(hash, key, value, i);
        return null;
    }

    addEntry()方法

    void addEntry(int hash, K key, V value, int bucketIndex) {
         if ((size >= threshold) && (null != table[bucketIndex])) {
             resize(2 * table.length); //扩容操作,将数据元素重新计算位置后放入newTable中,链表的顺序与之前的顺序相反
             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) {
        // 若table[i]不为null,则e指向table[i]链表的第一个元素
        Entry<K,V> e = table[bucketIndex];
        // 新的Entry对象添加至table[i]的表头
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

    get()方法

    计算hash值,然后调用indexFor()方法得到该key在table中的存储位置,得到该位置的单链表,遍历列表找到key和指定key内容相等的Entry,返回entry.value值

    实现代码如下:

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        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.equals(k)))
                return e.value;
        }
        return null;
    }

    HashMap的resize

           当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

           那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

  • 相关阅读:
    c# 大白话告诉你Thread的Sleep和Join的区别
    c# winform richtextbox控制每行颜色 + 滚动条始终滚动到最底部
    c# winform禁止窗口多开
    winform窗口关闭,进程没有关掉的解决办法
    System.Threading.Timer定时器使用注意事项
    c# socket 心跳 重连
    javascript 从对象数组中 按字段/属性取最大值或最小值
    codesmith设置mysql的连接字符串
    .net桌面程序或者控制台程序使用NLog时的注意事项
    vue获取不到后端返回的响应头
  • 原文地址:https://www.cnblogs.com/deltadeblog/p/8626814.html
Copyright © 2011-2022 走看看