HashMap源码解析

JDK1.8源码分析之HashMap
HashMap的数据结构(1.8)

前言

今天来说说HashMap,之前的List,主要是ArrayList、LinkedList,这两者反应了两种思想:

* ArrayList以数组形式实现,顺序插入、查找快,插入、删除较慢

* LinkedList以链表的形式实现,顺序插入、查找较慢,插入、删除方便

然而HashMap是拥有结合上面两种数据结构的优点,它是基于哈希表的Map接口的实现,以key-value的形式存在。

构造图如下:

蓝色线条:继承

绿色线条:接口实现、



正文

要理解HashMap,就必须了解其底层实现,而底层实现最终要的就是其数据结构了。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体

要分析理解HashMap源码之前必须对hashcode进行说明。

源于HashCode的官方文档定义:

hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,如 java.util.Hashtable提供的哈希表。

hashCode的常规协定:

在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。 
如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。 
以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。 
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。) 
当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

从上可得到:

1、hashCode的存在主要用于查找的快捷性,如Hashtable、HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;

2、如果两个对象相同,就适用于equals(Object)方法,那么这两个对象的hashCode一定要相同;

3、如果两个对象的equals方法被重写,那么对象的hashCode尽量重写,并产生hashCode的使用对象,一定要和equals方法中使用的一致;

4、两个对象的hashCode相同,并不一定表示两个对象相同,也就是不一定适用equals(Object)方法,只能说明这两个对象在散列存储结构中,如Hashtable,放在“同一个篮子里”。

HashMap定义
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

HashMap是一个散列表,它的存储的内容就是键值对(key-value)映射。

HashMap继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口

HashMap的实现不是同步的,意味着它不是线程安全的。他的key和value都可以为null。此外HashMap的映射不是有序的。

HashMap的属性

/**
 * 初始容量为16,容量必须是2的n次幂
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

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

/**
 * 默认加载因子为0.75f
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
 * 数组,长度必须为2的n次幂
 */
transient Node<K,V>[] table;

/**
 * Holds cached entrySet(). Note that AbstractMap fields are used
 * for keySet() and values().
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * 存储的元素数量
 */
transient int size;

/**
 * 用来实现fast-fail机制的
 */
transient int modCount;

/**
 * 下次扩容的临界值,size>=threshold就会扩容,threshold等于capacity*load factor (capacity * load factor).
 *
 * @serial
 */
int threshold;

/**
 * 加载因子
 *
 * @serial
 */
final float loadFactor;

HashMap是通过“拉法链”实现hash表的,它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。

其中table数组的实现:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

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

Node是HashMap的内部类,继承了Map的Entry接口,定义了键key、值value和下一个节点的引用next,以及hash值。

很明确的可以看出Entry的结构,他是一个单线链表的一个节点。也就是说HashMap的底层结构是一个数组,而数组的元素是一个单向链表


之前介绍的List中,查询的时候需要遍历所有的数组,为了解决这个问题HashMap采用hash算法将key散列为一个int值,这个int值对应到数组的下标,再做查询的时候,拿到key散列值,根据数组下标就能直接找到存储在数组的元素。但是由于hash可能出现相同的散列值,为了解决冲突,HashMap采用将相同的散列值存储到一个链表中,也就是说在一个链表中的元素他们的散列值绝对是相同的。找到数组下标取出链表,再遍历链表是不是比遍历整个数组效率高!!

HashMap构造函数

/**
 * 构造一个指定初始容量和加载因子的构造函数
 */
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);
}

/**
 * 构造一个默认容量为16,默认加载因子为0.75的HashMap
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * 构造一个指定map的HashMap
 */
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

/**
 * 返回一个大于输入参数且最近的2的整数次幂的数
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}


在这里提到了两个参数:初始容量,加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。

API方法


put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
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未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //(n - 1) & hash 确定元素存放在那个桶中,桶为空,
    // 新生成节点放入,此时这个节点放在数组中
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {//桶中的元素已经存在
        Node<K,V> e; K k;
        //比较桶中的第一个元素(数组中的节点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //将第一个元素赋值给e,用e来记录
            e = p;
        //hash值不相等,即key不相等,为红黑树节点
        else if (p instanceof TreeNode)
            //放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //在链表最末插入节点
            for (int binCount = 0; ; ++binCount) {
                //到达链表的尾部
                if ((e = p.next) == null) {
                    //在尾部插入新节点
                    p.next = newNode(hash, key, value, null);
                    //节点数达到阈值,转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;//跳出循环
                }
                //判断链表中的节点的key和插入的元素的key是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;//相等跳出循环
                //用于遍历桶中的链表,与前面的e=p.next结合,可以遍历链表
                p = e;
            }
        }
        //在桶中找到key值、hash值与插入元素相等的节点
        if (e != null) { // existing mapping for key
            //记录e的value
            V oldValue = e.value;
            //onlyIfAbsent为false或者oldValue为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            afterNodeAccess(e);//访问回调
            return oldValue;//返回旧值
        }
    }
    ++modCount;//fast-fail更改
    //实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    //插入后回调
    afterNodeInsertion(evict);
    return null;
}
扩容

/**
 * 扩容函数
 *
 * @return the table
 */
final Node<K,V>[] resize() {
    //当前table保存
    Node<K,V>[] oldTab = table;
    //保存table大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;//保存当前阈值
    int newCap, newThr = 0;
    //之前的table大小大于0
    if (oldCap > 0) {
        //之前table大于最大容量
        if (oldCap >= MAXIMUM_CAPACITY) {
            //阈值为最大整形
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //容量翻倍,使用左移,效率高
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //阈值翻倍
            newThr = oldThr << 1; // double threshold
    }
    //之前阈值大于0
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // oldCap = 0并且oldThr = 0,使用缺省值(如使用HashMap()构造函数,之后再插入一个元素会调用resize函数,会进入这一步)
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //新阈值为0
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        //初始化table
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //之前的table已经初始化过了
    if (oldTab != null) {
        //赋值元素重新进行hash
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

扩容会伴随一次重新hash分配,并且会遍历hash表中的所有元素,是非常耗时的,所以应进来避免resize()。

查找方法

/**
 * 查询方法
 */
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
 * Implements Map.get and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //table已经初始化,长度大于0,根据hash寻找table中的项也不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //桶中第一个元素相等
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //桶中不止一个节点
        if ((e = first.next) != null) {
            //为红黑树节点
            if (first instanceof TreeNode)
                //在红黑树中找
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //否则在链表中找
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
remove方法

与put方法类似

/**
 * 删除
 */
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**
 * Implements Map.remove and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to match if matchValue, else ignored
 * @param matchValue if true only remove if value is equal
 * @param movable if false do not move other nodes while removing
 * @return the node, or null if none
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

clear方法

/**
 * 清除所有元素,并把每个位置置为null
 */
public void clear() {
    Node<K,V>[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

总结

1、两者最主要的区别在于HashTable是线程安全的,HashMap是非线程安全的。HashTable的实现方法里面添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能可能会高些,若无特殊需求建议使用HashMap,在多线程的环境下使用HashMap则可以使用Collections.synchronizedMap( )方法来获取一个线程安全的集合(Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时,使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单说就是Collections.synchronizedMap()方法帮我们在操作HashMap的时候自动添加了synchronized来实现线程同步,其他类似的Collections.synchronizedxxx()也是一样的道理

2、HashMap允许key为null值,而HashTable则不允许。HashMap以null作为key值则总是存储在数组的第一个节点上。

3、HashMap是实现了Map接口,HashTable是实现了Map接口和Dictionary抽象类

4、HashMap的初始容量为16,HashTable为11,两者的加载因子都是0.75。扩容的时候,HashMap是为原来的两倍,HashTable是两倍还要+1.

5、两者的底层实现都是数组+链表+红黑树。

6、两者计算hash值的方法不同:

HashTable是直接用key的hashCode对table的长度进行取模。

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
HashMap是key的hashCode与hashCode的高16位做异或运算。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

https://www.cnblogs.com/leesf456/p/5242233.html

原文: http://tengj.top/2016/04/15/javajh3hashmap/  作者: 嘟嘟MD

原文地址:https://www.cnblogs.com/huangzhe1515023110/p/9276031.html