本篇对HashMap实现的源码进行简单的分析。 所使用的HashMap源码的版本信息如下:

/*
* @(#)HashMap.java 1.73 07/03/13
*
* Copyright 2006 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/

一.概述

在Java中每一个对象都有一个哈希码,这个值可以通过hashCode()方法获得。hashCode()的值和对象的equals方法息息相关,是两个对象的值是否相等的依据,所以当我们覆盖一个类的equals方法的时候也必须覆盖hashCode方法。

例如String的hashCode方法为:

public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;

for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}

可以看得出,一个字符串的哈希值为s[0]31n-1 + s[1]31n-2 + … + s[n-1],是一个整数。也就是说所有的字符串可以通过hashCode()将其映射到整数的区间中,由于在java中整数的个数是有限的(四个字节有正负,第一位为符号位-231 ~ 231 -1),当s[0]31n-1 + s[1]31n-2 + … + s[n-1]足够大的时候可能会溢出,导致其变成负值。从上面的情况我们可以看出两个不同的字符串可能会被映射到同一个整数,发生冲突。因此java的开发人员选择了31这个乘数因子,尽量使得各个字符串映射的结果在整个java的整数域内均匀分布。

谈完java对象的哈希码,我们来看看今天的主角HashMap,HashMap可以看作是Java实现的哈希表。HashMap中存放的是key-value对,对应的类型为java.util.HashMap.Entry,所以在HashMap中数据都存放在一个Entry引用类型的数组table中。这里key是一个对象,为了把对象映射到table中的一个位置,我们可以通过求余法来,所以我们可以使用 [key的hashCode % table的长度]来计算位置(当然在实际操作的时候由于需要考虑table上的key的均匀分布可能需要对key的hashCode做一些处理)。

二.源码解析

相关属性 首先肯定是需要一个数组table,作为数据结构的骨干。

transient Entry[] table;

这边定义了一个Entry数组的引用。 继续介绍几个概念把

capacity容量 是指数组table的长度
loadFactor 装载因子,是实际存放量/capacity容量 的一个比值,在代码中这个属性是描述了装载因子的最大值,默认大小为0.75
threshold(阈值)代表hashmap存放内容数量的一个临界点,当存放量大于这个值的时候,就需要将table进行夸张,也就是新建一个两倍大的数组,并将老的元素转移过去。threshold = (int)(capacity * loadFactor);

put方法详解

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        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;
    }

在HashMap中我们的key可以为null,所以第一步就处理了key为null的情况。
当key为非null的时候,你也许会认为:恩,直接和table长度相除取模吧,但是这里没有,而是又好像做了一次哈希,这是为什么呢?这个还得先看indexFor(hash, table.length)方法,这个方法是决定存放位置的

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

明眼的都可以发现,因为在HashMap中table的长度为2n (我们把运算都换成二进制进行考虑),所以h & (length-1)就等价于h%length,这也就是说,如果对原本的hashCode不做变换的话,其除去低length-1位后的部分不会对key在table中的位置产生任何影响,这样只要保持低length-1位不变,不管高位如何都会冲突,所以就想办法使得高位对其结果也产生影响,于是就对hashCode又做了一次哈希

static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

当找到key所对应的位置的时候,对对应位置的Entry的链表进行遍历,如果以及存在key的话,就更新对应的value,并返回老的value。如果是新的key的话,就将其增加进去。modCount是用来记录hashmap结构变化的次数的,这个在hashmap的fail-fast机制中需要使用(当某一个线程获取了map的游标之后,另一个线程对map做了结构修改的操作,那么原先准备遍历的线程会抛出异常)。addEntry的方法如下

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

get方法

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

get方法其实就是将key以put时相同的方法算出在table的所在位置,然后对所在位置的链表进行遍历,找到hash值和key都相等的Entry并将value返回。

Java HashMap 核心源码解读的更多相关文章

  1. 3 手写Java HashMap核心源码

    手写Java HashMap核心源码 上一章手写LinkedList核心源码,本章我们来手写Java HashMap的核心源码. 我们来先了解一下HashMap的原理.HashMap 字面意思 has ...

  2. 6 手写Java LinkedHashMap 核心源码

    概述 LinkedHashMap是Java中常用的数据结构之一,安卓中的LruCache缓存,底层使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少 ...

  3. 小D课堂 - 新版本微服务springcloud+Docker教程_4-06 Feign核心源码解读和服务调用方式ribbon和Feign选择

    笔记 6.Feign核心源码解读和服务调用方式ribbon和Feign选择         简介: 讲解Feign核心源码解读和 服务间的调用方式ribbon.feign选择             ...

  4. 2 手写Java LinkedList核心源码

    上一章我们手写了ArrayList的核心源码,ArrayList底层是用了一个数组来保存数据,数组保存数据的优点就是查找效率高,但是删除效率特别低,最坏的情况下需要移动所有的元素.在查找需求比较重要的 ...

  5. 1 手写Java ArrayList核心源码

    手写ArrayList核心源码 ArrayList是Java中常用的数据结构,不光有ArrayList,还有LinkedList,HashMap,LinkedHashMap,HashSet,Queue ...

  6. Android版数据结构与算法(四):基于哈希表实现HashMap核心源码彻底分析

    版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 存储键值对我们首先想到HashMap,它的底层基于哈希表,采用数组存储数据,使用链表来解决哈希碰撞,它是线程不安全的,并且存储的key只能有一个为 ...

  7. 5 手写Java Stack 核心源码

    Stack是Java中常用的数据结构之一,Stack具有"后进先出(LIFO)"的性质. 只能在一端进行插入或者删除,即压栈与出栈 栈的实现比较简单,性质也简单.可以用一个数组来实 ...

  8. 解密虚拟 DOM——snabbdom 核心源码解读

    本文源码地址:https://github.com/zhongdeming428/snabbdom 对很多人而言,虚拟 DOM 都是一个很高大上而且远不可及的专有名词,以前我也这么认为,后来在学习 V ...

  9. 4.1 手写Java PriorityQueue 核心源码 - 原理篇

    本章先讲解优先级队列和二叉堆的结构.下一篇代码实现 从一个需求开始 假设有这样一个需求:在一个子线程中,不停的从一个队列中取出一个任务,执行这个任务,直到这个任务处理完毕,再取出下一个任务,再执行. ...

随机推荐

  1. 网络爬虫-url索引

    网络爬虫-url索引 http://www.cnblogs.com/yuandong/archive/2008/08/28/Web_Spider_Url_Index.html url索引的作用是判断一 ...

  2. 如何提高多线程程序的cpu利用率

    正如大家所知道的那样,多核多cpu越来越普遍了,而且编写多线程程序也是件很简单的事情.在Windows下面,调用CreateThread函数一次就能够以你想要的函数地址新建一个子线程运行.然后,事情确 ...

  3. GIT:本地有更改,但强制作远程仓库里作更新

    有时,紧急线上修改时,这个功能有用处的. git fetch --all git reset --hard origin/master ================ git reset --har ...

  4. 使用session技术来实现网上商城购物车的功能

    首先.简单的了解session和cookie的区别: 一.session和cookie的区别: session是把用户的首写到用户独占的session中(服务器端) cookie是把用户的数据写给用户 ...

  5. nginx做负载均衡配置文件

    nginx做负载均衡是在反向代理的基础上做的,代码如下: ## Basic reverse proxy server ## ## Apache backend for www.baidu.com ## ...

  6. 如何将sql server数据库转化成sqlite数据库

    今天在将sql server转化为sqlite的数据库的时候,遇到不少的问题,在网上搜了很长时间,都没有找到合适的软件将sql server转化成sqlite,其中用到了SqliteDev软件,在转化 ...

  7. Eclipse如何导出可执行jar包

    在编写shell脚本时用到了可执行的jar包,而jar包从Eclipse中导出时需要同时导出jar文件以及库文件夹,具体导出方式如下: (1)点击主方法所在的java,运行java applicati ...

  8. 安装Hadoop系列 — 导入Hadoop源码项目

    将Hadoop源码导入Eclipse有个最大好处就是通过 "ctrl + shift + r" 可以快速打开Hadoop源码文件. 第一步:在Eclipse新建一个Java项目,h ...

  9. 数据备份--dump

    数据中 心操作大量的数据.当遭到破坏时,这就是一场灾难.这时候需要备份来恢复,及时你又大量的备份数据都没用,备份也肯定不是在浪费时间.你也许很幸运从 来没有经历过数据丢失.但是, 由于这种事情极少发生 ...

  10. 在Ubuntu 12.04安装和设置SSH服务

    1.安装 Ubuntu缺省安装了openssh-client,所以在这里就不安装了,如果你的系统没有安装的话,再用apt-get安装上即可. 安装ssh-server sudo apt-get ins ...