之前我们看过了两种类型的集合,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结点指向一个链表的头部串联整个链表。(单向链表)





          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);,新建节点直接存放在此位置上。

假如我们使用hash取模之后得到的索引位置为3,发现此处为空,于是占用他。



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

 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.8】JDK1.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.8.0_45源码解读——HashMap的实现

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

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

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

  6. JDK1.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. JavaScript 开发工具webstrom使用指南

    本文给大家推荐了一款非常热门的javascript开发工具webstrom,着重介绍了webstrom的特色功能.设置技巧.使用心得以及快捷键汇总,非常的全面. 看到网上一篇介绍webstrom的文章 ...

  2. nginx集群tomcat

    一.准备工作 下载nginx,http://nginx.org/,本文采用nginx-1.8.0,下载之后直接解压,免安装 下载tomcat,以配置3台tomcat服务器做负载均衡为例 二.修改tom ...

  3. DBGrid 各属性的设置

    在 Delphi 语言的数据库编程中,DBGrid 是显示数据的主要手段之一.但是 DBGrid 缺省的外观未免显得单调和缺乏创意.其实,我们完全可以在我们的程序中通过编程来达到美化DBGrid 外观 ...

  4. redhat6.4下安装Oracle11g

    一.在Root用户下执行以下步骤: 1)修改用户的SHELL的限制,修改/etc/security/limits.conf文件 *               soft    nproc  2047 ...

  5. SVG的内部事件添加

    SVG的内部事件添加: <%@ page language="java" contentType="text/html; charset=UTF-8" p ...

  6. WPF ResourceDictionary的使用

    作用:一个应用程序中,某个窗口需要使用样式,但是样式非常多,写在一个窗口中代码分类不方便.最好Style写在专门的xaml文件中,然后引用到窗口中,就像HTML引用外部css文件一样. 初衷:就在于可 ...

  7. Flex移动皮肤开发(二)

    范例文件 mobile-skinning-part2.zip 在这个讨论创建 Flex 移动 skin 的系列的 第 1 部分 中,我讨论了 Flex 团队在 Mobile 主题中所做的性能优化的原理 ...

  8. Jquery右击显示菜单事件,运用smartMenu插件。

    基本格式: 1.引用jquery.smartMenu插件.css样式: <script src="gongju/jquery-1.11.2.min.js" type=&quo ...

  9. OpenCV 3.2正式发布啦

    2016年12月23号OpenCV社区宣布了OpenCV3.2版本正式发布,这个是在OpenCV3.1版本发布一年以后再次升级.在3.2版本中有总数超过数千个的改进与修正,是OpenCV3.x系列中最 ...

  10. win10下VS2015局域网调试配置

    一.前言 换win10页挺久了一直没有使用 IISExpress 的局域网功能,今天一使用才发现 win10 比起 win7 下配置多了许多坑. 二.配置步骤 首先我们先来拿到本机 ip 地址 打开命 ...