从源码看HashMap键值对集合

时间:2022-09-17 07:35:10

之前我们看过了两种类型的集合,ArrayList集合和LinkedList集合,两种集合各有优势,我们不具体说了,但是本篇要看的集合可以完成它们完成不了的任务。比如:现有一篇文章,要你统计其中出现了哪些单词,每个单词总共出现了几次。这个问题很明显需要记录两个变量(某单词及其出现次数),但是我们之前介绍的集合都只能同时存储一种类型的变量,无法实现对应的效果。

     我们的HashMap又可以叫做键值对集合(官方名称映射),比如:

public class Test_Class {
    public static void main(String[] args){
        HashMap<String,Integer> map = new HashMap<String, Integer>();
        map.put("hello",10);
    }
}
/*第一条记录,保存了hello这个单词在文章中总共出现10次*/

一、超接口Map

          想要研究一个具体的类,先要看看它的父类或者父接口。HashMap实现了Map<K,V>接口,K表示键,V表示值,一个键对应一个值,整个集合中键是唯一的不可重复。

public interface Map<K,V> {
   int size();
   boolean isEmpty();
   boolean containsKey(Object key);
   boolean containsValue(Object value);
   V get(Object key);
   V put(K key, V value);
   V remove(Object key);
   void putAll(Map<? extends K, ? extends V> m);
   void clear();
   Set<K> keySet();
   Collection<V> values();
   Set<Map.Entry<K, V>> entrySet();
   ......
   //源码中方法很多,我们简单介绍几个常用的
}

接口中定义了 V get(Object key); 方法用来根据指定的键值,返回该键所对应的value值,有方法 V put(K key, V value); 添加指定键值和value的一条记录到集合中,有方法 V remove(Object key); 根据指定的value值删除一条或者多条记录等等,当然这些方法都只是声明,我们马上可以看看HashMap对这些方法的具体的实现是什么。



     二、HashMap的实现原理

          HashMap是Map的一种实现,它具体实现了上述Map接口中的所有方法。我们看看它的内部是以什么样的形式组织和存储内容的。

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

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    transient Node<K,V>[] table;
    transient int size;
    transient int modCount;
    final float loadFactor;

/*成员内部类,很重要的类*/
    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;
        }
    }
}

上述代码主要从代码的角度展现了HashMap的内部数据是以什么样的形式来存储的。我们可以看到,HashMap中定义了初始容量为16,最大容量,默认的 DEFAULT_LOAD_FACTOR(具体是什么下面再说) 参数值,实际上HashMap内部存储为数组加链表,定义了一个Node类数组,每个Node结点指向一个链表的头部串联整个链表。(单向链表)

从源码看HashMap键值对集合



          Node结点其实构成的是一个单向链表,每个结点由 final K key;,不可更改的键,可以修改的 V value; 值,还有指向下一个结点的指针Node<K,V> next; 组成,当然还包含了对结点的操作方法。



     三、添加元素到集合中

          我们使用put方法添加元素到集合中:

    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;
        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;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            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;
                    }
                    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;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

接下来我们来解释一下整体的调用过程,首先在调用put方法时候,调用了另外的一个方法 putVal(看起来十分恶心),传过去一个key个hash值,还有key和value,还有两个布尔参数(后面我们会知道他们的作用)。

          首先if判断这个HashMap是否是null,如果是的话,调用resize方法初始化一个HashMap(容量为16,loadFactor参数为0.75等其他参数值),然后 (p = tab[i = (n - 1) & hash]) == null 用我们想要插入的元素的hash值和HashMap容量取模判断将要存储的位置,因为我们知道每个对象的hash值是不同的,所以在插入元素的时候用他们的hash值和整个HashMap数组长度取模使得他们根据自己的hash存储在HashMap的固定位置上,如果我们将要插入节点的位置上是空的就 tab[i] = newNode(hash, key, value, null);,新建节点直接存放在此位置上。

从源码看HashMap键值对集合

假如我们使用hash取模之后得到的索引位置为3,发现此处为空,于是占用他。从源码看HashMap键值对集合



          我们接着看,刚才是预定的位置没被使用,那如果已经被占用了怎么办?

 if (p.hash == hash &&
 ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;

这行代码紧接着,对占用我们位置的结点键的值和我们的键的值进行比较,如果一样说明,这是对这条记录的修改,于是准备覆盖原位置的结点将p赋值给e。紧接着的一行代码可以先跳过设计到树,最后一个else中主要是将我们将要插入的结点链接到某个链表上。方法的尾部判断e是否为空,如果不为空说明在某条链表中找到与我们将要插入的结点的键的值一样的结点(就是需要覆盖原链表中某个结点的值),于是覆盖某个结点的值返回该结点的值。当然方法的最后对容量进行了判断,如果超过需要扩充的界限就会调用方法扩充HashMap数组的长度。

最后总结一下整个put方法的几个重要的过程:

  • 判断HashMap是否为空,是就初始化一个HashMap
  • 判断预定位置是否被占用,未被占用则占用
  • 预定位置被占用则判断是否有键值相等的结点,有则覆盖
  • 遍历之后没有找到键值相等的结点,就将此结点链接在某链表的末尾
  • 判断容量是否越界,是则扩充容量



    四、删除操作

         remove方法的源码如下:
 public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
 }
 /*主要还是调用方法removeNode*/
 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;
    }

我们具体看看代码,第一个if语句主要是判断:如果当前的HashMap不为null并且将要删除的结点的key和capcity取模后存在于HashMap数组中,就执行余下的代码。(主要还是为了保证将要删除的元素是存在的),接下来的一句if判断主要是,如果第一个位置的结点就是我们将要删除的结点的话,就将此结点赋值node,接下来就是遍历整个链表查找将要删除的键值,如果找到赋值给node。最后根据不同情况进行删除操作,如果p=node 说明将要删除的结点是第一个结点直接将此位置赋null即可,否则,将p的next赋null即可(经过遍历p始终指向将要删除的前一个结点),最后完成删除。

最后小结一下整个删除操作的过程:

  • 传入指定的键值,将键值及其hash值传入方法removeNode
  • 通过判断HashMap是否为null以及hash取模之后的位置是否为null,提高函数执行效率
  • 在链表中寻找键值,并且做好标记准备删除
  • 删除元素结点

至于get查找返回指定的值,是否包含指定键,是否包含指定值等方法类似。



          最后,因为HashMap存取没有顺序,如果对于一些需要按键存取数值的数据并且对存取顺序没有要求的话,可以考虑使用HashMap提升效率。

本篇文章是博主查看jdk源码总结而来,如果有不当之处,望大家指出来。

从源码看HashMap键值对集合的更多相关文章

  1. 【JDK1&period;8】JDK1&period;8集合源码阅读——HashMap

    一.前言 笔者之前看过一篇关于jdk1.8的HashMap源码分析,作者对里面的解读很到位,将代码里关键的地方都说了一遍,值得推荐.笔者也会顺着他的顺序来阅读一遍,除了基础的方法外,添加了其他补充内容 ...

  2. 浅析Java源码之HashMap

    写这篇文章还是下了一定决心的,因为这个源码看的头疼得很. 老规矩,源码来源于JRE1.8,java.util.HashMap,不讨论I/O及序列化相关内容. 该数据结构简介:使用了散列码来进行快速搜索 ...

  3. JDK源码解析---HashMap源码解析

    HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. HashMap是非线程安全的,只是 ...

  4. jdk1&period;8&period;0&lowbar;45源码解读——HashMap的实现

    jdk1.8.0_45源码解读——HashMap的实现 一.HashMap概述 HashMap是基于哈希表的Map接口实现的,此实现提供所有可选的映射操作.存储的是<key,value>对 ...

  5. JDK8源码解析 -- HashMap(一)

    最近一直在忙于项目开发的事情,没有时间去学习一些新知识,但用忙里偷闲的时间把jdk8的hashMap源码看完了,也做了详细的笔记,我会把一些重要知识点分享给大家.大家都知道,HashMap类型也是面试 ...

  6. JDK1&period;8源码学习-HashMap

    JDK1.8源码学习-HashMap 目录 一.HashMap简介 HashMap 主要用来存放键值对,它是基于哈希表的Map接口实现的,是常用的Java集合之一. 我们都知道在JDK1.8 之前 的 ...

  7. 从源码看Azkaban作业流下发过程

    上一篇零散地罗列了看源码时记录的一些类的信息,这篇完整介绍一个作业流在Azkaban中的执行过程,希望可以帮助刚刚接手Azkaban相关工作的开发.测试. 一.Azkaban简介 Azkaban作为开 ...

  8. 解密随机数生成器(二)——从java源码看线性同余算法

    Random Java中的Random类生成的是伪随机数,使用的是48-bit的种子,然后调用一个linear congruential formula线性同余方程(Donald Knuth的编程艺术 ...

  9. 从Chrome源码看浏览器的事件机制

    .aligncenter { clear: both; display: block; margin-left: auto; margin-right: auto } .crayon-line spa ...

随机推荐

  1. tp框架之分页与第三方类的应用

    1.先把分页类放在根目录下,比如放在某个模块下 2.在类里面写入命名空间,注意类名的格式(类名要与里面的方法名一致) 3.在需要的方法里面按照路径进行实例化,然后就可以使用了 方法: public f ...

  2. WCF入门(二)-----实战开发

    在这个实战中我们将使用DataContract,ServiceContract来构建WCF服务,并使用VS2008内置的“WCFSVCHost”运行我们创建的WCF服务,并使用“WCF测试客户端”来测 ...

  3. iOS开发:UILabel无法响应点击事件的问题

    UILabel 是默认关闭用户的交互的 即: _contentLabel.userInteractionEnabled = NO; 修改之后: _contentLabel.userInteractio ...

  4. 部署LNMP架构Blog博客平台 ---惟净

    部署环境:VM虚拟机 操作系统:CentOS-6.8-x64 IP地址:192.168.31.91Mysql数据库版本:5.6.34 Cmake软件包版本:3.5.2Nginx软件包版本:1.10.2 ...

  5. 对象的创建过程(chapter5&period;7&period;3)

    总结一下对象的创建过程,假设有一个名为Dog的类: 1. 即使没有显示地使用static关键字,构造器实际上也是静态的方法,因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类 ...

  6. matlab错误:Subscript indices must either be real positive integers or logicals&period;

    matlab错误:Subscript indices must either be real positive integers or logicals. 中文解释:下标索引必须是正整数类型或者逻辑类 ...

  7. nginx转发tomcat请求转成https后页面不能下载apk文件而是直接打开

    访问域名下面的apk文件 https://xxxx/xxx.apk 浏览器没有下载而是直接打开了文件 没有找到问题原因,可能是https的原因,要是用http就可以下载,转发https就有问题 后来是 ...

  8. 2013 QCon北京演讲:跨终端的WebKit渲染机制

    转载请注明原文地址:http://blog.csdn.net/milado_nju 1. 该演讲主要介绍WebKit的渲染机制的内部工作原理和一些新的技术,特别是针对不断出现的多种终端所做的一些努力. ...

  9. Redis主从集群及哨兵模式

    本次实验环境准备用一台服务器模拟3台redis服务器,1主2从 主从集群搭建 第一步:安装Redis 安装Redis,参考前面安装Redis文章,保证单机使用没有问题. 第二步:配置服务器文件 定位到 ...

  10. Windows PowerShell基本语法及常用命令

    PowerShell常用命令: 一 Get类 1.Get-Command : 得到所有PowerShell命令,获取有关 cmdlet 以及有关 Windows PowerShell 命令的其他元素的 ...