转载

HashMap实现原理和源码解析

哈希表(hash table)也叫散列表,是一种非常重要的数据结构。许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对Java集合框架中的对应实现HashMap的实现原理进行讲解,然后会对JDK8的HashMap源码进行分析。

一、什么是哈希表

先了解下基本数据结构。

  1. 数组 :采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。
  2. 线性链表 :对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)。
  3. 二叉树 :对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
  4. 哈希表 :相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。

我们知道,数据结构的物理存储结构只有两种: 顺序存储结构 链式存储结构 (像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性, 哈希表的主干就是数组 存储位置 = f(关键字), f函数就是 哈希函数, 关键字就是 key 这个函数的设计好坏会直接影响到哈希表的优劣。

HashMap实现原理和源码解析

5. 哈希冲突 :   然而万事无完美,如果两个不同的元素, 通过哈希函数得出的实际存储地址相同 怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的 哈希冲突 ,也叫哈希碰撞。哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是 数组+链表 的方式。

二、HashMap实现原理

HashMap的主干是一个Node数组。Node是HashMap的基本组成单元,每一个Node包含一个key-value键值对。

/**

  * The table, initialized on first use, and resized as

  * necessary. When allocated, length is always a power of two.

  * (We also tolerate length zero in some operations to allow

  * bootstrapping mechanics that are currently not needed.)

  * 第一次使用的时候才进行初始化,如果有需要会重新调整大小,当重新分配内存的时候,数组长度总是2的次方

  */

  transient Node<K,V>[] table;

Node是HashMap中的一个静态内部类。代码如下:

/**

* Basic hash bin node, used for most entries.  (See below for

* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)

*/

// 与1.7中 Entry的内容大同小异,只是换了个名称而已!

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

final int hash;  //对key的hashcode值进行hash运算后得到的值,存储在Node,避免重复计算

final K key;

V value;

Node<K,V> next;  //存储指向下一个Node的引用

Node(int hash, K key, V value, Node<K,V> next) {

this.hash = hash;

this.key = key;

this.value = value;

this.next = next;

}

public final K getKey()        { return key; }

public final V getValue()      { return value; }

public final String toString() { return key + "=" + value; }

public final int hashCode() {

return Objects.hashCode(key) ^ Objects.hashCode(value);

}

public final V setValue(V newValue) {

V oldValue = value;

value = newValue;

return oldValue;

}

public final boolean equals(Object o) {

if (o == this)

return true;

if (o instanceof Map.Entry) {

Map.Entry<?,?> e = (Map.Entry<?,?>)o;

if (Objects.equals(key, e.getKey()) &&

Objects.equals(value, e.getValue()))

return true;

}

return false;

}

}

几个重要属性:

/**

* The default initial capacity - MUST be a power of two.

* 默认初始化容量大小16,容量大小必须是2的次方

*/

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

/**

* The maximum capacity, used if a higher value is implicitly specified

* by either of the constructors with arguments.

* MUST be a power of two <= 1<<30.

* 最大的容量为 2^30

*/

static final int MAXIMUM_CAPACITY = 1 << 30;

/**

* The load factor used when none specified in constructor.

* 负载因子,一旦超过就会进行扩容

*/

static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**

* The number of times this HashMap has been structurally modified

* Structural modifications are those that change the number of mappings in

* the HashMap or otherwise modify its internal structure (e.g.,

* rehash).  This field is used to make iterators on Collection-views of

* the HashMap fail-fast.  (See ConcurrentModificationException).

* 用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了

*(比如put,remov*e等操作),需要抛出异常ConcurrentModificationException

*/

transient int modCount;

/**

* The next size value at which to resize (capacity * load factor).

*

* @serial

*阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,

*threshold一般为capacity*loadFactory。HashMap在进行扩容时需要参考threshold

*/

int threshold;

/**

* The bin count threshold for using a tree rather than list for a

* bin.  Bins are converted to trees when adding an element to a

* bin with at least this many nodes. The value must be greater

* than 2 and should be at least 8 to mesh with assumptions in

* tree removal about conversion back to plain bins upon

* shrinkage.

*当一个bucket是一个链表,链表个数大于等于8时,就要树状化,也就是要从链表结构变成红黑树结构

*/

static final int TREEIFY_THRESHOLD = 8;

HashMap构造函数:有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值initialCapacity默认为16,loadFactory默认为0.75

//指定初始容量,负载因子

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

    }

//指定初始容量

public HashMap(int initialCapacity) {

        this(initialCapacity, DEFAULT_LOAD_FACTOR);

    }

//无参

public HashMap() {

        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted

    }

//将已存在的map放进去进行初始化,若为空则抛null异常

public HashMap(Map<? extends K, ? extends V> m) {

        this.loadFactor = DEFAULT_LOAD_FACTOR;

        putMapEntries(m, false);

    }

在常规构造器HashMap()中,没有为数组table分配内存空间,而是在执行put操作的时候才真正构建table数组。以下是put方法:

// 如果已经存在key对应的节点,则覆盖value值

public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

// 重写,putVal的第三个参数onlyIfAbsent=true,如果已经存在key对应的节点,不覆盖value值

@Override

public V putIfAbsent(K key, V value) {

return putVal(hash(key), key, value, true, true);

}

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

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) // 如果map为空时,调用resize()进行初始化!

n = (tab = resize()).length;

if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有在数组中找到对应的节点,则直接插入一个Node (未发生碰撞)

tab[i] = newNode(hash, key, value, null);

else {    // 找到了(n - 1) & hash 对应下标的数组(tab)中的节点 ,也就是发生了碰撞

Node<K,V> e; K k;

// 1. hash值一样,key值一样,则找到目标Node

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

// 2. 数组中找到的这个节点p是TreeNode类型,则需要插入到RBT里面一个节点

else if (p instanceof TreeNode)

e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

else {

// 3. 不是TreeNode类型,则表示是一个链表,这里就类似与jdk1.7中的操作

for (int binCount = 0; ; ++binCount) { // 遍历链表

if ((e = p.next) == null) {

// 4. 此时查找当前链表的次数已经超过7个,则需要链表RBT化!

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)))) // 5. 找到链表中对应的节点

break;

p = e;

}

}

// 如果e不为空,则表示在HashMap中找到了对应的节点

if (e != null) { // existing mapping for key

V oldValue = e.value;

// 当onlyIfAbsent=false 或者key对应的旧value为空时,用新的value替换旧value

if (!onlyIfAbsent || oldValue == null)

e.value = value;

afterNodeAccess(e);

return oldValue;

}

}

++modCount; // 操作次数+1

if (++size > threshold) // hashmap节点个数+1,并判断是否超过阈值,如果超过则重建结构!

resize();

afterNodeInsertion(evict);

return null;

}

下面主要关注是三个函数:

  • putTreeVal(this, tab, hash, key, value);
  • treeifyBin(tab, hash);
  • treeify();
  • resize();

本文永久更新链接地址: https://www.linuxidc.com/Linux/2018-04/152029.htm

原文  https://www.linuxidc.com/Linux/2018-04/152029.htm
正文到此结束
Loading...