java读源码 之 map源码分析(HashMap,图解)一

简介: java读源码 之 map源码分析(HashMap,图解)一

开篇之前,先说几句题外话,写博客也一年多了,一直没找到一种好的输出方式,博客质量其实也不高,很多时候都是赶着写出来的,最近也思考了很多,以后的博客也会更注重质量,同时也尽量写的不那么生硬,能让大家在轻松的氛围中学习到知识才是最好的


好了,闲话不再多说,进入我们今天的主题,HashMap能说的东西太多了,不管是其数据接口,算法,还是单纯的源码分析,不过我们还是直接从源码入手,进而分析其数据结构及算法


通过本篇,你将了解以下问题:


1.HashMap的结构是什么?

2.HashMap的存储数据的逻辑是什么?


在分析之前,我们先要对HashMap的接口有一个大概的了解,这要可以帮我们更好的理解源码,然后通过源码的学习,我们又能对它所持有的数据接口有更深的理解,嗯?是不是很有道理?


HashMap实际上是一个散列表的数据结构,即数组和链表的结合体。这样的结构结合了链表在增删方面的高效和数组在寻址上的优势

image.png

如上所示,当我们添加一个元素到HashMap中时,首先经过一定算法,计算出该元素应该放到数组中哪个位置,如果在该位置上已经有元素了,就将其链接到元素的尾部,这样就形成了一个链表的结构。


HashMap的源码体积比较大,如果还按之前我们分析其他容易那种方法的话,实在不知道从何写起,所以这篇文章,我们从实际的例子出发,一步步深入进去了解HashMap

public class HashMain {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("1", "java");
        map.put("2", "c++");
        map.put("3", "c#");
        map.put("4", "python");
        map.put("5", "php");
        map.put("6", "js");
        System.out.println(map);
    }
}

我们打个断点看下:

image.png

可以看到,HashMap中存储元素的是它的一个内部类Node,我们一起来看下这到底是个什么玩意儿?

static class Node<K,V> implements Map.Entry<K,V> {
        // key对应的hash值
        final int hash;
        final K key;
        V value;
        // 通过next指针,保存了下个节点的元素,看到这个我们也能知道,
      // 不同于LinkedList,这是个单向链表
        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; }
      // 通过键值的hash值进行异或操作得到这个Node的hashCode
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
      // 必须要为 Map.Entry 且 key跟value都相等的时候才会返回true
        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接口实现, Map接口中的一个内部类Entry,我们继续看看Entry又是个什么东西呢?

interface Entry<K,V> {
        /**
         * 返回了这个Entry对应的key
         */
        K getKey();
        /**
         * 返回对应的value
         */
        V getValue();
        /**
         * 覆盖老的value值
         */
        V setValue(V value);
        boolean equals(Object o);
        int hashCode();
        /**
     * 1.8新增的方法,返回一个采用自然排序比较key的比较器
         */
        public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
            // 这个与操作代表了返回的这个比较器实现了Serializable接口,是可序列化的
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getKey().compareTo(c2.getKey());
        }
        /**
     * 1.8新增的方法,返回一个采用自然排序比较value的比较器
         */
        public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
            // 这个与操作代表了返回的这个比较器实现了Serializable接口,是可序列化的
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getValue().compareTo(c2.getValue());
        }
        /**
     * 1.8新增的方法,返回一个采用给定比较器比较key的比较器
         */
        public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
        }
       /**
     * 1.8新增的方法,返回一个采用给定比较器比较value的比较器
         */
        public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
        }
    }

分析完Node之后,我们可以知道,Node就是我们之前所说的数组+链表中的“链表”,并且它还是一个单向的链表。

OK,为了更好的理解其数据结构,我们现在来分析它存入数据的方法,也就是put方法,看看一次数据的存储到底经过了什么?

// 将KV键值对存入map中,如果map中已经包含了这个key,那么value会被替换
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  /**
     * @param key的hash值
     * @param key
     * @param value
     * @param onlyIfAbsent 如果为true的话,不去改变已经存在的value
     * @param evict 在HashMap中这个值没什么用,我们分析LinkedHashMap时会用到它
     * @return 
     */ 
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果HashMap中的table尚未初始化或者长度为0,则将其进行扩容到初始长度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    // 如果计算出新增节点的将要放入的位置上还没有被占用,就直接创建一个新的节点放入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 如果位置已经被占用,但将要放入的元素key跟原本的元素相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 之后会根据onlyIfAbsent进行判断,如果为false的话,会对原节点的value直接进行覆盖
                e = p;
            else if (p instanceof TreeNode)
                // 如果是一个TreeNode,调用对应的方法,这个在之后的文章中再分析
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 说明是一个Node
                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相等的话,就直接break
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 如果onlyIfAbsent为false,或者旧值为null的话,进行替换
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

分析完后, 我们总结下它的逻辑:


  1. 对key的hashCode()做一次散列(hash函数,具体内容下一篇讲解),然后根据这个散列值计算index(i = (n - 1) & hash)这个表达式我们再ArrayDequeue中已经介绍过了,相当于模运算
  2. 如果没有发生碰撞(哈希冲突),则直接放到数组中;
  3. 如果碰撞了,以链表的形式挂在数组对应的元素后;
  4. 如果因为碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
  5. 如果节点已经存在就替换old value(保证key的唯一性)
  6. 如果数组中存储的元素达到了阈值(超过负载因子*当前容量),就要resize(重新调整大小并重新散列)。

接下来,我们来分析它的get方法

public V get(Object key) {
    Node<K,V> e;
    // 通过key的hash值找到对应的Node节点,如果没有的话返回null,存在的话返回node节点的value
    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;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果table已经完成了初始化,并且经过散列后的位置上的元素不为null的话
        if (first.hash == hash && 
            ((k = first.key) == key || (key != null && key.equals(k))))
            // 正好key的值跟散列后数组上对应位置的节点的key相等,直接返回这个节点
            return first;
        if ((e = first.next) != null) {
            // 如果是TreeNode,去树中查找
            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;
}

我们也分析下获取元素时的逻辑:


  1. 根据传入key进行hash运算,将运算后的值跟数组长度进行模运算,得出这个key对应数组的位置
  2. 如果key的hash值正好等于数组上这个元素的key的hash值的话,直接返回数组上这个位置的元素
  3. 如果不相等,就在这个节点下挂的树或者链表中查询对应的key(有树或链表的情况下)
  4. 都不符合,返回null

这篇文章到这里就暂时结束啦~,HashMap比较难讲清楚,这篇文章也只是做一个开篇,能让大家对其大概有一个认设就是再好不过了,后续还会继续写HashMap,希望大家多多指教,动动小手点个赞啦


相关文章
|
1天前
|
Java
【源码】【Java并发】【ConcurrentHashMap】适合中学体质的ConcurrentHashMap
本文深入解析了ConcurrentHashMap的实现原理,涵盖JDK 7与JDK 8的区别、静态代码块、构造方法、put/get/remove核心方法等。JDK 8通过Node数组+链表/红黑树结构优化并发性能,采用CAS和synchronized实现高效锁机制。文章还详细讲解了hash计算、表初始化、扩容协助及计数更新等关键环节,帮助读者全面掌握ConcurrentHashMap的工作机制。
26 6
|
4天前
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
36 5
|
5天前
|
Java
【源码】【Java并发】【LinkedBlockingQueue】适合中学体质的LinkedBlockingQueue入门
前言 有了前文对简单实用的学习 【Java并发】【LinkedBlockingQueue】适合初学体质的LinkedBlockingQueue入门 聪明的你,一定会想知道更多。哈哈哈哈哈,下面主播就...
34 6
【源码】【Java并发】【LinkedBlockingQueue】适合中学体质的LinkedBlockingQueue入门
|
6天前
|
安全 Java
【源码】【Java并发】【ArrayBlockingQueue】适合中学者体质的ArrayBlockingQueue
前言 通过之前的学习是不是学的不过瘾,没关系,马上和主播来挑战源码的阅读 【Java并发】【ArrayBlockingQueue】适合初学体质的ArrayBlockingQueue入门 还有一件事
39 5
【源码】【Java并发】【ArrayBlockingQueue】适合中学者体质的ArrayBlockingQueue
|
7天前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
74 14
|
10天前
|
Java
【源码】【Java并发】【ReentrantLock】适合中学者体质的ReentrantLock源码阅读
因为本文说的是ReentrantLock源码,因此会默认,大家对AQS有基本的了解(比如同步队列、条件队列大概> 长啥样?)。 不懂AQS的小朋友们,你们好呀!也欢迎先看看这篇
56 13
【源码】【Java并发】【ReentrantLock】适合中学者体质的ReentrantLock源码阅读
|
12天前
|
Java 中间件 调度
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
48 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
|
13天前
|
存储 Java
【源码】【Java并发】【ThreadLocal】适合中学者体质的ThreadLocal源码阅读
前言 下面,跟上主播的节奏,马上开始ThreadLocal源码的阅读( ̄▽ ̄)" 内部结构 如下图所示,我们可以知道,每个线程,都有自己的threadLocals字段,指向ThreadLocalMap
360 81
【源码】【Java并发】【ThreadLocal】适合中学者体质的ThreadLocal源码阅读
|
14天前
|
Java
【源码】【Java并发】【AQS】从ReentrantLock、Semaphore、CutDownLunch、CyclicBarrier看AQS源码
前言 主播觉得,AQS的原理,就是通过这2个队列的协助,实现核心功能,同步队列(CLH队列)和条件队列(Condition队列)。 同步队列(CLH队列) 作用:管理需要获...
59 18
【源码】【Java并发】【AQS】从ReentrantLock、Semaphore、CutDownLunch、CyclicBarrier看AQS源码
|
1月前
|
JavaScript Java Docker
干货含源码!如何用Java后端操作Docker(命令行篇)
只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~