概述

HashMap对于做Java的小伙伴来说太熟悉了。估计你们每天都在使用它。它为什么叫做HashMap?它的内部是怎么实现的呢?为什么我们使用的时候很多情况都是用String作为它的key呢?带着这些疑问让我们来了解HashMap!

HashMap介绍

1、介绍

HashMap是一个用”KEY”-“VALUE”来实现数据存储的类。你可以用一个”key”去存储数据。当你想获得数据的时候,你可以通过”key”去得到数据。所以你可以把HashMap当作一个字典。 那么HashMap的名字从何而来呢?其实HashMap的由来是基于Hasing技术(Hasing),Hasing就是将很大的字符串或者任何对象转换成一个用来代表它们的很小的值,这些更短的值就可以很方便的用来方便索引、加快搜索。

在讲解HashMap的存储过程之前还需要提到一个知识点 
我们都知道在Java中每个对象都有一个hashcode()方法用来返回该对象的 hash值。HashMap中将会用到对象的hashcode方法来获取对象的hash值。

2、关系

图1展示了HashMap的类结构关系。

HashMap继承了AbstractMap,并且支持序列化和反序列化。由于实现了Clonable接口,也就支持clone()方法来复制一个对象。今天主要说HashMap的内部实现,这里就不对序列化和clone做讲解了。

3、内部介绍

上面的图很清晰的说明了HashMap内部的实现原理。就好比一个篮子,篮子里装了很多苹果,苹果里包含了自己的信息和另外一个苹果的引用

1、和上图显示的一样,HashMap内部包含了一个Entry类型的数组table, table里的每一个数据都是一个Entry对象。

2、再来看table里面存储的Entry类型,Entry类里包含了hashcode变量,key,value 和另外一个Entry对象。为什么要有一个Entry对象呢?其实如果你看过linkedList的源码,你可能会知道这就是一个链表结构。通过我找到你,你再找到他。不过这里的Entry并不是LinkedList,它是单独为HashMap服务的一个内部单链表结构的类。

3、那么Entry是一个单链表结构的意义又是什么呢?在我们了解了HashMap的存储过程之后,你就会很清楚了,接着让我们来看HashMap怎么工作的。

HashMap的存储过程

下面分析一段代码的HashMap存储过程。(这里只是作为演示的例子,并没有真实的去取到了Hash值,如果你有需要可以通过Debug来得到key的Hash值)

        HashMap hashMap = new HashMap();//line1
hashMap.put("one","hello1");//line2
hashMap.put("two","hello2");//line3
hashMap.put("three","hello3");//line4
hashMap.put("four","hello4");//line5
hashMap.put("five","hello5");//line6
hashMap.put("six","hello6");//line7
hashMap.put("seven","hello7");//line8

put操作的伪代码可以表示如下:

public V put(K key, V value){
int hash = hash(key);
int i = indexFor(hash, table.length);
//在table[i]的地方添加一个包含hash,key,value信息的Entry类。
}

下面我们来看上面代码的过程 
1、line1创建了一个HashMap,所以我们来看构造函数

/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

空构造函数调用了它自己的另一个构造函数,注释说明了构建了一个初始容量的空HashMap,那我们就来看它另外一个构造函数。

public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor); this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
} void init() {
}

上面的代码只是简单的给loadFactor(其实是数组不够用来扩容的)和threshold(内部数组的初始化容量),init()是一个空方法。所以现在数组table还是一个空数组。

 /**
* An empty table instance to share when the table is not inflated.
*/
static final Entry<?,?>[] EMPTY_TABLE = {}; /**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

2、接下来到了line2的地方, hashMap.put(“one”,”hello1”);在这里先提一下put方法源码:

public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);//如果是空的,加载
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);获取hash值
int i = indexFor(hash, table.length);生成索引
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//遍历已存在的Entry,如果要存入的key和hash值都一样就覆盖。
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;
}

源码很简单,先判断table如果是空的,就初始化数组table,接着如果key是null就单独处理。否则的话就得到key的hash值再生成索引,这里用了indexFor()方法生成索引是因为:hash值一般都很大,是不适合我们的数组的。来看indexFor方法

/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}

就是一个&操作,这样返回的值比较小适合我们的数组。

继续 line2put操作,因为开始table是空数组,所以会进入 inflateTable(threshold)方法,其实这个方法就是出实话数组容量,初始化长度是16,这个长度是在开始的构造方法赋值的。 
所以,现在空数组变成了长度16的数组了,就像下图一样。 

接着由于我们的key不为null,到了获取hash值和索引,这里假设int hash = hash(key)和int i = indexFor(hash, table.length)生成的索引i为hash=2306996,i = 4;那么就会在table索引为4的位置新建一个Entry,对应的代码是addEntry(hash, key, value, i);到此结果如下图: 

新建的Entry内部的变量分别是,hash,key,value,和指向下一节点的next Entry。

3、继续来看line3,line3和line2一样,而且数组不为空直接hash(key)和index。所以直接看图了 

4、到了line4,这里line4情况有点特殊,我们假设line4里key生成的hashcode产生的index也为4,比如hash(“three”) 的值 63281940 
hash&(15)产生的index为4。这种情况由于之前的位置已经有Entry了,所以遍历Entry如果key和hashcode都相同,就直接替换,否则新添加一个Entry,来看一下对应源码

public V put(K key, V value) {
...//一些代码
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;
}
}
//for循环里判断如果hash和key都一样直接替换。 modCount++;
addEntry(hash, key, value, i);//没有重复的话就addEntry
return null;
}

上面代码先判断是否需要替换,不需要就调用了addEntry方法。来看addEntry

void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}//判断数组容量是否足够,不足够扩容 createEntry(hash, key, value, bucketIndex);
}

里面又调用了createEntry

void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
//获取当前节点,然后新建一个含有当前hash,key,value信息的一个节点,并且该节点的Entry指向了前一个Entry并赋值给table[index],成为了最新的节点Entry,同时将size加1。
}

到这里相信大家很清楚了。来看看图: 

5、到这里之后的代码都在上面的分析情况当中。我就不一一画图了,直接给出程序执行到最后的图 
line5到line8

代码 hashcode index key value next
hashMap.put(“four”,”hello4”); 54378290 9 four hello4 null
hashMap.put(“five”,”hello5”); 39821723 8 five hello5 null
hashMap.put(“six”,”hello6”); 86726537 4 six hello6 line4产生的Entry
hashMap.put(“seven”,”hello7”); 28789082 2 seven hello7 line3产生的Entry

结果图如下: 

到此put 操作就结束了,再来看看取

HashMap的取值过程

我们通过hashMap.get(K key) 来获取存入的值,key的取值很简单了。我们通过数组的index直接找到Entry,然后再遍历Entry,当hashcode和key都一样就是我们当初存入的值啦。看源码:

 public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue();
}

调用getEntry(key)拿到entry ,然后返回entry的value,来看getEntry(key)方法

final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
} int hash = (key == null) ? 0 : hash(key);
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 != null && key.equals(k))))
return e;
}
return null;
}

按什么规则存的就按什么规则取,获取到hash,再获取index,然后拿到Entry遍历,hash相等的情况下,如果key相等就知道了我们想要的值。

再get方法中有null的判断,null取hash值总是0,再getNullKey(K key)方法中,也是按照遍历方法来查找的。

到这你肯定明白了为什么HashMap可以用null做key。

了解的存储取值过程和内部实现,其它的方法自己看看源码很好理解,在此就不一一解释了。

几个问题

问题1、HashMap是基于key的hashcode的存储的,如果两个不同的key产生的hashcode一样取值怎么办? 
看了上面的分析,你肯定知道,再数组里面有链表结构的Entry来实现,通过遍历所有的Entry,比较key来确定到底是哪一个value;

问题2、HashMap是基于key的hashcode的存储的,如果两个key一样产生的hashcode一样怎么办? 
在put操作的时候会遍历所有Entry,如果有key相等的则替换。所以get的时候只会有一个

问题3、我们总是习惯用一个String作为HashMap的key,这是为什么呢?其它的类可以做为HashMap的key吗? 
这里因为String是不可以变的,并且java为它实现了hashcode的缓存技术。我们在put和get中都需要获取key的hashcode,这些方法的效率很大程度上取决于获取hashcode的,所以用String的原因:1、它是不可变的。2、它实现了hashcode的缓存,效率更高。如果你对String不了解可以看:Java你可能不知道的事-String

问题4、可变的对象能作为HashMap的key吗? 
可变的对象是可以当做HashMap的key的,只是你要确保你可变变量的改变不会改变hashcode。比如以下代码

public class TestMemory {

    public static void main(String[] args) {
HashMap hashMap = new HashMap();
TestKey testKey = new TestKey();
testKey.setAddress("sdfdsf");//line3
hashMap.put(testKey,"hello");
testKey.setAddress("sdfsdffds");//line5
System.out.println(hashMap.get(testKey));
}
} public class TestKey {
String name;
String address; public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public String getAddress() {
return address;
} public void setAddress(String address) {
this.address = address;
} @Override
public int hashCode() {
if (name==null){
return 0;
}
return name.hashCode();
}
}

上面的代码line3到line5对象里的address做了改变,但是由于hashCode是基于name来生成的,name没变,所以依然能够正常找到value。但是如果把setAdress换成name,get就会返回null。这就是为什么我们选择String的原因。

到这里相信你对HashMap内部已经非常清楚了,如果本篇文章对你有帮助记得点赞和评论,或者关注我,我会继续更新文章,感谢支持!

Java你可能不知道的事(3)HashMap的更多相关文章

  1. java你可能不知道的事(2)--堆和栈

    在java语言的学习和使用当中你可能已经了解或者知道堆和栈,但是你可能没有完全的理解它们.今天我们就一起来学习堆.栈的特点以及它们的区别.认识了这个之后,你可能对java有更深的理解. Java堆内存 ...

  2. java你可能不知道的事(2)--堆和栈<转>

    在java语言的学习和使用当中你可能已经了解或者知道堆和栈,但是你可能没有完全的理解它们.今天我们就一起来学习堆.栈的特点以及它们的区别.认识了这个之后,你可能对java有更深的理解. Java堆内存 ...

  3. Java你可能不知道的事系列(1)

    概述 本类文章会不段更新分析学习到的经典面试题目,在此记录下来便于自己理解.如果有不对的地方还请各位观众拍砖. 今天主要分享一下常用的字符串的几个题目,相信学习java的小伙伴们对String类是再熟 ...

  4. Java你可能不知道的事系列1

    概述 本类文章会不段更新分析学习到的经典面试题目,在此记录下来便于自己理解.如果有不对的地方还请各位观众拍砖. 今天主要分享一下常用的字符串的几个题目,相信学习java的小伙伴们对String类是再熟 ...

  5. Spring中你可能不知道的事(一)

    Spring作为Java的王牌开源项目,相信大家都用过,但是可能大家仅仅用到了Spring最常用的功能,Spring实在是庞大了,很多功能可能一辈子都不会用到,今天我就罗列下Spring中你可能不知道 ...

  6. ES6 你可能不知道的事 – 基础篇

    序 ES6,或许应该叫 ES2015(2015 年 6 月正式发布),对于大多数前端同学都不陌生. 首先这篇文章不是工具书,不会去过多谈概念,而是想聊聊关于每个特性 你可能不知道的事,希望能为各位同学 ...

  7. overflow:hidden 你所不知道的事

    overflow:hidden 你所不知道的事 overflow:hidden这个CSS样式是大家常用到的CSS样式,但是大多数人对这个样式的理解仅仅局限于隐藏溢出,而对于清除浮动这个含义不是很了解. ...

  8. Java单例你所不知道的事,与Volatile关键字有染

    版权声明:本文为博主原创文章,未经博主允许不得转载. 如果问一个码农最先接触到的设计模式是什么,单例设计模式一定最差也是“之一”. 单例,Singleton,保证内存中只有一份实例对象存在. 问:为什 ...

  9. 【Java基础】关于枚举类你可能不知道的事

    目录 谈谈枚举 1. 枚举类的定义 2. 枚举类的底层实现 3. 枚举类的序列化实现 4. 用枚举实现单列 5. 枚举实例的创建过程是线程安全的 谈谈枚举 如果一个类的对象个数是有限的而且是不变的,我 ...

随机推荐

  1. 从零开始学习jQuery (一) 入门篇

    本系列文章导航 从零开始学习jQuery (一) 入门篇 一.摘要 本系列文章将带您进入jQuery的精彩世界, 其中有很多作者具体的使用经验和解决方案,  即使你会使用jQuery也能在阅读中发现些 ...

  2. Lambda表达式演变

    Lambda表达式是一种匿名函数.   演变步骤:   一般的方法委托 => 匿名函数委托 => Lambda表达式   Lambda表达式其实并不陌生,他的前生就是匿名函数,所以要谈La ...

  3. 歌词文件解析(一):LRC格式文件的解析

    LRC是英文lyric(歌词)的缩写,被用做歌词文件的扩展名.以lrc为扩展名的歌词文件可以在各类数码播放器中同步显示.LRC 歌词是一种包含着“*:*”形式的“标签(tag)”的基于纯文本的歌词专用 ...

  4. 创建html元素

    如果我要创建一个div元素. 1.使用DOM对象创建: 使用document.createElement('div')方法创建元素. 2.使用JQuery创建: 使用$('<div>通过J ...

  5. C#获取执行存储过程的" 返回值"代码

    以下是C#代码: /// <summary> /// 执行存储过程,返回" 返回值" /// </summary> /// <param name=& ...

  6. HoverTree开源项目已经实现管理员登录

    ASP.NET开源项目HoverTree已经实现了管理员登录功能,最新代码请到以下网址查看.http://hovertree.com/down/ 点击Clone右边的Download就可以下载最新开发 ...

  7. 【要什么自行车】ASP.NET MVC4笔记01:Asp.net MVC 分页,采用 MvcPager 和CYQ.Data来分页

    Control: ) { ; ; using (MAction action = new MAction("brain")) { MDataTable table = action ...

  8. sQLserver T-SQL 事务的用法

    原文在: https://www.lesg.cn/netdaima/2016-55.html 在使用Mssql的时候经常需要用到存储过程 有些操作在前面发生错误的时候:需要回滚:这就需要事务了: 下面 ...

  9. C#编程总结(一)序列化

    C#编程总结(一)序列化 序列化是将对象状态转换为可保持或传输的格式的过程.与序列化相对的是反序列化,它将流转换为对象.这两个过程结合起来,可以轻松地存储和传输数据. 几种序列化技术:      1) ...

  10. ZooKeeper----Java实例文档

    **************************************************************************************************** ...