zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    python内建eval, exec 和exec函数的用法
    wxPython控件学习之wx.grid.Grid (包括对GridCellEditor和GridCelRender的扩展,以支持更多的grid cell 样式, 以GridCellColorEditor为例)
    wxPython控件学习之wx.ComboBox
    tar 解压缩命令
    ios是否安装了某应用
    iPhone IOS中DEB如何安装
    一种简便获取iPhone IMEI 的方法
    iOSOpenDevtroubleshoot
    Mac&iOS Socket编程
    Debian Binary Package Building HOWTO
  • 原文地址:https://www.cnblogs.com/huangzhe1515023110/p/9276031.html
Copyright © 2011-2022 走看看