对于 Map ,最直观就是理解就是键值对,映射,key-value 形式。一个映射不能包含重复的键,一个键只能有一个值。平常我们使用的时候,最常用的无非就是 HashMap。

HashMap 实现了 Map 接口,允许使用 null 值 和 null 键,并且不保证映射顺序。

HashMap 有两个参数影响性能:

初始容量:表示哈希表在其容量自动增加之前可以达到多满的一种尺度

加载因子:当哈希表中的条目超过了容量和加载因子的乘积的时候,就会进行重哈希操作。

如下成员变量源码:

  1. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  2. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
  3. transient Node<K,V>[] table;

可以看到,默认加载因子为 0.75, 默认容量为 1 << 4,也就是 16。加载因子过高,容易产生哈希冲突,加载因子过小,容易浪费空间,0.75是一种折中。

另外,整个 HashMap 的实现原理可以简单的理解成:当我们 put 的时候,首先根据 key 算出一个数值 x,然后在 table[x] 中存放我们的值。这样有一个好处是,以后的 get 等操作的时间复杂度直接就是O(1),因为 HashMap 内部就是基于数组的一个实现。

put 方法的实现 与 哈希冲突

下面再结合代码重点分析下 HashMap 的 put 方法的内部实现 和 哈希冲突的解决办法:

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  5. boolean evict) {
  6. Node<K,V>[] tab; Node<K,V> p; int n, i;
  7. if ((tab = table) == null || (n = tab.length) == 0)
  8. n = (tab = resize()).length;
  9. if ((p = tab[i = (n - 1) & hash]) == null)
  10. tab[i] = newNode(hash, key, value, null);
  11. else {
  12. Node<K,V> e; K k;
  13. if (p.hash == hash &&
  14. ((k = p.key) == key || (key != null && key.equals(k))))
  15. e = p;
  16. else if (p instanceof TreeNode)
  17. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  18. else {
  19. for (int binCount = 0; ; ++binCount) {
  20. if ((e = p.next) == null) {
  21. p.next = newNode(hash, key, value, null);
  22. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  23. treeifyBin(tab, hash);
  24. break;
  25. }
  26. if (e.hash == hash &&
  27. ((k = e.key) == key || (key != null && key.equals(k))))
  28. break;
  29. p = e;
  30. }
  31. }
  32. if (e != null) { // existing mapping for key
  33. V oldValue = e.value;
  34. if (!onlyIfAbsent || oldValue == null)
  35. e.value = value;
  36. afterNodeAccess(e);
  37. return oldValue;
  38. }
  39. }
  40. ++modCount;
  41. if (++size > threshold)
  42. resize();
  43. afterNodeInsertion(evict);
  44. return null;
  45. }

首先我们看到 hash(key) 这个就是表示要根据 key 值算出一个数值,以此来决定在 table 数组的哪一个位置存放我们的数值。(Ps:这个 hash(key) 方法 也是大有讲究的,会严重影响性能,实现得不好会让 HashMap 的 O(1) 时间复杂度降到 O(n),在JDK8以下的版本中带来灾难性影响。它需要保证得出的数在哈希表中的均匀分布,目的就是要减少哈希冲突)

重要说明一下:

**JDK8 中哈希冲突过多,链表会转红黑树,时间复杂度是O(logn),不会是O(n) **

**JDK8 中哈希冲突过多,链表会转红黑树,时间复杂度是O(logn),不会是O(n) **

**JDK8 中哈希冲突过多,链表会转红黑树,时间复杂度是O(logn),不会是O(n) **

然后,我们再看到:

  1. if ((p = tab[i = (n - 1) & hash]) == null)
  2. tab[i] = newNode(hash, key, value, null);
  3. else {
  4. ......

这就表示,如果没有 哈希冲突,那么就可以放入数据 tab[i] = newNode(hash, key, value, null); 如果有哈希冲突,那么就执行 else 需要解决哈希冲突。

那么放入数据 其实就是 建立一个 Node 节点,该 Node节点有属性 key,value,分别保存我们的 key 值 和 value 值,然后再把这个 Node 节点放入到 table 数组中,并没有什么神秘的地方。

  1. static class Node<K,V> implements Map.Entry<K,V> {
  2. final int hash;
  3. final K key;
  4. V value;
  5. Node<K,V> next;
  6. Node(int hash, K key, V value, Node<K,V> next) {
  7. this.hash = hash;
  8. this.key = key;
  9. this.value = value;
  10. this.next = next;
  11. }
  12. }

上述可以看到 Node 节点中 有一个 Node<K,V> next; ,其实仔细思考下就应该知道这个是用来解决哈希冲突的。下面再看看是如何解决哈希冲突的:

哈希冲突:通俗的讲就是首先我们进行一次 put 操作,算出了我们要在 table 数组的 x 位置放入这个值。那么下次再进行一个 put 操作的时候,又算出了我们要在 table 数组的 x 位置放入这个值,那之前已经放入过值了,那现在怎么处理呢?

其实就是通过链表法进行解决。

首先,如果有哈希冲突,那么:

  1. if (p.hash == hash &&
  2. ((k = p.key) == key || (key != null && key.equals(k))))
  3. e = p;

需要判断 两者的 key 是否一样的,因为 HashMap 不能加入重复的键。如果一样,那么就覆盖,如果不一样,那么就先判断是不是 TreeNode 类型的:

  1. else if (p instanceof TreeNode)
  2. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

这里表示 是不是现在已经转红黑树了(在大量哈希冲突的情况下,链表会转红黑树),一般我们小数据的情况下,是不会转的,所以这里暂时不考虑这种情况(Ps:本人也没太深入研究红黑树,所以就不说这个了)。

如果是正常情况下,会执行下面的语句来解决哈希冲突:

  1. for (int binCount = 0; ; ++binCount) {
  2. if ((e = p.next) == null) {
  3. p.next = newNode(hash, key, value, null);
  4. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  5. treeifyBin(tab, hash);
  6. break;
  7. }
  8. if (e.hash == hash &&
  9. ((k = e.key) == key || (key != null && key.equals(k))))
  10. break;
  11. p = e;
  12. }

这里其实就是用链表法来解决。并且:

冲突的节点放在链表的最下面。

冲突的节点放在链表的最下面。

冲突的节点放在链表的最下面。

因为 首先有:p = tab[i = (n - 1) & hash] ,再 for 循环,然后有 if ((e = p.next) == null) { ,并且如果 当前节点的下一个节点有值的话,那么就 p = e;,这就说明了放在最下面。

强烈建议自己拿笔拿纸画画。

总结

一个映射不能包含重复的键,一个键只能有一个值。允许使用 null 值 和 null 键,并且不保证映射顺序。

HashMap 解决冲突的办法先是使用链表法,然后如果哈希冲突过多,那么会把链表转换成红黑树,以此来保证效率。

如果出现了哈希冲突,那么新加入的节点放在链表的最后面。

参考

强烈建议看一下:

Java HashMap工作原理及实现

Java 8:HashMap的性能提升

HashTable

HashTable 是 HashMap 的线程安全版本。 内部的实现几乎和 HashMap 一模一样。例如:

同样的有一个数组:

  1. private transient Entry<?,?>[] table;

对于 put 方法:

  1. public synchronized V put(K key, V value) {
  2. ......
  3. // Makes sure the key is not already in the hashtable.
  4. Entry<?,?> tab[] = table;
  5. int hash = key.hashCode();
  6. int index = (hash & 0x7FFFFFFF) % tab.length;
  7. @SuppressWarnings("unchecked")
  8. Entry<K,V> entry = (Entry<K,V>)tab[index];
  9. for(; entry != null ; entry = entry.next) {
  10. if ((entry.hash == hash) && entry.key.equals(key)) {
  11. V old = entry.value;
  12. entry.value = value;
  13. return old;
  14. }
  15. }
  16. addEntry(hash, key, value, index);
  17. return null;
  18. }

这里可以看到, for 循环表示如果出现了哈希冲突,那么就放在最后一位。因为不断的进行 entry = entry.next,直到 entry != null。需要注意的是,JDK8 中的 HashMap 如果有很多哈希冲突的话,那么是可能会把链表变成红黑树以此来提高效率。但是这里 HashTable 并没有这样做。

另外,从这里也可以看出,HashTable 实现多线程同步的主要方式是通过加 synchronized 关键字。

另外,对于 get 方法:

  1. @SuppressWarnings("unchecked")
  2. public synchronized V get(Object key) {
  3. Entry<?,?> tab[] = table;
  4. int hash = key.hashCode();
  5. int index = (hash & 0x7FFFFFFF) % tab.length;
  6. for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
  7. if ((e.hash == hash) && e.key.equals(key)) {
  8. return (V)e.value;
  9. }
  10. }
  11. return null;
  12. }

这里最明显的就是 synchronized,其实还有很多其他的方法用的也是 synchronized。get 方法的处理也是先根据 key 定位到 table 的某一个位置,最后再 for 循环拿到该值(因为可能出现了哈希冲突,所以要 for 循环)。

总结

  • Hashtable的方法是同步的,HashMap则是非同步的,所以在多线程场合要手动同步HashMap,这个区别就像Vector和ArrayList一样。
  • Hashtable不允许null值(key和value都不可以),HashMap允许null值(key和value都可以)。
  • Hashtable比HashMap多一个elements方法用于遍历。
  • Hashtable使用Enumeration,HashMap使用Iterator。
  • 哈希值的使用不同,Hashtable直接使用对象的hashCode,而HashMap重新计算hash值,而且用与代替求模。
  • Hashtable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

[Java]HashMap实现与哈希冲突,与HashTable的区别的更多相关文章

  1. hashmap实现及哈希冲突

    原文: https://www.cnblogs.com/peizhe123/p/5790252.html HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置.当程序执行 map. ...

  2. 一次电话Java面试的问题总结(JDK8新特性、哈希冲突、HashMap原理、线程安全、Linux查询命令、Hadoop节点)

    面试涉及问题含有: Java JDK8新特性 集合(哈希冲突.HashMap的原理.自动排序的集合TreeSet) 多线程安全问题 String和StringBuffer JVM 原理.运行流程.内部 ...

  3. 【面试普通人VS高手系列】HashMap是怎么解决哈希冲突的?

    常用数据结构基本上是面试必问的问题,比如HashMap.LinkList.ConcurrentHashMap等. 关于HashMap,有个学员私信了我一个面试题说: "HashMap是怎么解 ...

  4. Java集合(九)哈希冲突及解决哈希冲突的4种方式

    Java集合(九)哈希冲突及解决哈希冲突的4种方式 一.哈希冲突 (一).产生的原因 哈希是通过对数据进行再压缩,提高效率的一种解决方法.但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致 ...

  5. [翻译]Java HashMap工作原理

    大部分Java开发者都在使用Map,特别是HashMap.HashMap是一种简单但强大的方式去存储和获取数据.但有多少开发者知道HashMap内部如何工作呢?几天前,我阅读了java.util.Ha ...

  6. Java集合类中的哈希总结

    JAVA集合类中的哈希总结 目 录 1.哈希表 2.Hashtable.HashMap.ConcurrentHashMap.LinkedHashMap.TreeMap区别 3.Hashtable.Ha ...

  7. Java学习笔记(二二)——Java HashMap

    [前面的话] 早上起来好瞌睡哈,最近要注意一样作息状态.       HashMap好好学习一下. [定义] Hashmap:是一个散列表,它存储的内容是键值对(key——value)映射.允许nul ...

  8. java集合框架之java HashMap代码解析

     java集合框架之java HashMap代码解析 文章Java集合框架综述后,具体集合类的代码,首先以既熟悉又陌生的HashMap开始. 源自http://www.codeceo.com/arti ...

  9. 【转】Java HashMap工作原理(好文章)

    大部分Java开发者都在使用Map,特别是HashMap.HashMap是一种简单但强大的方式去存储和获取数据.但有多少开发者知道HashMap内部如何工作呢?几天前,我阅读了java.util.Ha ...

随机推荐

  1. shell之起步

    初学者,先不要考虑好不好看,效率高不高!先要实现需求!需求是第一位! grep.sed.awk.三剑客! 学好shell,需要前提! 1.linux系统命令熟练 2.搞清楚正则,grep.sed.aw ...

  2. 蓝天白云大草原风景PSD背景素材

    蓝天白云大草原风景PSD源文件背景素材,蓝天白云,大草原,风景,背景素材,自然风景,草原景色,绿色清新背景 地址:http://www.huiyi8.com/psd/

  3. 分享知识-快乐自己:Hibernate框架常用API详解

    1):Configuration配置对象 Configuration用于加载配置文件. 1): 调用configure()方法,加载src下的hibernate.cfg.xml文件 Configura ...

  4. MySQL-left join _20160928

    left join 左连接 返回from 后面表的全部记录和 left join 后面表和from 后面表条件相符的全部记录 一般格式为下面,首先table A 和tableB都有两个唯一的字段标识I ...

  5. 【LeetCode】064. Minimum Path Sum

    题目: Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right w ...

  6. Ajax学习(1)

    Web 1.0 它指的就是具有完全不同的请求和响应模型的传统 Web.比如,到 hdu.edu.cn 网站上点击一个按钮.就会对服务器发送一个请求,然后响应再返回到浏览器.该请求不仅仅是新内容和项目列 ...

  7. Java虚拟机学习 - 体系结构 内存模型(转载)

    一:Java技术体系模块图 二:JVM内存区域模型 1.方法区 也称"永久代” .“非堆”,  它用于存储虚拟机加载的类信息.常量.静态变量.是各个线程共享的内存区域.默认最小值为16MB, ...

  8. linux正则表达式基础

    linux中awk,sed,grep等 命令使用区别正则表达式基础 在最简单的情况下,一个正则表达式看上去就是一个普通的查找串.例如,正则表达式"testing"中没有包含任何元字 ...

  9. Hibernate区分不同对象的方法

    1.关系数据库按主键区分不同记录. create table CUSTOMERS (ID int promary key not null, NAME varchar(15));     insert ...

  10. Oracle tns 协议

    下面是翻译国外的一篇博客,原文连接如下: https://thesprawl.org/research/oracle-tns-protocol/ 简介 TNS(Transparent Network ...