理解 hashCode()

2024-06-03 19:58
文章标签 理解 hashcode

本文主要是介绍理解 hashCode(),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前面的例子只是正确解决问题的第一步。它只说明,如果不为你的“键”重载 hashCode()
和 equals(),那么使用散列的数据结构(HashSet, HashMap, LinkedHashSet, or 
LinkedHashMap)就无法正确处理你的“键”。然而,要很好地解决此问题,你必须了解
这些数据结构的内部构造。


首先,使用散列的目的在于:想要使用一个对象来查找另一个对象。不过使用 TreeSet 或
TreeMap 也能实现此目的,还可以自己实现一个 Map。要达到此目的,必须提供
Map.entrySet()方法,以生成 Map.Entry 对象的 Set。MPair 被定义为一种新型的
Map.Entry,为了能够将其存入 TreeSet 中,MPair 必须实现 Comparable 接口,并要
重载 equals()方法:


//: c11:MPair.java
// A new type of Map.Entry.
import java.util.*; 
public class MPair implements Map.Entry, Comparable { 
private Object key, value; 
public MPair(Object k, Object v) { 
    key = k;
    value = v;
  }
public Object getKey() { return key; }
public Object getValue() { return value; } 
public Object setValue(Object v) { 
    Object result = value; 
    value = v;
return result; 
  }
public boolean equals(Object o) { 
return key.equals(((MPair)o).key); 
  }
public int compareTo(Object rv) { 
return ((Comparable)key).compareTo(((MPair)rv).key); 
  }
} ///:~


注意,比较所感兴趣的只是“键”,所以重复的“值”是完全可以接受的。


下面的例子使用一对 ArrayList 实现了一个 Map:


//: c11:SlowMap.java
// A Map implemented with ArrayLists.
import com.bruceeckel.simpletest.*; 
import java.util.*; 
import com.bruceeckel.util.*; 


public class SlowMap extends AbstractMap { 
private static Test monitor = new Test(); 
private List 
    keys = new ArrayList(), 
    values = new ArrayList(); 
public Object put(Object key, Object value) { 
    Object result = get(key); 
if(!keys.contains(key)) { 
      keys.add(key); 
      values.add(value); 
    } else
      values.set(keys.indexOf(key), value);
return result; 
  }
public Object get(Object key) {
if(!keys.contains(key)) 
return null;
return values.get(keys.indexOf(key)); 
  }
public Set entrySet() { 
    Set entries = new HashSet();
    Iterator
      ki = keys.iterator(), 
      vi = values.iterator(); 
while(ki.hasNext()) 
      entries.add(new MPair(ki.next(), vi.next())); 
return entries; 
  }
public String toString() { 
    StringBuffer s = new StringBuffer("{"); 
    Iterator
      ki = keys.iterator(), 
      vi = values.iterator(); 
while(ki.hasNext()) { 
      s.append(ki.next() + "=" + vi.next());
if(ki.hasNext()) s.append(", "); 
    }
    s.append("}"); 
return s.toString(); 
  }
public static void main(String[] args) { 
    SlowMap m = new SlowMap(); 
    Collections2.fill(m, Collections2.geography, 15); 
    System.out.println(m); 
    monitor.expect(new String[] { 
"{ALGERIA=Algiers, ANGOLA=Luanda, BENIN=Porto-Novo,"+
" BOTSWANA=Gaberone, BURKINA FASO=Ouagadougou, " + 
"BURUNDI=Bujumbura, CAMEROON=Yaounde, " + 
"CAPE VERDE=Praia, CENTRAL AFRICAN REPUBLIC=Bangui,"+
" CHAD=N'djamena, COMOROS=Moroni, " + 
"CONGO=Brazzaville, DJIBOUTI=Dijibouti, " + 
"EGYPT=Cairo, EQUATORIAL GUINEA=Malabo}"
    });
  }
} ///:~
put()方法只是将“键”与“值”放入相应的 ArrayList。在 main()中装载了一个 SlowMap,
然后通过打印证明它能正常运作。
此例说明创建一种新的 Map 并不困难。但是正如其名 SlowMap 所示,它不会很快,所以
如果有更好的选择,就应该放弃它。它的问题在于对“键”的查询,由于没有排序,所以
只能使用简单的线性查询,而这是最慢的查询方式。


散列的价值在于速度:散列使得查询得以快速进行。由于速度的瓶颈是对“键”的查询,
因此解决方案之一就是保持“键”的排序状态,然后使用 Collections.binarySearch()进
行查询(本章末尾会有一个练习,带着你走完整个过程)。


散列则更进一步,它将“键”保存在某处,使你能够很快速的找到。正如你在本章所看到
的,存储一组元素最快的数据结构是数组,所以使用它来代表“键”的信息(请小心留意,
我是说“键的信息”,而不是“键”本身)。本章也曾讲过,数组有一个特性:一旦分配,
容量就不能改变。因此我们就有一个问题:我们需要能够在 Map 中保存任意数量的“值”,
但是如果“键”的数量被数组的容量限制了,该怎么办呢?


答案就是:数组并不保存“键”本身。而是通过“键”对象生成一个数字,将其作为数组
的下标索引。这个数字就是散列码,由定义在 Object 中的 hashCode()生成(在计算机科
学的术语中称为散列函数)。你的类总是应该重载 hashCode()方法。为解决数组容量被
固定的问题,不同的“键”可以产生相同的下标。也就是说,可能会有冲突(collision)。
因此,数组多大就不重要了,每个“键”总能在数组中找到它的位置。


于是查询一个“值”的过程首先就是计算散列码,然后使用散列码查询数组。如果能够保
证没有冲突(如果“值”的数量是固定的,那么就有可能),那你可就有了一个完美的散
列函数,但是这种情况很特殊。通常,冲突是由“外部链接”(external chaining)处理:
数组并不直接保存“值”,而是保存“值”的 list。然后对 list 中的“值”使用 equals()
方法进行线性的查询。这部分的查询自然会比较慢,但是,如果有好的散列函数,数组的
每个位置就只有较少的“值”。因此,不是查询所有的 list,而是快速地跳到数组的某个位
置,只对很少的元素进行比较。这便是 HashMap 会如此快的原因。


理解了散列的原理,就能够实现一个简单的散列 Map 了:


//: c11:SimpleHashMap.java
// A demonstration hashed Map.
import java.util.*; 
import com.bruceeckel.util.*; 


public class SimpleHashMap extends AbstractMap { 
// Choose a prime number for the hash table
// size, to achieve a uniform distribution:
private static final int SZ = 997;
private LinkedList[] bucket = new LinkedList[SZ]; 
public Object put(Object key, Object value) { 
    Object result = null;
int index = key.hashCode() % SZ; 
if(index < 0) index = -index;
if(bucket[index] == null)
      bucket[index] = new LinkedList(); 
    LinkedList pairs = bucket[index]; 
    MPair pair = new MPair(key, value); 
    ListIterator it = pairs.listIterator();
boolean found = false;
while(it.hasNext()) { 
      Object iPair = it.next(); 
if(iPair.equals(pair)) { 
        result = ((MPair)iPair).getValue();
        it.set(pair); // Replace old with new
        found = true;
break;
      }
    }
if(!found) 
      bucket[index].add(pair); 
return result; 
  }
public Object get(Object key) {
int index = key.hashCode() % SZ; 
if(index < 0) index = -index;
if(bucket[index] == null) return null;
    LinkedList pairs = bucket[index]; 
    MPair match = new MPair(key, null);
    ListIterator it = pairs.listIterator();
while(it.hasNext()) { 
      Object iPair = it.next(); 
if(iPair.equals(match)) 
return ((MPair)iPair).getValue(); 
    }
return null;
  }
public Set entrySet() { 
    Set entries = new HashSet();
for(int i = 0; i < bucket.length; i++) {
if(bucket[i] == null) continue;
      Iterator it = bucket[i].iterator(); 
while(it.hasNext())
        entries.add(it.next()); 
    }
return entries; 
  }
public static void main(String[] args) { 
    SimpleHashMap m = new SimpleHashMap(); 
    Collections2.fill(m, Collections2.geography, 25); 
    System.out.println(m); 
  }
} ///:~


由于散列表中的“槽位(slot)”通常称为“桶”(bucket),因此我将作为散列表的数
组命名为bucket。为使散列分布均匀,桶的数量通常使用质数9。注意,为了能够自动处理
冲突,使用了一个LinkedList的数组;每一个新的元素只是直接添加到list的末尾。


如果指定的“键”已经存在于 list 中了,那么 put()将返回与此“键”相关联的“旧”“值”,
否则返回 null。本例中返回值是 result,它被初始化为 null,如果此“键值对”已经存在
于 list 中,则 result 被赋值为 list 中此“键”对应的“值”。


方法 put()和 get()要做的第一件事情,是对“键”调用 hashCode()。结果被强制转换为
正数,然后用数组容量对其取模,使它适合 bucket 数组的大小。如果数组的某个位置是
null,这表示还没有元素被散列至此,所以,为了保存定位于此的第一个对象,需要创建一
个新的 LinkedList。一般的过程是,查看当前位置的 list 中是否有相同的元素,如果有,
则将旧的“值”赋给 result,然后用新的“值”取代旧的“值”。标记 found 用来跟踪是
否找到(相同的)旧的“键值对”,如果没有,则将新 pair 添加到 list 的末尾。


方法 get()的代码与 put()相似,不过更简单。首先计算 bucket 数组的下标,如果此位置
有 LinkedList 存在,就对其进行查询。


entrySet()遍历所有的 list,将其中的元素加入到作为结果的 Set 中。有了这个方法,便

可以进行用“值”填充 Map,然后将它们打印出来的测试了。


这篇关于理解 hashCode()的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1027962

相关文章

深入理解Apache Airflow 调度器(最新推荐)

《深入理解ApacheAirflow调度器(最新推荐)》ApacheAirflow调度器是数据管道管理系统的关键组件,负责编排dag中任务的执行,通过理解调度器的角色和工作方式,正确配置调度器,并... 目录什么是Airflow 调度器?Airflow 调度器工作机制配置Airflow调度器调优及优化建议最

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.

深入理解C语言的void*

《深入理解C语言的void*》本文主要介绍了C语言的void*,包括它的任意性、编译器对void*的类型检查以及需要显式类型转换的规则,具有一定的参考价值,感兴趣的可以了解一下... 目录一、void* 的类型任意性二、编译器对 void* 的类型检查三、需要显式类型转换占用的字节四、总结一、void* 的

深入理解Redis大key的危害及解决方案

《深入理解Redis大key的危害及解决方案》本文主要介绍了深入理解Redis大key的危害及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、背景二、什么是大key三、大key评价标准四、大key 产生的原因与场景五、大key影响与危

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

深入理解RxJava:响应式编程的现代方式

在当今的软件开发世界中,异步编程和事件驱动的架构变得越来越重要。RxJava,作为响应式编程(Reactive Programming)的一个流行库,为Java和Android开发者提供了一种强大的方式来处理异步任务和事件流。本文将深入探讨RxJava的核心概念、优势以及如何在实际项目中应用它。 文章目录 💯 什么是RxJava?💯 响应式编程的优势💯 RxJava的核心概念

如何通俗理解注意力机制?

1、注意力机制(Attention Mechanism)是机器学习和深度学习中一种模拟人类注意力的方法,用于提高模型在处理大量信息时的效率和效果。通俗地理解,它就像是在一堆信息中找到最重要的部分,把注意力集中在这些关键点上,从而更好地完成任务。以下是几个简单的比喻来帮助理解注意力机制: 2、寻找重点:想象一下,你在阅读一篇文章的时候,有些段落特别重要,你会特别注意这些段落,反复阅读,而对其他部分