HashMap 是我们经常使用的一种数据结构。工作中会经常用到,面试也会总提到这个数据结构,找工作的时候,”HashTable 和HashMap的区别“被问到过没有?

本文会从原理,JDK源码,项目使用多个角度来分析HashMap。

1.HashMap是什么

JDK文档中如是说”基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)不保证映射的顺序“

里面大致包含如下意思:

HashMap是Map的实现,因此它内部的元素都是K-V(键,值)组成的。

HashMap内部元素是无序的

2.jdk中如何实现一个HashMap

HashMap在java.util包下,我们平时使用的类,有大部分都是这个包或者其子包的类

JDK中实现类的定义

  1. public class HashMap<K,V>
  2. extends AbstractMap<K,V>
  3. implements Map<K,V>, Cloneable, Serializable

它实现了Map接口

通常我们这么使用HashMap

  1. Map<Integer,String> maps=new HashMap<Integer,String>();
  2. , "a");
  3. , "b");

上面代码新建了一个HashMap并且往里插入了两个数据,这里不接受基本数据类型来做K,V

如果你这么写的话,就会出问题了

  1. Map<int,double> maps=new HashMap<int,double>();

上面例子很简单可是你知道内部他们怎么实现的吗?

我们来看看HashMap的构造方法

  1. public HashMap() {
  2. this.loadFactor = DEFAULT_LOAD_FACTOR;
  3. threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
  4. table = new Entry[DEFAULT_INITIAL_CAPACITY];
  5. init();
  6. }

都知道HashMap是个变长的数据结构,看了上面的构造方法可能你并不会认为它有那么神了。

  1. DEFAULT_LOAD_FACTOR   //默认加载因子,如果不制定的话是0.75
  1. DEFAULT_INITIAL_CAPACITY //默认初始化容量,默认是16
  1. threshold //阈(yu)值 根据加载因子和初始化容量计算得出

因此我们知道了,如果我们调用无参数的构造方法的话,我们将得到一个16容量的数组

数组是定长的,如何用一个定长的数据来表示一个不定长的数据呢,答案就是找一个更长的

下面来看看put方法是怎么实现的

  1. public V put(K key, V value) {
  2. if (key == null) //键为空的情况,HashMap和HashTable的一个区别
  3. return putForNullKey(value);
  4. int hash = hash(key.hashCode()); //根据键的hashCode算出hash值
  5. int i = indexFor(hash, table.length); //根据hash值算出究竟该放入哪个数组下标中
  6. for (Entry<K,V> e = table[i]; e != null; e = e.next) {//整个for循环实现了如果存在K那么就替换V
  7. Object k;
  8. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  9. V oldValue = e.value;
  10. e.value = value;
  11. e.recordAccess(this);
  12. return oldValue;
  13. }
  14. }
  15. modCount++;//计数器
  16. addEntry(hash, key, value, i); //添加到数组中
  17. return null;
  18. }

区区十几行代码,通过我添加的注释看懂并不难,细心的话可能会发现这里并没有体现变长的概念,如果我数据大于之前的容量的怎么继续添加呀,答案就在addEntry方法中

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. Entry<K,V> e = table[bucketIndex];
  3. table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
  4. if (size++ >= threshold)
  5. * table.length);
  6. }

这里显示了如果当前size>threshold的话那么就会扩展当前的size的两倍,如何扩展?

  1. void resize(int newCapacity) {
  2. Entry[] oldTable = table;
  3. int oldCapacity = oldTable.length;
  4. if (oldCapacity == MAXIMUM_CAPACITY) {
  5. threshold = Integer.MAX_VALUE;
  6. return;
  7. }
  8. Entry[] newTable = new Entry[newCapacity];
  9. transfer(newTable);
  10. table = newTable;
  11. threshold = (int)(newCapacity * loadFactor);
  12. }

new 一个新的数组,将旧数据转移到新的数组中,并且重新计算阈值,如何转移数据?

  1. void transfer(Entry[] newTable) {
  2. Entry[] src = table;
  3. int newCapacity = newTable.length;
  4. ; j < src.length; j++) {
  5. Entry<K,V> e = src[j];
  6. if (e != null) {
  7. src[j] = null;
  8. do {
  9. Entry<K,V> next = e.next;
  10. int i = indexFor(e.hash, newCapacity);
  11. e.next = newTable[i];
  12. newTable[i] = e;
  13. e = next;
  14. } while (e != null);
  15. }
  16. }
  17. }

根据hash值,和新的容量重新计算数据下标。天呀,太麻烦了吧。

到此为止我们知道了新建一个HashMap和添加一个HashMap之后源代码中都干了什么。

 3.hashcode你懂它不

HashMap是根据hashcode的来进行计算hash值的,equals方法默认也是通过hashcode来进行比较的

hashCode到底是个什么东西呢?

我们跟踪JDK源码到Object结果JDK确给了我们一个下面的本地方法

  1. public native int hashCode();

通过方法我们只能知道hashcode 是一个int值。

疑问更加多了,首先它如何保证不同对象的hashcode 值不一样呢,

既然hashcode是一个整形的,那么它最多的应该只能表示Integer.maxValue个值,

那么当大于这么多值的情况下这些对象的值又该如何表示呢。

要理解这些东西需要从操作系统说起了

//TODO 时间关系,后面再补

4.HashMap的优缺点

优点:超级快速的查询速度,如果有人问你什么数据结构可以达到O(1)的时间复杂度,没错是HashMap

动态的可变长存储数据(和数组相比较而言)

缺点:需要额外计算一次hash值

如果处理不当会占用额外的空间

 5.如果更加高效的使用HashMap

添加

前面我们知道了添加数据的时候,如果当前数据的个数加上1要大于hashmap的阈值的话,那么数组就会进行一个*2的操作。并且从新计算所有元素的在数组中的位置。

因此如果我们要添加一个1000个元素的hashMap,如果我们用默认值那么我么需要额外的计算多少次呢

当大于16*0.75=12的时候,需要从新计算 12次

当大于16*2*0.75=24的时候,需要额外计算 24次

……

当大于16*n*0.75=768的时候,需要额外计算 768次

所以我们总共在扩充过程中额外计算12+24+48+……+768次

因此强力建议我们在项目中如果知道范围的情况下,我们应该手动指定初始大小 像这样

  1. );

删除

JDK中如下方式进行删除

  1. public V remove(Object key) {
  2. Entry<K,V> e = removeEntryForKey(key);
  3. return (e == null ? null : e.value);
  4. }
  5. final Entry<K,V> removeEntryForKey(Object key) {
  6. : hash(key.hashCode());
  7. int i = indexFor(hash, table.length);
  8. Entry<K,V> prev = table[i];
  9. Entry<K,V> e = prev;
  10. while (e != null) {
  11. Entry<K,V> next = e.next;
  12. Object k;
  13. if (e.hash == hash &&
  14. ((k = e.key) == key || (key != null && key.equals(k)))) {
  15. modCount++;
  16. size--;
  17. if (prev == e)
  18. table[i] = next;
  19. else
  20. prev.next = next;
  21. e.recordRemoval(this);
  22. return e;
  23. }
  24. prev = e;
  25. e = next;
  26. }
  27. return e;
  28. }

根据上面代码我们知道了,如果删除是不进行了数组容量的重新定义的。所以,如果你有1000个元素的HashMap就算你最后删除只剩下一个了,你在内存中依然还有大于1000个容量,其中大于999个是空的。 为什么是大于因为扩容之后的HashMap实际容量大于1000个。

因此如果我们项目中有很大的HashMap,删除之后却很小了,我们还是弄一个新的小的存它 吧。

6.HashMap同步

如果HashMap在多线程下会出现什么问题呢

我们知道HashMap不是线程安全的(HashMap和HashTable的另外一个区别),如果我们也想要在多线程的环境下使用它怎么办呢?

也许你会说不是有HashTable吗?那我们就试试

  1. public class MyThread extends Thread { // 线程类
  2. private Map<Integer, String> maps; // 多线程处理的map
  3. public MyThread(Map<Integer, String> maps) {
  4. this.maps = maps;
  5. }
  6. @Override
  7. public void run() {
  8. );//随即删除的key
  9. op(delNumber);
  10. }
  11. public void op(int delNumber) {
  12. Iterator<Map.Entry<Integer, String>> t = maps.entrySet().iterator();
  13. while (t.hasNext()) {
  14. Map.Entry<Integer, String> entry = t.next();
  15. int key = entry.getKey();
  16. if (key == delNumber) { //看下key是否是需要删除的key是的话就删除
  17. maps.remove(key);
  18. break;
  19. }
  20. }
  21. }
  22. }
  1. public class HashMapTest {
  2. public static void main(String[] args) {
  3. testSync();
  4. }
  5. public static void testSync(){
  6. );
  7. //      Map<Integer, String> maps = new HashMap<Integer, String>(10000);
  8. //      Map<Integer, String> maps = new ConcurrentHashMap<Integer, String>(10000);
  9. ; i < 10000; i++) {
  10. maps.put(i, "a");
  11. }
  12. ;i<10;i++){
  13. new MyThread(maps).start();
  14. }
  15. }
  16. }

我们使用HashTable来运行试试,不一会就出现了如下错误信息

  1. Exception in thread "Thread-6" java.util.ConcurrentModificationException
  2. )
  3. )
  4. )
  5. Exception in thread "Thread-4" java.util.ConcurrentModificationException
  6. )
  7. )
  8. )
  9. Exception in thread "Thread-2" java.util.ConcurrentModificationException
  10. )
  11. )
  12. )
  13. Exception in thread "Thread-1" java.util.ConcurrentModificationException
  14. )
  15. )
  16. )
  17. Exception in thread "Thread-8" java.util.ConcurrentModificationException
  18. )
  19. )
  20. )
  21. Exception in thread "Thread-9" java.util.ConcurrentModificationException
  22. )
  23. )
  24. )
  25. Exception in thread "Thread-5" java.util.ConcurrentModificationException
  26. )
  27. )
  28. )
  29. ):  [../../../src/share/back/util.c:820]

不是说是安全的不?为什么会出现这个问题呢,继续看源代码

  1. public T next() {
  2. if (modCount != expectedModCount)
  3. throw new ConcurrentModificationException();
  4. return nextElement();
  5. }

当修改之后的计数器和期望的不一致的时候就会抛出异常了。对应于上面代码,线程1,遍历的时候假如有100个,本来删除之后就99个,但是线程2这段时间也删除了一个

所以实际只有98个了,线程1并不知道,当线程1调用next方法时候比较下结果不对,完了,数据不对了,老板要扣工资了,线程自己也解决不了,抛出去吧,别引起更大的问题了。

于是你得到了一个ConcurrentModificationException。

所以以后要注意了,HashTable,vector都不是绝对线程安全的了,所以我们需要将maps加上同步

  1. public void op(int delNumber) {
  2. synchronized (maps) {
  3. Iterator<Map.Entry<Integer, String>> t = maps.entrySet().iterator();
  4. while (t.hasNext()) {
  5. Map.Entry<Integer, String> entry = t.next();
  6. int key = entry.getKey();
  7. if (key == delNumber) { // 看下key是否是需要删除的key是的话就删除
  8. maps.remove(key);
  9. break;
  10. }
  11. }
  12. }
  13. }

synchronized(maps)加上之后就不会出现问题了,就算你用的是HashMap都不会出问题。

其实JDK中在早在1.5之后又了ConcurrentHashMap了这个类你可以放心的在多线程下使用并且不需要加任何同步 了。

java.util.HashMap 解析的更多相关文章

  1. 解决Apache CXF 不支持传递java.sql.Timestamp和java.util.HashMap类型问题

    在项目中使用Apache开源的Services Framework CXF来发布WebService,CXF能够很简洁与Spring Framework 集成在一起,在发布WebService的过程中 ...

  2. java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String

    问题背景:从前端传来的json字符串中取某些值,拼接成json格式入参调外部接口. 报如下错: java.lang.ClassCastException: java.util.HashMap cann ...

  3. java:警告:[unchecked] 对作为普通类型 java.util.HashMap 的成员的put(K,V) 的调用未经检查

    java:警告:[unchecked] 对作为普通类型 java.util.HashMap 的成员的put(K,V) 的调用未经检查 一.问题:学习HashMap时候,我做了这样一个程序: impor ...

  4. Mabitis 多表查询(一)resultType=“java.util.hashMap”

    1.进行单表查询的时候,xml标签的写法如下 进行多表查询,且无确定返回类型时 xml标签写法如下: <select id="Volume" parameterType=&q ...

  5. LinkedHashMap和HashMap的比较使用 由于现在项目中用到了LinkedHashMap,并不是太熟悉就到网上搜了一下。 ? import java.util.HashMap; impo

    LinkedHashMap和HashMap的比较使用 由于现在项目中用到了LinkedHashMap,并不是太熟悉就到网上搜了一下. import java.util.HashMap; import ...

  6. java.util.HashMap和java.util.HashTable (JDK1.8)

    一.java.util.HashMap 1.1 java.util.HashMap 综述 java.util.HashMap继承结构如下图 HashMap是非线程安全的,key和value都支持nul ...

  7. 关于spring mybateis 定义resultType="java.util.HashMap"

    关于spring mybateis  定义resultType="java.util.HashMap" List<HashMap<String, Object>& ...

  8. JDK1.8源码(七)——java.util.HashMap 类

    本篇博客我们来介绍在 JDK1.8 中 HashMap 的源码实现,这也是最常用的一个集合.但是在介绍 HashMap 之前,我们先介绍什么是 Hash表. 1.哈希表 Hash表也称为散列表,也有直 ...

  9. org.apache.ibatis.builder.IncompleteElementException: Could not find result map java.util.HashMap

    这样的配置有问题吗? <select id="getFreightCollectManagementList" resultMap="java.util.HashM ...

随机推荐

  1. Codeforces 1131 A. Sea Battle-暴力 (Codeforces Round #541 (Div. 2))

    A. Sea Battle time limit per test 1 second memory limit per test 256 megabytes input standard input ...

  2. BZOJ2654/COGS1764 [2012国家集训队]tree(陈立杰) [生成树,二分]

    BZOJ传送门,COGS传送门 tree Description 给你一个无向带权连通图,每条边是黑色或白色.让你求一棵最小权的恰好有need条白色边的生成树. 题目保证有解. Input 第一行V, ...

  3. 1.L 查询关键字 HEXLOC

    Take the compile listing for the program that has either OFFSET or LIST option selected. Use the OFF ...

  4. 【动态规划】【滚动数组】【搜索】Playrix Codescapes Cup (Codeforces Round #413, rated, Div. 1 + Div. 2) D. Field expansion

    显然将扩张按从大到小排序之后,只有不超过前34个有效. d[i][j]表示使用前i个扩张,当length为j时,所能得到的最大的width是多少. 然后用二重循环更新即可, d[i][j*A[i]]= ...

  5. JDK源码学习笔记——Object

    一.源码解析 public class Object { /** * 一个本地方法,具体是用C(C++)在DLL中实现的,然后通过JNI调用 */ private static native void ...

  6. 浅析HashMap和Hashtable的区别

    HashMap和Hashtable两个类都实现了Map接口,二者保存键值对(key-value对): HashMap和HashTable区别 第一,继承的父类不同.HashMap继承自Abstract ...

  7. <摘录>ldconfig和ldd用法

    ldconfig和ldd用法 关键字: ldconfig ldd /etc/ld.so.conf LD_LIBRARY_PATH一.ldconfig ldconfig --helpUsage: ldc ...

  8. TWinHttp之二

    TWinHttp之二 function EncodeParams(strings: TStrings): SockString;var i: Integer; S: string;begin for ...

  9. Android Parcel对象详解

    关于Parcel的使用 在分析Parcel之前,首先按照分析流程,介绍下关于Parcel的相关常规使用. 首先是关于Parcel的获取: Parcel parcle = Parcel.Obtain() ...

  10. iOS:时间格式化(标准时间转为时间戳、时间戳转为标准时间、时间戳转为日期)

    新建一个NSString(Time)分类,创建类方法实现时间的转换 .h文件 #import <Foundation/Foundation.h> @interface NSString ( ...