详解HashMap的内部工作原理
本文将用一个简单的例子来解释下HashMap内部的工作原理。首先我们从一个例子开始,而不仅仅是从理论上,这样,有助于更好地理解,然后,我们来看下get和put到底是怎样工作的。
我们来看个非常简单的例子。有一个”国家”(Country)类,我们将要用Country对象作为key,它的首都的名字(String类型)作为value。下面的例子有助于我们理解key-value对在HashMap中是如何存储的。
1. Country.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
package org.arpit.javapostsforlearning; public class Country { String name; long population; public Country(String name, long population) { super (); this .name = name; this .population = population; } public String getName() { return name; } public void setName(String name) { this .name = name; } public long getPopulation() { return population; } public void setPopulation( long population) { this .population = population; } // If length of name in country object is even then return 31(any random number) and if odd then return 95(any random number). // This is not a good practice to generate hashcode as below method but I am doing so to give better and easy understanding of hashmap. @Override public int hashCode() { if ( this .name.length()% 2 == 0 ) return 31 ; else return 95 ; } @Override public boolean equals(Object obj) { Country other = (Country) obj; if (name.equalsIgnoreCase((other.name))) return true ; return false ; } } |
如果想了解更多关于Object对象的hashcode和equals方法的东西,可以参考:java中的hashcode()和equals()方法
2. HashMapStructure.java(main class)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
import java.util.HashMap; import java.util.Iterator; public class HashMapStructure { /** * @author Arpit Mandliya */ public static void main(String[] args) { Country india= new Country( "India" , 1000 ); Country japan= new Country( "Japan" , 10000 ); Country france= new Country( "France" , 2000 ); Country russia= new Country( "Russia" , 20000 ); HashMap<country,string> countryCapitalMap= new HashMap<country,string>(); countryCapitalMap.put(india, "Delhi" ); countryCapitalMap.put(japan, "Tokyo" ); countryCapitalMap.put(france, "Paris" ); countryCapitalMap.put(russia, "Moscow" ); Iterator<country> countryCapitalIter=countryCapitalMap.keySet().iterator(); //put debug point at this line while (countryCapitalIter.hasNext()) { Country countryObj=countryCapitalIter.next(); String capital=countryCapitalMap.get(countryObj); System.out.println(countryObj.getName()+ "----" +capital); } } } |
现在,在第23行设置一个断点,在项目上右击->调试运行(debug as)->java应用(java application)。程序会停在23行,然后在countryCapitalMap上右击,选择“查看”(watch)。将会看到如下的结构:
从上图可以观察到以下几点:
有一个叫做table大小是16的Entry数组。
这个table数组存储了Entry类的对象。HashMap类有一个叫做Entry的内部类。这个Entry类包含了key-value作为实例变量。我们来看下Entry类的结构。Entry类的结构:
1
2
3
4
5
6
7
8
|
static class Entry implements Map.Entry { final K key; V value; Entry next; final int hash; ... //More code goes here } ` |
每当往hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的 Entry数组table中。现在你一定很想知道,上面创建的Entry对象将会存放在具体哪个位置(在table中的精确位置)。答案就是,根据key 的hashcode()方法计算出来的hash值(来决定)。hash值用来计算key在Entry数组的索引。
现在,如果你看下上图中数组的索引10,它有一个叫做HashMap$Entry的Entry对象。
我们往hashmap放了4个key-value对,但是看上去好像只有2个元素!!!这是因为,如果两个元素有相同的hashcode,它们会被放在同一个索引上。问题出现了,该怎么放呢?原来它是以链表(LinkedList)的形式来存储的(逻辑上)。
上面的country对象的key-value的hash值是如何计算出来的。
Japan的Hash值是95,它的长度是奇数。 India的Hash值是95,它的长度是奇数。 Russia的Hash值是31,它的长度是偶数。 France的Hash值是31,它的长度是偶数。
下图会清晰的从概念上解释下链表。
所以,现在假如你已经很好地了解了hashmap的结构,让我们看下put和get方法。
Put :
让我们看下put方法的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
/** * Associates the specified value with the specified key in this map. If the * map previously contained a mapping for the key, the old value is * replaced. * * @param key * key with which the specified value is to be associated * @param value * value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or <tt>null</tt> * if there was no mapping for <tt>key</tt>. (A <tt>null</tt> return * can also indicate that the map previously associated * <tt>null</tt> with <tt>key</tt>.) */ 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 ; } |
现在我们一步一步来看下上面的代码。
对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。
key的hashcode()方法会被调用,然后计算hash值。hash值用来找到存储Entry对象的数组的索引。有时候hash函数可能写的 很不好,所以JDK的设计者添加了另一个叫做hash()的方法,它接收刚才计算的hash值作为参数。如果你想了解更多关于hash()函数的东西,可 以参考:hashmap中的hash和indexFor方法
indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引。
在我们的例子中已经看到,如果两个key有相同的hash值(也叫冲突),他们会以链表的形式来存储。所以,这里我们就迭代链表。
- 如果在刚才计算出来的索引位置没有元素,直接把Entry对象放在那个索引上。
- 如果索引上有元素,然后会进行迭代,一直到Entry->next是null。当前的Entry对象变成链表的下一个节点。
- 如果我们再次放入同样的key会怎样呢?逻辑上,它应该替换老的value。事实上,它确实是这么做的。在迭代的过程中,会调用equals() 方法来检查key的相等性(key.equals(k)),如果这个方法返回true,它就会用当前Entry的value来替换之前的value。
Get:
现在我们来看下get方法的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
/** * Returns the value to which the specified key is mapped, or {@code null} * if this map contains no mapping for the key. * * <p> * More formally, if this map contains a mapping from a key {@code k} to a * value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise it returns * {@code null}. (There can be at most one such mapping.) * * </p><p> * A return value of {@code null} does not <i>necessarily</i> indicate that * the map contains no mapping for the key; it's also possible that the map * explicitly maps the key to {@code null}. The {@link #containsKey * containsKey} operation may be used to distinguish these two cases. * * @see #put(Object, Object) */ 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 ; } |
当你理解了hashmap的put的工作原理,理解get的工作原理就非常简单了。当你传递一个key从hashmap总获取value的时候:
对key进行null检查。如果key是null,table[0]这个位置的元素将被返回。
key的hashcode()方法被调用,然后计算hash值。
indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。
在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。
要牢记以下关键点:
- HashMap有一个叫做Entry的内部类,它用来存储key-value对。
- 上面的Entry对象是存储在一个叫做table的Entry数组中。
- table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。
- key的hashcode()方法用来找到Entry对象所在的桶。
- 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。
- key的equals()方法用来确保key的唯一性。
- value对象的equals()和hashcode()方法根本一点用也没有。
转自:原文链接: javacodegeeks 翻译: ImportNew.com - miracle1919
译文链接: http://www.importnew.com/10620.html
作者:miracle1919
详解HashMap的内部工作原理的更多相关文章
- 详解Java GC的工作原理+Minor GC、FullGC
详解Java GC的工作原理+Minor GC.FullGC 引用地址:http://www.blogjava.net/ldwblog/archive/2013/07/24/401919.html J ...
- Odoo search 搜索视图详解与搜索视图工作原理
转载请注明原文地址:https://www.cnblogs.com/ygj0930/p/10826430.html 搜索视图 搜索视图的search标签本身没什么属性可以使用,只要是<searc ...
- OkHttp3源码详解(六) Okhttp任务队列工作原理
1 概述 1.1 引言 android完成非阻塞式的异步请求的时候都是通过启动子线程的方式来解决,子线程执行完任务的之后通过handler的方式来和主线程来完成通信.无限制的创建线程,会给系统带来大量 ...
- 详解Java GC的工作原理
JVM学习笔记之JVM内存管理和JVM垃圾回收的概念,JVM内存结构由堆.栈.本地方法栈.方法区等部分组成,另外JVM分别对新生代和旧生代采用不同的垃圾回收机制. 首先来看一下JVM内存结构,它是由堆 ...
- LVS原理详解(3种工作方式8种调度算法)--老男孩
一.LVS原理详解(4种工作方式8种调度算法) 集群简介 集群就是一组独立的计算机,协同工作,对外提供服务.对客户端来说像是一台服务器提供服务. LVS在企业架构中的位置: 以上的架构只是众多企业里面 ...
- LVS原理详解(3种工作模式及8种调度算法)
2017年1月12日, 星期四 LVS原理详解(3种工作模式及8种调度算法) LVS原理详解及部署之二:LVS原理详解(3种工作方式8种调度算法) 作者:woshiliwentong 发布日期: ...
- 8. 理解ZooKeeper的内部工作原理
到目前为止,我们已经讨论了ZooKeeper服务的基础知识,并详细了解了数据模型及其属性. 我们也熟悉了ZooKeeper 监视(watch)的概念,监视就是在ZooKeeper命名空间中的znode ...
- Apache Lucene评分机制的内部工作原理
Apache Lucene评分机制的内部工作原理' 第5章
- 4000余字为你讲透Codis内部工作原理
一.引言 Codis是一个分布式 Redis 解决方案,可以管理数量巨大的Redis节点.个推作为专业的第三方推送服务商,多年来专注于为开发者提供高效稳定的消息推送服务.每天通过个推平台下发的消息数量 ...
随机推荐
- 安装nodejs 后运行 npm 命令无响应处理方法
安装和卸载过nodejs, 也编辑过 C:\Users\{账户}\下的.npmrc文件. 再全新安装nodejs ,运行npm 命令,无响应. 处理方法,删除C:\Users\{账户}\下的.npmr ...
- javascript中this、apply、call、bind的用法和区别
参考阮一峰文章链接:http://javascript.ruanyifeng.com/oop/basic.html#toc10
- 【winform程序】自定义webrowser控件调用IE的版本
修改注册表: bit: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\MAIN\FeatureControl\FEATURE_BROW ...
- 手游2dx面试笔记一
第一轮IQ测试:都来面试程序了,相信IQ再怎么也坑不到哪里去吧.要问什么样的题,几页纸呐, 如:1.找出不同类:羚羊.斑马.鲨鱼 2.在()里添一字使2边都能组词:木()料 3.中间值?:1,2,4, ...
- Android ListView 滚动的N种方法
Android 里面让ListView滚动有N种方法,这儿列举三种: 我的需求是通过按键让Listview滚动起来,当然这些按键不是通过Android标识接口传输过来的,所以不能通过监听按键事件来实现 ...
- Zend Framework学习日记(2)--HelloWorld篇(转)
Zend Framework学习日记(2)--HelloWorld篇 这一篇主要演示如何用zf命令行工具建立一个基于Zend Framework框架的工程,也是我初学Zend Framework的小练 ...
- DataGrid 简单数据绑定实例1
1.默认数据显示(自动显示列) 后台绑定 //DataGrid 数据绑定 dataGridOne.ItemsSource = _Context.Info.ToList(); 前台定义 <Data ...
- (转)asp.net(C#)手记之Repeater与两级菜单
先来张图片说明下我们要实现的菜单: 这个菜单只实现了2级哈. 我采用的方法是嵌套2个Repeater. 先看下数据库中的表结构: 数据: 上代码: aspx: <asp:Repeater ID= ...
- Android(通用机能)
数据存储 本地数据存在都是私有化的. 共享方法1是构造数据源组件.方法2将数据放入扩展存储设备. Mashup 服务组件默认没有运行在独立进程或线程中,因此费时操作一般需要起线程.可配置指定新进程. ...
- Android-操作栏之副标题
我们的目标是在操作栏右侧加上一个选项菜单,点击它就可显示或者隐藏操作栏的副标题. 由于操作栏是在API11级以后出现的,因此必须考虑兼容性问题.我们直接让低于API11的设备根本看不到选项菜单即可.建 ...