深入Java基础--哈希表HashMap应用及源码详解

Posted Jack__Frost

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入Java基础--哈希表HashMap应用及源码详解相关的知识,希望对你有一定的参考价值。

继续深入Java基础系列。今天是研究下哈希表,毕竟我们很多应用层的查找存储框架都是哈希作为它的根数据结构进行封装的嘛。

本系列:

(1)深入Java基础(一)——基本数据类型及其包装类

(2)深入Java基础(二)——字符串家族

(3)深入Java基础(三)–集合(1)集合父类以及父接口源码及理解

(4)深入Java基础(三)–集合(2)ArrayList和其继承树源码解析以及其注意事项


文章结构:

(1)哈希概述及HashMap应用;
(2)HashMap源码分析;
(3)再次总结关键点


一、哈希概述及HashMap应用:

概述:详细介绍

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

对比:数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。哈希则是一种寻址容易,插入删除也容易的数据结构。

实际应用:

1.Hash主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做Hash值. 也可以说,Hash就是找到一种数据内容和数据存放地址之间的映射关系
2.查找:哈希表,又称为散列,是一种更加快捷的查找技术。我们之前的查找,都是这样一种思路:集合中拿出来一个元素,看看是否与我们要找的相等,如果不等,缩小范围,继续查找。而哈希表是完全另外一种思路:当我知道key值以后,我就可以直接计算出这个元素在集合中的位置,根本不需要一次又一次的查找!

HashMap概述:

Hash表 是一种逻辑数据结构,HashMap是Java中的一种数据类型(结构类型),它通过代码实现了Hash表 这种数据结构,并在此结构上定义了一系列操作。

HashMap关键点罗列:

1.基于数组来实现哈希表的,数组就好比内存储空间,数组的index就好比内存的地址;
2.每个记录就是一个Entry 《K, V>对象,数组中存储的就是这些对象;
3.HashMap的哈希函数 = 计算出hashCode + 计算出数组的index;
4.HashMap解决冲突:使用链地址法,每个Entry对象都有一个引用next来指向链表的下一个Entry;(也就是链地址法)

但是!JDK1.8升级了!!

JDK1.8之前:使用单向链表来存储相同索引值的元素。在最坏的情况下,这种方式会将HashMap的get方法的性能从O(1)降低到O(n)。

在JDK1.8:为了解决在频繁冲突时hashmap性能降低的问题,使用平衡树来替代链表存储冲突的元素。这意味着我们可以将最坏情况下的性能从O(n)提高到O(logn)。

在Java 8中使用常量TREEIFY_THRESHOLD来控制是否切换到平衡树来存储。目前,这个常量值是8,这意味着当有超过8个元素的索引一样时,HashMap会使用树来存储它们。

这一动态的特性使得HashMap一开始使用链表,并在冲突的元素数量超过指定值时用平衡二叉树替换链表。不过这一特性在所有基于hash table的类中并没有,例如Hashtable和WeakHashMap。目前,只有ConcurrentHashMap,LinkedHashMap和HashMap会在频繁冲突的情况下使用平衡树。

5.装填因子:默认为0.75;

(加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。)

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

在这里插入图片描述

7.不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

8.影响性能的因素:“初始容量” 和 “加载因子”

容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。


HashMap基本操作:

public class TestHashMap {
    public static void main(String[] args) {

        HashMap<String, String> hashMap = new HashMap<String, String>();
        hashMap.put("fu", "辅助");
        hashMap.put("ad", "输出");
        hashMap.put("sd", "上单");

        System.out.println(hashMap);//toString重写了,所以可直接打出
        System.out.println("fu:" + hashMap.get("fu"));//拿出key为fu的键值
        System.out.println(hashMap.containsKey("fu"));//判断是否存在fu的键
        System.out.println(hashMap.keySet());//返回一个key集合。
        System.out.println("判空:"+hashMap.isEmpty());//判空

        hashMap.remove("fu");
        System.out.println(hashMap.containsKey("fu"));//判断是否存在fu的键

        Iterator it = hashMap.keySet().iterator();//遍历输出值。前提先拿到一个装载了key的Set集合
        while(it.hasNext()) {
            String key = (String)it.next();
            System.out.println("key:" + key);
            System.out.println("value:" + hashMap.get(key));
        }

    }
}

二、HashTable重要的部分源码分析:(基于jdk1.8)

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

    private static final long serialVersionUID = 362498820763181265L;
      // 默认的初始容量是16,必须是2的幂。 
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	// 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换) 
	static final int MAXIMUM_CAPACITY = 1 << 30;
	// 默认加载因子 
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //!!Java 8 HashMap的分离链表。 在没有降低哈希冲突的度的情况下,使用红黑书代替链表。
    /*
    使用链表还是树,与一个哈希桶中的元素数目有关。下面两个参数中展示了Java 8的HashMap在使用树和使用链表之间切换的阈值。当冲突的元素数增加到8时,链表变为树;当减少至6时,树切换为链表。中间有2个缓冲值的原因是避免频繁的切换浪费计算机资源。
    */
    static final int TREEIFY_THRESHOLD = 8;
	static final int UNTREEIFY_THRESHOLD = 6;

//HashMap的一个内部类,实现了Map接口的内部接口Entry。Map接口的一系列方法
	static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//哈希值
        final K key;
        V value;
        Node<K,V> next;//对下一个节点的引用(看到链表的内容,结合定义的Entry数组,哈希表的链地址法!!!实现)

        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; }//获取Key
        public final V getValue()      { return value; }//获取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数组,长度是2的幂。    
    // HashMap采用链表法解决冲突,每一个Entry本质上是一个单向链表    
    transient Node<K,V>[] table;
    //缓存我们装载的Node,每个结点。这也是跟keySet一样,可用于遍历HashMap。遍历使用这个比keySet是快多的喔,一会介绍并给例子。
    transient Set<Map.Entry<K,V>> entrySet;
    // HashMap的底层数组中已用槽的数量    
    transient int size;  
    // HashMap被改变的次数   
    transient int modCount;
    // HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)    
    int threshold;
    // 加载因子实际大小 
    final float loadFactor;

	/*
	构造器
	*/
	public HashMap(int initialCapacity, float loadFactor) {
	//初始容量不能<0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
	 //初始容量不能 > 最大容量值,HashMap的最大容量值为2^30
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
     //负载因子不能 < 0
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    //当在实例化HashMap实例时,如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是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;
    }
    //本质还是上面的构造器,只不过不选择加载因子而已
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //这里更是两个都不选,都选取默认的大小。
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
	}     
	//这个大概了解就是,可以用Map来构造HashMap咯
	public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    //那堆获取size,判空方法就不列了。
    // 获取key对应的value    
    public V get(Object key) {
        Node<K,V> e;
        // 获取key的hash值 
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab;
        Node<K,V> first, e; 
		 int n; 
		 K k;
		 // 在“该hash值对应的链表”上查找“键值等于key”的元素。也就是取出这个链表上,索引对应的值。
		 //这里一边判断一边赋值了,1.把要查的那一行table数组给到临时数组tab(那一行的table数组是通过hash计算得出的);2.把那一行的数组的第一个给到暂存结点first。(所谓第一个其实是单链表中头部,链表的头插法)    
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
             // 直接命中
            if (first.hash == hash && // 每次都是校验第一个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;
    }
    //红黑树查找法
    final TreeNode<K,V> getTreeNode(int h, Object k) {
            return ((parent != null) ? root() : this).find(h, k, null);
    }
     //根节点
    final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
    }
    /** 
     * 从根节点p开始查找指定hash值和关键字key的结点 
     * 当第一次使用比较器比较关键字时,参数kc储存了关键字key的 比较器类别 
     * 非递归式的树查询写法。。。有点复杂。
    */  
    final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;//一开始是根节点,然后遍历下去,表示当前结点
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
                if ((ph = p.hash) > h) //如果给定哈希值小于当前节点的哈希值,进入左节点  
                    p = pl;
                else if (ph < h)//如果大于当前节点,进入右结点  
                    p = pr;
                else if ((pk = p.key) == k || (k != null && k.equals(pk))) //如果哈希值相等,且关键字相等,则返回当前节点,终止查找。  
                    return p;
                else if (pl == null) //如果左节点为空,则进入右结点  
                    p = pr;
                else if (pr == null)//如果右结点为空,则进入左节点  
                    p = pl;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0) //如果不按哈希值排序,而是按照比较器排序,则通过比较器返回值决定进入左右结点  
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.find(h, k, kc)) != null)//如果在右结点中找到该关键字,直接返回
                    return q;
                else
                    p = pl;  //进入左节点  
            } while (p != null);
            return null;
    }
    //同理推出是否包含某key的方法
    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }
    /*
    插入方法!!为了好理解,我们先去看下文讲的jdk1.7的put吧,这个jdk1.8的红黑树、链式的转换太复杂了,一会回来再看。
    */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    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是否为空
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//创建一个新的table数组,用resize确定大小,并且获取该数组的长度
             //根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//如果对应的节点存在
            Node<K,V> e; K k;
            //判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
           //判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
            else if HashMap实现原理及源码分析

1.Java集合-HashMap实现原理及源码分析

HashMap实现原理及源码分析

HashMap实现原理及源码分析

HashMap实现原理及源码分析

Java源码——HashMap的源码分析及原理学习记录