Java——HashMap原理

目录
1. HashMap简介
2. HashMap的底层结构
3. HashMap源码分析
4. HashMap的扩容机制
5. HashMap的性能

1、HashMap简介

  1. HashMap是一个用于存储(key-value)结构的散列表
  2. ,继承了AbstractMap,实现了Map、Cloneable、java.io.Serializable 3个接口
  3. HashMap的keyvalue都是可以为null
  4. HashMap是线程不安全的,如果在高并发情况下想要保证线程安全,可以考虑使用HashTable或者currentHashMap。
  5. currentHashMap效率会比HashTable高。

2、HashMap的底层结构

  1. HashMap底层数据结构是数组+链表
  2. 其能够有相当快的查询速度(时间复杂度为O(1) )是因为对keyhashcode 使用hash算法进行运算,得出存储在数组中的下标
  3. 如果储存的对象一旦多起来了,就有可能导致hash冲突,即数组下标重复 。HashMap为了解决这个问题,采用了链表的结构
  4. 如果最新插入的Entry 的数组下标中已经存有数据了,则把该位置让出来给新插入的Entry并让其指向上一个Entry节点。

这里写图片描述

3、HashMap源码(jdk1.7)

3.1主要属性

DEFAULT_LOAD_FACTOR (加载因子),当加载因子越小的时候,数组利用率会越低,HashMap的hash冲突就会越低,即entry链表长度越短,查找效率越高。反之,加载因子越大,数组利用率会越高,HashMap的hash冲突就会越多,即entry链表长度越长,查找效率越低。

    //存储entry数组的默认初始容量 ,为2的4次方
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    //默认的最大容量 为2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //默认加载因子,数组使用率到达75%的时候就扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //当数组表还没扩容的时候,一个共享的空表对象
    static final Entry<?,?>[] EMPTY_TABLE = {};

    //内部数组,用来装entry,大小只能是2的n次方。
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    //存储Entry<K,V>的个数
    transient int size;

    /**
     * 扩容的临界点(加载因子*数组容量),如果当前容量达到该值,则需要扩容了。
     * 如果当前数组容量为0时(空数组),则该值作为初始化内部数组的初始容量
     */
    int threshold;

    //构造函数传进来的加载因子
    final float loadFactor;

    //Hash被修改的次数
    transient int modCount;

    //threshold的最大值
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

    //计算hash值时候用,初始是0
    transient int hashSeed = 0;

    //含有所有entry节点的一个set集合
    private transient Set<Map.Entry<K,V>> entrySet = null;
3.2 Entry类分析

Entry

static class Entry<K,V> implements Map.Entry<K,V> {

        final K key;

        V value;
        //多个Entry是构成单向链表结构,数组中存储的是一个个entry所构成的链表,next是指向下一个entry节点
        Entry<K,V> next;
        //用于记录本entry节点的hash值
        int hash;

        //初始化节点
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        //获取节点的key
        public final K getKey() {
            return key;
        }

        //获取节点的value
        public final V getValue() {
            return value;
        }

        //设置新value,并返回旧的value
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        //判断传入节点与此结点是否相等,如果相等则返回true,反之返回false
        public final boolean equals(Object o) {
            //传入对象不是Entry,就返回false
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            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和value生成hashCode
        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        //每当相同key的value被覆盖时被调用一次,在HashMap的子类LinkedHashMap中实现了这个方法
        void recordAccess(HashMap<K,V> m) {
        }

        //每移除一个entry就被调用一次,在HashMap的子类LinkedHashMap中实现了这个方法;
        void recordRemoval(HashMap<K,V> m) {
        }
    }
3.3 构造器分析

HashMap调用构造方法的时候,参数为非具体值并不会创建容器。当传具体值的时候,会创建一个内部数组(table数组)。默认初始容量是16,在3.1主要属性 中的代码块中有所体现,字段为DEFAULT_INITIAL_CAPACITY

    /**
     * 生成一个空HashMap,传入容量与加载因子
     * @param initialCapacity 初始容量
     * @param loadFactor 加载因子
     */
    public HashMap(int initialCapacity, float loadFactor) {
        //初始容量不能小于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        //初始容量不能大于默认的最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;

        //加载因子不能小于0,且不能为NaN(Not a Number)
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        //把加载因子赋值给属性
        this.loadFactor = loadFactor;
        //设置临界值
        threshold = initialCapacity;
        //该方法只在LinkedHashMap中有实现,主要在构造函数初始化和clone、readObject中有调用。
        init();
    }

    /**
     * 生成一个空hashmap,传入初始容量,加载因子使用默认值(0.75)
     * @param initialCapacity 初始容量
     */
    public HashMap(int initialCapacity) {
        //生成空数组,并指定扩容值
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 根据已有map对象生成一个hashmap,初始容量与传入的map相关,加载因子使用默认值
     * @param m Map对象
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        //生成空数组,并指定扩容值
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);

        //由于此时数组为空,所以使用“扩容临界值”新建一个数组
        inflateTable(threshold);

        //将传入map的键值对添加到初始数组中
        putAllForCreate(m);
    }
3.4 存储分析(put)

put方法和get方法是HashMap中最常用的方法,下面对put方法进行分析。

如果key为null的话,hash值为0,对象存储在table中index为0的位置(table[0])。当K不为null时,会先计算出K的hash值,然后通过hash值与table的length映射出一个下标i,这个i就是Entry存放在数组的位置。然后遍历table[i]的Entry,如果有相同K的,就替换新的value,返回oldvalue,结束。反之把新的Entry插入链表头部。

put算法流程图如下

这里写图片描述

put源码
 /**
     * 存入一个键值对,如果key重复,则更新value
     * @param key 键值名
     * @param value 键值
     * @return 如果存的是新key则返回null,如果覆盖了旧键值对,则返回旧value
     */
    public V put(K key, V value) {
        //如果数组为空,则新建数组
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }

        //如果key为null,则把value放在table[0]中
        if (key == null)
            return putForNullKey(value);

        //生成key所对应的hash值
        int hash = hash(key);

        //根据hash值和数组的长度找到:该key所属entry在table中的位置i
        int i = indexFor(hash, table.length);

        /**
         * 先找到i位置,然后遍历entry,
         * 如果发现存在key与传入key相等,则替换其value。返回oldvalue
         * 如果没有找到相同的key,则继续执行下一条指令,将此键值对存入链表头
         */
        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;
            }
        }
        //map操作次数加一
        modCount++;
        //扩容检测和添加entry
        addEntry(hash, key, value, i);
        return null;
    }
3.5获取源码分析(get)

下面是put的分析

get方法用于获取出入的参数Key所对应的value,get方法会先对key进行判断,如果为null就执行getForNullKey(),否则就通过getEntry()来找到entry,再通过entry获取value。其中getForNullKey()和getEntry()的算法流程如下

这里写图片描述

这里写图片描述


    /**
     * 根据key找到对应value
     * @param key 键值名
     * @return 键值value
     */
    public V get(Object key) {
        //如果key为null,则从table[0]中取value
        if (key == null)
            return getForNullKey();

        //如果key不为null,则先根据key,找到其entry
        Entry<K,V> entry = getEntry(key);

        //返回entry节点里的value值
        return null == entry ? null : entry.getValue();
    }

 private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        //查找table[0]处的链表,如果找到entry的key为null,就返回其value
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

    /**
     * 根据key值查找所属entry节点
     * @param key 键值名
     * @return entry节点
     */
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        //如果key为null,则其hash值为0,否则计算hash值
        int hash = (key == null) ? 0 : hash(key);

        //根据hash值找到table下标,然后迭代该下标中的链表里的每一个entry节点
        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;
    }

4、HashMap的扩容机制

当HashMap存储的元素越来越多的时候,hash冲突就会越来越多,查询效率就会越来越低。因为定位到table[i]的时候,需要遍历table[i]中的链表,冲突越多,链表的长度就会越长,所以查询的效率就越慢。为了提高效率,就需要对table进行扩容处理,也就是进行resize(),resize()这个操作也会出现在ArrayList中。

言归正传,resize是HashMap中最消耗性能的一个操作,在resize中,原数组中的所有数据会重新计算一次其在新数组中的位置,并存放进去。当HashMap在进行put操作的时候,检测到table的使用量达到扩容的临界threshold 的时候,就会进行扩容操作(threshold= table.length * DEFAULT_LOAD_FACTOR)

 /**
     * 对数组扩容,即创建一个新数组,并将旧数组里的东西重新存入新数组
     * @param newCapacity 新数组容量
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;

        //如果当前数组容量已经达到最大值了,则将扩容的临界值设置为Integer.MAX_VALUE(Integer.MAX_VALUE是容量的临界点)
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        //创建一个扩容后的新数组
        Entry[] newTable = new Entry[newCapacity];

        //将当前数组中的键值对存入新数组
        transfer(newTable, initHashSeedAsNeeded(newCapacity));

        //用新数组替换旧数组
        table = newTable;

        //计算下一个扩容临界点
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * 将现有数组中的内容重新通过hash计算存入新数组
     * @param newTable 新数组
     * @param rehash
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;

        //遍历现有数组中的链表
        for (Entry<K,V> e : table) {
            //查找链表里的每一个entry
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }

                //根据新的数组长度,重新计算此entry所在下标i
                int i = indexFor(e.hash, newCapacity);

                //将新的entry插入链表头部
                e.next = newTable[i];
                newTable[i] = e;

                //查看下一个entry
                e = next;
            }
        }
    }

5、HashMap性能

HashMap中,加载因子是衡量的是一个散列表的空间的使用程度,加载因子越大表示散列表的使用率越高,HashMap的hash冲突就会越多,即entry链表长度越长,查找效率越低。当加载因子越小的时候,数组利用率会越低,HashMap的hash冲突就会越少,即entry链表长度越短,查找效率越高。HashMap查找一个元素的效率为O(1+avg(entry.length)),即O(1)。经研究发现,加载因子选取0.75是比较合适的



PS:博文中如有什么不对的地方恳请大家指出,谢谢~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值