一、概述

  哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表。

  是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

  给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

1.1、基本数据结构

  数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)

  线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)

  二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。

  哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。

二、HashMap

2.1、jdk6-HashMap

  JDK6中HashMap :数组+链表:里面是一个数组,然后数组中每个元素是一个单向链表。

  每个数组内实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。

  004-多线程-集合-Map-jdk6/7/8中ConcurrentHashMap、HashMap分析

2.1.1、构造函数

  几个构造函数的核心均是以下操作

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

  capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。默认大小16;DEFAULT_INITIAL_CAPACITY = 16;
  loadFactor:负载因子,默认为 0.75。
  threshold:扩容的阈值,等于 capacity * loadFactor

  其中table即是当前hash表,默认大小为16,init默认是一个空实现,为了后续扩展

  可以看到table的定义

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry[] table;

  不是线程安全的,并且长度必须是2的倍数

2.1.2、put方法说明

    public V put(K key, V value) {
         // 如果 key 为 null,最终会将这个 entry 放到 table[0] 中
        if (key == null)
            return putForNullKey(value);
      // 1. 求 key 的 hash 值
        int hash = hash(key.hashCode());
      // 2. 找到对应的数组下标
        int i = indexFor(hash, table.length);
       // 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,
      //    如果有,直接覆盖,put 方法返回旧值就结束了
        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;
            }
        }

        modCount++;
      // 4. 不存在重复的 key,将此 entry 添加到链表中
        addEntry(hash, key, value, i);
        return null;
    }

1》计算数组位置indexFor  

  使用 key 的 hash 值对数组长度进行取模

    static int indexFor(int h, int length) {
        return h & (length-1);
    }

  简单说就是取 hash 值的低 n 位。如在数组长度为 32 的时候,其实取的就是 key 的 hash 值的低 5 位,作为它在数组中的下标位置。

2》添加节点到链表

  //将新值放到链表的表头,然后 size++
    //参数(hash,key,value,key的hash在hash表中数据位置)
    void addEntry(int hash, K key, V value, int bucketIndex) {
       Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)// threshold=16*0.75=12 ,即size=12的时候开始扩容 2*16=32
            resize(2 * table.length);
    }

  这里是先添加,然后扩容threshold=16*0.75=12 ,即size=12的时候开始扩容 2*16=32

3》数组扩容  

  在插入新值的时候,如果当前的 size 已经达到了阈值,并且要插入的数组位置上已经有元素,那么就会触发扩容,扩容后,数组大小为原来的 2 倍。

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        // 新的数组
        Entry[] newTable = new Entry[newCapacity];
        // 将原来数组中的值迁移到新的更大的数组中
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

扩容就是用一个新的大数组替换原来的小数组,并将原来数组中的值迁移到新的数组中。

由于是双倍扩容,迁移过程中,会将原来 table[i] 中的链表的所有节点,分拆到新的数组的 newTable[i] 和 newTable[i + oldLength] 位置上。如原来数组长度是 16,那么扩容后,原来 table[0] 处的链表中的所有元素会被分配到新数组中 newTable[0] 和 newTable[16] 这两个位置。

其中transfer如下

004-多线程-集合-Map-jdk6/7/8中ConcurrentHashMap、HashMap分析
    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }
View Code

相关文章:

  • 2021-12-24
  • 2022-12-23
  • 2021-11-29
  • 2021-05-19
猜你喜欢
  • 2022-01-16
  • 2021-11-28
  • 2022-01-19
  • 2022-12-23
  • 2021-11-14
  • 2021-07-15
  • 2023-02-01
相关资源
相似解决方案