Java HashMap的工作原理

2023-05-16

本文由 ImportNew - miracle1919 翻译自 javacodegeeks。欢迎加入翻译小组。转载请见文末要求。本文由 ImportNew - miracle1919 翻译自 javacodegeeks。欢迎加入翻译小组。转载请见文末要求。

面试的时候经常会遇见诸如:“java中的HashMap是怎么工作的”,“HashMap的get和put内部的工作原理”这样的问题。本文将用一个简单的例子来解释下HashMap内部的工作原理。首先我们从一个例子开始,而不仅仅是从理论上,这样,有助于更好地理解,然后,我们来看下get和put到底是怎样工作的。

我们来看个非常简单的例子。有一个”国家”(Country)类,我们将要用Country对象作为key,它的首都的名字(String类型)作为value。下面的例子有助于我们理解key-value对在HashMap中是如何存储的。

1. Country.java

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

2. HashMapStructure.java(main class)

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)。将会看到如下的结构:



从上图可以观察到以下几点:

  1. 有一个叫做table大小是16的Entry数组。

  2. 这个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
}   `
  1. 每当往hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的Entry数组table中。现在你一定很想知道,上面创建的Entry对象将会存放在具体哪个位置(在table中的精确位置)。答案就是,根据key的hashcode()方法计算出来的hash值(来决定)。hash值用来计算key在Entry数组的索引。

  2. 现在,如果你看下上图中数组的索引10,它有一个叫做HashMap$Entry的Entry对象。

  3. 我们往hashmap放了4个key-value对,但是看上去好像只有2个元素!!!这是因为,如果两个元素有相同的hashcode,它们会被放在同一个索引上。问题出现了,该怎么放呢?原来它是以链表(LinkedList)的形式来存储的(逻辑上)。

上面的country对象的key-value的hash值是如何计算出来的。

`


<code>Japan的Hash值是95,它的长度是奇数。

India的Hash值是95,它的长度是奇数。

Russia的Hash值是31,它的长度是偶数。

France,它的长度是偶数。
</code>  

`

下图会清晰的从概念上解释下链表。

所以,现在假如你已经很好地了解了hashmap的结构,让我们看下put和get方法。

Put :

让我们看下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;
 }

现在我们一步一步来看下上面的代码。

  1. 对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。

  2. key的hashcode()方法会被调用,然后计算hash值。hash值用来找到存储Entry对象的数组的索引。有时候hash函数可能写的很不好,所以JDK的设计者添加了另一个叫做hash()的方法,它接收刚才计算的hash值作为参数。如果你想了解更多关于hash()函数的东西,可以参考:hashmap中的hash和indexFor方法

  3. indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引。

  4. 在我们的例子中已经看到,如果两个key有相同的hash值(也叫冲突),他们会以链表的形式来存储。所以,这里我们就迭代链表。

  • 如果在刚才计算出来的索引位置没有元素,直接把Entry对象放在那个索引上。
  • 如果索引上有元素,然后会进行迭代,一直到Entry->next是null。当前的Entry对象变成链表的下一个节点。
  • 如果我们再次放入同样的key会怎样呢?逻辑上,它应该替换老的value。事实上,它确实是这么做的。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)),如果这个方法返回true,它就会用当前Entry的value来替换之前的value。

Get:

现在我们来看下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;
 }

当你理解了hashmap的put的工作原理,理解get的工作原理就非常简单了。当你传递一个key从hashmap总获取value的时候:

  1. 对key进行null检查。如果key是null,table[0]这个位置的元素将被返回。

  2. key的hashcode()方法被调用,然后计算hash值。

  3. indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。

  4. 在获取了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()方法根本一点用也没有。


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java HashMap的工作原理 的相关文章

  • 用两个栈实现队列之程序员面试经典

    题目描述 用两个栈来实现一个队列 xff0c 完成队列的Push和Pop操作 队列中的元素为int类型 比如有栈A和栈B xff0c 在模拟队列的时候先将所有数据依次放入栈A中 xff0c 在要弹出的时候将A中的数据依次从上到下放进栈B x
  • 高度最小的BST之程序员面试经典

    题目描述 对于一个元素各不相同且按升序排列的有序序列 xff0c 请编写一个算法 xff0c 创建一棵高度最小的二叉查找树 给定一个有序序列int vals 请返回创建的二叉查找树的高度 二叉排序树 xff08 Binary Sort Tr
  • 二叉树平衡检查之程序员面试经典

    题目描述 实现一个函数 xff0c 检查二叉树是否平衡 xff0c 平衡的定义如下 xff0c 对于树中的任意一个结点 xff0c 其两颗子树的高度差不超过1 给定指向树根结点的指针TreeNode root xff0c 请返回一个bool
  • JAVA语言之三色排序

    有一个只由0 xff0c 1 xff0c 2三种元素构成的整数数组 xff0c 请使用交换 原地排序而不是使用计数进行排序 给定一个只含0 xff0c 1 xff0c 2的整数数组A 及它的大小 xff0c 请返回排序后的数组 保证数组大小
  • Linux 服务器下载并安装jdk8 学习教程

    获得一台linux服务器 要在linux下安装jdk xff0c 首先你得先有一台linux服务器 xff0c 虚拟机或者租一台都可以 yum安装jdk xff08 力荐 xff09 在linux上使用yum安装是非常粗暴无脑的 xff0c
  • JAVA语言之有序矩阵查找

    现在有一个行和列都排好序的矩阵 xff0c 请设计一个高效算法 xff0c 快速查找矩阵中是否含有值x 给定一个int矩阵mat xff0c 同时给定矩阵大小n xm 及待查找的数x xff0c 请返回一个bool值 xff0c 代表矩阵中
  • JAVA语言之最短子数组长度

    对于一个数组 xff0c 请设计一个高效算法计算需要排序的最短子数组的长度 给定一个int数组A 和数组的大小n xff0c 请返回一个二元组 xff0c 代表所求序列的长度 原序列位置从0开始标号 若原序列有序 xff0c 返回0 保证A
  • JAVA语言之相邻两数最大差值

    有一个整形数组A xff0c 请设计一个复杂度为O n 的算法 xff0c 算出排序后相邻两数的最大差值 给定一个int数组A 和A 的大小n xff0c 请返回最大的差值 保证数组元素多于1个 测试样例 xff1a 1 2 5 4 6 5
  • Spring MVC 流程图

    Spring MVC工作流程图 图一 图二 Spring工作流程描述 1 用户向服务器发送请求 xff0c 请求被Spring 前端控制Servelt DispatcherServlet捕获 xff1b 2 DispatcherServle
  • 输出单层结点之程序员面试经典

    题目描述 对于一棵二叉树 xff0c 请设计一个算法 xff0c 创建含有某一深度上所有结点的链表 给定二叉树的根结点指针TreeNode root xff0c 以及链表上结点的深度 xff0c 请返回一个链表ListNode xff0c
  • java中没有2进制的数据类型,对二进制的操作,需要使用共三种操作符

    lt lt 左移位操作符 gt gt 右移位操作符 gt gt gt 无符号右移操作符 使用左移时 xff0c 数会变大 xff0c 很多时间 xff0c 用来代替 乘方 的操作 比如 2的平方 61 2 2 61 4 61 2 lt lt
  • 面向对象的特征有哪些方面

    需要学习资料的 43 微信公众号 学习资源后台找我 本人比较忙 我看到了会在后台帮你 xff0c 谢谢关注啦 抽象 xff1a 抽象是将一类对象的共同特征总结出来构造类的过程 xff0c 包括数据抽象和行为抽象两方面 抽象只关注对象有哪些属
  • 句子的逆序

    对于一个字符串 xff0c 请设计一个算法 xff0c 只在字符串的单词间做逆序调整 xff0c 也就是说 xff0c 字符串由一些由空格分隔的部分组成 xff0c 你需要将这些部分逆序 给定一个原字符串A 和他的长度 xff0c 请返回逆
  • 字符串移位

    对于一个字符串 xff0c 请设计一个算法 xff0c 将字符串的长度为len的前缀平移到字符串的最后 给定一个字符串A 和它的长度 xff0c 同时给定len xff0c 请返回平移后的字符串 测试样例 xff1a 34 ABCDE 34
  • 拼接最小字典序

    对于一个给定的字符串数组 xff0c 请找到一种拼接顺序 xff0c 使所有小字符串拼接成的大字符串是所有可能的拼接中字典序最小的 给定一个字符串数组strs xff0c 同时给定它的大小 xff0c 请返回拼接成的串 测试样例 xff1a
  • go语言时间类型和时间戳

    时间类型 获取当地时间 fmt span class token punctuation span span class token function Println span span class token punctuation sp
  • 是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?

    不可以 xff0c 静态方法只能访问静态成员 xff0c 因为非静态方法的调用要先创建对象 xff0c 在调用静态方法时可能对象并没有被初始化
  • GC是什么?为什么要有GC?

    GC 是垃圾收集的意思 xff0c 内存处理是编程人员容易出现问题的地方 xff0c 忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃 xff0c Java 提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的 xf
  • 空格替换

    请编写一个方法 xff0c 将字符串中的空格全部替换为 20 假定该字符串有足够的空间存放新增的字符 xff0c 并且知道字符串的真实长度 小于等于1000 xff0c 同时保证字符串由大小写的英文字母组成 给定一个string iniSt
  • 合法括号序列判断

    对于一个字符串 xff0c 请设计一个算法 xff0c 判断其是否为一个合法的括号串 给定一个字符串A 和它的长度n xff0c 请返回一个bool值代表它是否为一个合法的括号串 测试样例 xff1a 34 34 6 返回 xff1a tr

随机推荐

  • 最长无重复字符子串

    对于一个字符串 请设计一个高效算法 xff0c 找到字符串的最长无重复字符的子串长度 给定一个字符串A 及它的长度n xff0c 请返回它的最长无重复字符子串长度 保证A中字符全部为小写英文字符 xff0c 且长度小于等于500 测试样例
  • 列出一些你常见的运行时异常(非检查异常)?

    ArithmeticException xff08 算术异常 xff09 ClassCastException xff08 类转换异常 xff09 IllegalArgumentException xff08 非法参数异常 xff09 In
  • 阐述final、finally、finalize的区别

    final xff1a 修饰符 xff08 关键字 xff09 有三种用法 xff1a 如果一个类被声明为final xff0c 意味着它不能再派生出新的子类 xff0c 即不能被继承 xff0c 因此它和abstract是反义词 将变量声
  • 检查是否为BST

    题目描述 请实现一个函数 xff0c 检查一棵二叉树是否为二叉查找树 给定树的根结点指针TreeNode root xff0c 请返回一个bool xff0c 代表该树是否为二叉查找树 代码如下 xff1a package com mian
  • 线程的sleep()方法和yield()方法有什么区别?

    需要学习资料的 43 微信公众号 学习资源后台找我 本人比较忙 我看到了会在后台帮你 xff0c 谢谢关注啦 sleep 方法给其他线程运行机会时不考虑线程的优先级 xff0c 因此会给低优先级的线程以运行的机会 xff1b yield 方
  • 可查询最值的栈

    定义栈的数据结构 xff0c 请在该类型中实现一个能够得到栈最小元素的min函数 代码如下 xff1a 定义两个栈 xff0c 一个stackData xff0c 一个stackMin 将数组中的元素一个个压入stackData栈的时候 x
  • 归并排序(详解)

    时间复杂度 xff1a O xff08 n logn xff09 空间复杂度 xff1a O xff08 n xff09 归并排序分为拆分和归并两个过程 拆分 xff1a 拆分是一个递归的过程 xff0c 实质是将待排序数组均分再均分 xf
  • 双栈队列

    编写一个类 只能用两个栈结构实现队列 支持队列的基本操作 push xff0c pop 给定一个操作序列ope 及它的长度n xff0c 其中元素为正数代表push操作 xff0c 为0代表pop操作 xff0c 保证操作序列合法且一定含p
  • 在进行数据库编程时,连接池有什么作用?

    由于创建连接和释放连接都有很大的开销 xff08 尤其是数据库服务器不在本地时 xff0c 每次建立连接都需要进行TCP 的三次握手 xff0c 释放连接需要进行TCP四次握手 xff0c 造成的开销是不可忽视的 xff09 为了提升系统访
  • Java 虚拟机 gc算法总结

    一 垃圾收集基本的算法 1 引用计数 Reference Counting 为每一个对象添加一个计数器 xff0c 计数器记录了对该对象的活跃引用的数量 如果计数器为0 xff0c 则说明这个对象没有被任何变量所引用 xff0c 即应该进行
  • JAVA设计模式之工厂模式(简单工厂模式+工厂方法模式)

    从jason0539转载 链接地址http blog csdn net jason0539 在面向对象编程中 最通常的方法是一个new操作符产生一个对象实例 new操作符就是用来构造对象实例的 但是在一些情况下 new操作符直接生成对象会带
  • JAVA设计模式之抽象工厂模式

    从jason0539转载 链接地址http blog csdn net jason0539 前面已经介绍过 简单工厂模式和工厂方法模式 xff0c 这里继续介绍第三种工厂模式 xff0d 抽象工厂模式 xff0c 还是以汽车的制造为例 例子
  • 双栈排序

    请编写一个程序 xff0c 按升序对栈进行排序 xff08 即最大元素位于栈顶 xff09 xff0c 要求最多只能使用一个额外的栈存放临时数据 xff0c 但不得将元素复制到别的数据结构中 给定一个int numbers C 43 43
  • TCP/IP协议与UDP/IP协议的区别

    TCP xff08 Transmission Control Protocol xff0c 传输控制协议 xff09 是面向连接的协议 xff0c 也就是说 xff0c 在收发数据前 xff0c 必须和对方建立可靠的连接 一个TCP连接必须
  • 滑动窗口

    有一个整型数组 arr 和一个大小为 w 的窗口从数组的最左边滑到最右边 窗口每次向右边滑一个位置 返回一个长度为n w 43 1的数组res xff0c res i 表示每一种窗口状态下的最大值 以数组为 4 3 5 4 3 3 6 7
  • 数组变树

    对于一个没有重复元素的整数数组 xff0c 请用其中元素构造一棵MaxTree xff0c MaxTree定义为一棵二叉树 xff0c 其中的节点与数组元素一一对应 xff0c 同时对于MaxTree的每棵子树 xff0c 它的根的元素值为
  • JAVA设计模式初探之适配器模式

    转载http blog csdn net jason0539 1 概述 将一个类的接口转换成客户希望的另外一个接口 Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作 2 解决的问题 即Adapter模式使得原本由
  • Linux使用Note

    这个文档正在维护完善中 1 释放Swap空间 依次执行如下命令即可 xff0c span class token function sync span span class token builtin class name echo spa
  • JAVA设计模式之单例模式

    转载http blog csdn net jason0539 概念 xff1a java 中单例模式是一种常见的设计模式 xff0c 单例模式的写法有好几种 xff0c 这里主要介绍三种 xff1a 懒汉式单例 饿汉式单例 登记式单例 单例
  • Java HashMap的工作原理

    本文由 ImportNew miracle1919 翻译自 javacodegeeks 欢迎加入 翻译小组 转载请见文末要求 本文由 ImportNew miracle1919 翻译自 javacodegeeks 欢迎加入 翻译小组 转载请