`

HashMap 底层算法分析

阅读更多
Hash算法

HashMap使用Hash算法,所以在解剖HashMap之间,需要先简单的了解Hash算法,Hash算法一般也成为散列算法,通过散列算法将任意的值转化成固定的长度输出,该输出就是散列值,这是一种压缩映射,也就是,散列值的空间远远小于输入的值空间。
简单的说,hash算法的意义在于提供了一种快速存取数据的方法,它用一种算法建立键值与真实值之间的对应关系,(每一个真实值只能有一个键值,但是一个键值可以对应多个真实值),这样可以快速在数组等里面存取数据。

下面我们建立一个HashMap,然后往里面放入12对key-value,这个HashMap的默认数组长度为16,我们的key分别存放在该数组的格子中,每个格子下面存放的元素又是以链表的方式存放元素。

    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("What", "chenyz");
        map.put("You", "chenyz");
        map.put("Don't", "chenyz");
        map.put("Know", "chenyz");
        map.put("About", "chenyz");
        map.put("Geo", "chenyz");
        map.put("APIs", "chenyz");
        map.put("Can't", "chenyz");
        map.put("Hurt", "chenyz");
        map.put("you", "chenyz");
        map.put("google", "chenyz");
        map.put("map", "chenyz");
        map.put("hello", "chenyz");
    }

当我们新添加一个元素时,首先我们通过Hash算法计算出这个元素的Hash值的hashcode,通过这个hashcode的值,我们就可以计算出这个新元素应该存放在这个hash表的哪个格子里面,如果这个格子中已经存在元素,那么就把新的元素加入到已经存在格子元素的链表中。

运行上面的程序,我们对HashMap源码进行一点修改,打印出每个key对象的hash值

What-->hash值:8
You-->hash值:3
Don't-->hash值:7
Know-->hash值:13
About-->hash值:11
Geo-->hash值:12
APIs-->hash值:1
Can't-->hash值:7
Hurt-->hash值:1
you-->hash值:10
google-->hash值:3
map-->hash值:8
hello-->hash值:0

计算出来的Hash值分别代表该key应该存放在Hash表中对应数字的格子中,如果该格子已经有元素存在,那么该key就以链表的方式依次放入格子中



从上表可以看出,Hash表是线性表和链表的综合所得,根据数据结构的定义,可以得出粗劣的结论,Hash算法的存取速度要比数组差一些,但是比起单纯的链表,在查找和存取方面却要好多。

如果要查找一个元素时,同样的方式,通过Hash函数计算出这个元素的Hash值hashcode,然后通过这个hashcode值,直接找到跟这个hash值相对应的线性格子,进如该格子后,对这个格子存放的链表元素逐个进行比较,直到找到对应的hash值。

在简单了解完Hash算法后,我们打开HashMap源码

初始化HashMap

下面我们看看Map map = new HashMap();这段代码究竟做了什么,发生了什么数据结构的变化。

HashMap中几个重要的属性

transient Entry[] table;
用来保存key-value的对象Entry数组,也就是Hash表

transient int size;
返回HashMap的键值对个数

final float loadFactor;
负载因子,用来决定Entry数组是否扩容的因子,HashMap默认是0.75f

int threshold;
重构因子,(capacity * load factor)负载因子与Entry[]数组容积的乘值

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

    final float loadFactor;

    transient Entry[] table;

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    static final int DEFAULT_INITIAL_CAPACITY = 16;

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

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }
以public HashMap(int initialCapacity, float loadFactor)构造函数为例,另外两个构造函数实际上也是以同种方式来构建HashMap.

首先是要确定hashMap的初始化的长度,这里使用的策略是循环查出一个大于initialCapacity的2的次方的数,例如initialCapacity的值是10,那么大于10的数是2的4次方,也就是16,capacity的值被赋予了16,那么实际上table数组的长度是16,之所以采用这样的策略来构建Hash表的长度,是因为2的次方运算对于计算机来说是有相当的效率。

loadFactor,被称为负载因子,HashMap的默认负载因子是0.75f

threshold,接下来是重构因子,由负载因子和容量的乘机组成,它表示当HashMap元素被存放了多少个之后,需要对HashMap进行重构。

通过这一系列的计算和定义后,初始化Entry[] table;

put(key,value)

接下来看一对key-value是如何被存放到HashMap中:put(key,value)

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        
        int i = indexFor(hash, table.length);
        System.out.println(key+"-->hash值:"+i);//这就是刚才程序打印出来的key对应hash值
        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++;
        addEntry(hash, key, value, i);
        return null;
    }

    static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

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

这里是整个hash的关键,请打开源码查看一步一步查看。

hash(key.hashCode()) 计算出key的hash码 //对于hash()的算法,这里有一篇分析很透彻的文章<HashMap hash方法分析>
indexFor(hash, table.length) 通过一个与算法计算出来,该key应在存放在Hash表的哪个格子中。
for (Entry<K,V> e = table[i]; e != null; e = e.next) 然后再遍历table[i]格中的链表,判断是否已经存在一样的key,如果存在一样的key值,那么就用新的value覆盖旧的value,并把旧的value值返回。
addEntry(hash, key, value, i) 如果经过遍历链表没有发现同样的key,那么进行addEntry函数的操作,增加当前key到hash表中的第i个格子中的链表中

    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)
            resize(2 * table.length);
    }

Entry<K,V> e = table[bucketIndex];  创建一个Entry对象来存放键值(ps:Entry对象是一个链表对象)
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 将Entry对象添加到链表中
if (size++ >= threshold) resize(2 * table.length); 最后将size进行自增,判断size值是否大于重构因子,如果大于那么就是用resize进行扩容重构。

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

这里为什么是否需要扩容重构,其实是涉及到负载因子的性能问题

loadFactor负载因子
上面说过loadFactor是一个hashMap的决定性属性,HashSet和HashMap的默认负载因子都是0.75,它表示,如果哈希表的容量超过3/4时,将自动成倍的增加哈希表的容量,这个值是权衡了时间和空间的成本,如果负载因子较高,虽然会减少对内存空间的需求,但也会增加查找数据的时间开销,无论是put()和get()都涉及到对数据进行查找的动作,所以负载因子是不适宜设置过高



get(key)

接下来看看get(key)做了什么

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

这些动作似乎是跟put(key,value)相识,通过hash算法获取key的hash码,再通过indexFor定位出该key存在于table的哪一个下表,获取该下标然后对下标中的链表进行遍历比对,如果有符合就直接返回该key的value值。

keySet()

这里还涉及另一个问题,上面说了HashMap是跟set没有任何亲属关系,但map也一样实现了keySet接口,下面谱析一下keySet在hashMap中是如何实现的,这里给出部分代码,请结合源码查看

public K next() {
            return nextEntry().getKey();
        }

    final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();

            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        current = e;
            return e;
        }

代码很简单,就是对每个格子里面的链表进行遍历,也正是这个原因,当我们依次将key值put进hashMap中,但在使用map.entrySet().iterator()进行遍历时候却不是put时候的顺序。

扩容
在前面说到put函数的时候,已经提过了扩容的问题

if (size++ >= threshold)
resize(2 * table.length);


这里一个是否扩容的判断,当数据达到了threshold所谓的重构因子,而不是HashMap的最大容量,就进行扩容。

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

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


transfer方法实际上是将所有的元素重新进行一些hash,这是因为容量变化了,每个元素相对应的hash值也会不一样。

使用HashMap

1.不要再高并发中使用HashMap,HashMap是线程不安全,如果被多个线程共享之后,将可能发生不可预知的问题。
2.如果数据大小事固定的,最好在初始化的时候就给HashMap一个合理的容量值,如果使用new HashMap()默认构造函数,重构因子的值是16*0.75=12,当HashMap的容量超过了12后,就会进行一系列的扩容运算,重建一个原来成倍的数组,并且对原来存在的元素进行重新的hash运算,如果你的数据是有成千上万的,那么你的成千上万的数据也要跟这你的扩容不断的hash,这将产生高额的内存和cpu的大量开销。

当然啦,HashMap的函数还有很多,不过都是基于table的链表进行操作,当然也就是hash算法,Map & hashMap在平时我们的应用非常多,最重要的是我们要对每句代码中每块数据结构变化心中有数。
分享到:
评论

相关推荐

    hashmap面试题_hashmap_

    二、HashMap底层原理 HashMap的内部实现基于数组+链表/红黑树的结构。数组中的每个元素都是一个Entry对象,每个Entry包含键值对和指向下一个Entry的引用。当冲突较多导致链表过长时,会自动转换为红黑树,以保证查找...

    Java8 HashMap扩容算法实例解析

    通过分析 Java 8 HashMap 的源代码,可以看到 HashMap 底层数组的长度都是 2 的幂次方值。在插入元素和扩容移位的过程中,HashMap 使用 (n - 1) & hash 来计算元素的位置,并使用 lo 链表和 hi 链表来存储元素。 7....

    深入解读大厂java面试必考点之HashMap全套学习资料

    HashMap是Java编程语言中最常用的集合类之一,尤其在面试中,HashMap的相关知识是考察候选人对数据结构和算法理解的重要部分。本套学习资料全面涵盖了HashMap的深入解析,旨在帮助求职者掌握大厂面试中的核心知识点...

    通过 HashMap、HashSet 的源代码分析其 Hash 存储机制1

    本文将深入分析HashMap和HashSet的哈希存储机制。 首先,HashSet是一个基于哈希表实现的无序不重复元素集合。它通过计算对象的哈希码来决定元素的存储位置,从而确保了快速的查找和插入操作。在HashSet中,元素的...

    Java HashMap实现原理分析(一)

    首先,HashMap的底层数据结构是一个动态扩容的数组,称为`table`,类型为`Node, V&gt;[]`。`Node`是一个内部类,代表键值对的数据结构,包含键和值以及指向下一个节点的引用,用于处理哈希冲突。当用户调用`put`方法时...

    逆向-音乐学家方大刚-快速定位hashMap

    总的来说,逆向HashMap涉及到深厚的编程基础、逆向工程技能和对底层数据结构的理解。对于想要深入学习逆向工程或提高逆向分析能力的人来说,这样的主题是非常有价值的。而从`com.dodonew.online.apk`这个文件中提取...

    hashmap:R中更快的哈希图

    HashMap则通过更高效的哈希算法和冲突解决策略提高了这些操作的速度。 Rcpp是一个强大的工具,允许R与C++之间无缝交互,使得开发高性能的R扩展包成为可能。HashMap库就是利用Rcpp将C++的高效哈希表实现引入到R中。...

    Java集合相关面试题

    本文将对Java集合相关面试题进行总结和分析,涵盖List和Map相关的面试题,包括ArrayList、LinkedList、HashMap、ConcurrentHashMap等,并对数据结构和算法复杂度分析进行讲解。 在讲解Java集合之前,我们首先需要...

    数据结构和算法详细分析用C-python-java详解.rar

    通过阅读《数据结构与算法分析--C语言描述》、《数据结构与算法分析——Java语言描述》和《数据结构与算法:Python语言描述》这三本书,你将能够掌握这些关键概念,并能够在实际项目中灵活运用。

    java面试难点讲解:hashmap,spring aop,classload,dubbo,zookeeper,session等。

    面试必考之HashMap源码分析与实现 探索JVM底层奥秘ClassLoader源码分析与案例讲解 面试必备技能之Dubbo企业实战 分布式框架Zookeeper之服务注册与订阅 互联网系统垂直架构之Session解决方案 分库分表之后分布式下...

    Collection E、Map K,V接口底层实现类数据结构的区别和联系.pdf

    在分析这两个接口及其底层实现类的数据结构的区别和联系之前,首先需要了解Java集合框架的构成和各个接口、类的作用。 Collection接口是单列集合的根接口,它包含了List、Set以及Queue等子接口。List接口下的集合...

    面试真题目录大全,详细版

    2. 新浪网站和头条的新闻推荐算法分析 其他 1. JVM 的参数调整(Xss、Xmx) 2. JVM 的垃圾收集器(CMS、G1等) 3. ThreadLocal 的作用 4. SpringMVC 的 @Autowired 注解底层实现 5. Tomcat 的默认最大线程数 本...

    Java面试专属视频

    大型公司面试必答之数据结构与算法精讲 探索JVM底层奥秘ClassLoader源码分析与案例讲解 .锁、分布式锁、无锁实战全局性IDSpringMvc深入理解源码分析 Nosql Redis Jedis常用命令 JAVA并发编程之多线程并发同步业务...

    并发编程atomic&collections-课上笔记1

    本文主要讲述了 Java 中的并发编程,包括 atomic 包的介绍、CAS 算法的原理、ABA 问题的解决方案,以及 collections 中的 HashMap、HashTable 和 ConcurrentHashMap 的源码分析。 Atomic 包的介绍 ----------------...

    6本java算法书

    这使得C语言在理解和学习数据结构和算法时,对底层原理的理解更为深刻。 这六本书籍的内容可能涵盖了这些基础概念的深入探讨,包括但不限于:排序和搜索算法的原理及优化、树和图的遍历方法、递归和动态规划的应用...

    Java数据结构和算法(英文)

    这个资料包中的"数据结构与算法分析(Java版).pdf"很可能是Mark Allen Weiss的经典著作,该书深入浅出地讲解了数据结构和算法,并以Java为实现语言,是Java程序员学习此领域的宝贵资源。阅读这本书将有助于提升你对...

    大型国企内部Java面试题

    主要是Java后端的,16K左右的,涉及SE、WEB、三大框架SSM、springboot、MQ、数据库、springcloud、JVM、Redis、多线程、hashmap的底层、面试技巧等 SSM涉及浅层的底层,如IOC、AOP,专为没看过源码的人应付面试准备...

    Java和C的算法题

    5. **时间复杂度与空间复杂度**:在解决算法问题时,分析其时间复杂度和空间复杂度是评估算法效率的关键。Java和C都要求开发者对这些概念有深入的理解,以便优化算法,避免不必要的性能开销。 6. **递归与迭代**:...

    Java算法题,数据结构分析和实现.zip

    在本压缩包“Java算法题,数据结构分析和实现.zip”中,主要涵盖了与数据结构、算法以及编程语言(如Java、C/C++、Python)相关的学习资源,特别适合大学生和编程初学者深入理解这些核心概念。以下是这些知识点的...

    LZW算法说明 及 C与 Java实现

    在提供的文件中,"LZW数据压缩算法的原理分析_中文.doc"可能是对LZW算法的详细解释,适合初学者了解算法的工作机制;"LZW算法说明_英文.doc"可能是更深入的技术文档,可能包含算法的优化和变种;"LZW_JAVA"和"LZW_C...

Global site tag (gtag.js) - Google Analytics