散列与散列码

 1.初识散列码应用

    先看下面这段代码:

    输出:

    首先会使用Dog和与之相关联的Cat填充HashMap,然后打印Map内容。然后使用标识数字为3的Dog作为键,查找相关的map信息并打印。

    detectString()方法使用反射机制来实例化及使用Dog类或任何由Dog派生出来的类。如果我们为解决当前的问题从Dog继承创建了一个新类型的时候,detectString()方法使用的这个技巧就变得很有用了。

 

    结果却显示,它不工作。问题出在Dog类继承自Object,他的散列码生成方式Object.hashCode()默认是按照地址值生成的,于是前一个dog.newInstance(3)的散列码不同于后一个dog.newInstance(3)的散列码;这就导致了不能查找到相应信息。

    这里仅仅重载hashcode()是不够的,还需要重载equals()方法,因为HashMap是通过equals()来判断当前的键是否与表中存在的键相同。

    正确的equals()方法必须满足下列5个条件:

a)自反性:对任意的x.equals(x)一定返回true。

b)对称性:对任意x、y,如果x.equals(y)为true,则y.equals(x)也为true。

c)传递性:对于任意x、y、z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)也返回true。

d)一致性:若x.equals(y)返回true,无论重复多少次都返回true。

e)对任何不是null的x,x.equals(null)一定返回false。

 

    再次强调,默认的Object.euqals()只是比较对象的地址,所以一个Dog(3)并不等于另一个Dog(3)。因此,如果要使用自己的类作为HashMap的键,必须同时重载hashCode()和equals()。

    如下:

    输出:

 

    此处,hashCode()返回的是Dog的number标识码,equals()比较的也是number。instanceof悄悄的进行了非null的判断。

 

2.理解hashCode()

    前面的例子只是正确解决问题的第一步,它只是说明,不为你的键覆盖hashCode()和equals(),那么使用散列的数据结构(HashSet,HashMap,LinkedHashSet,LinkedHashMap)就不能正确处理你的键。要更好的解决此问题,必须了解这些数据结构的内部构造。

    首先,使用散列的目的在于:想要使用一个对象来查找另一个对象。与散列实现相反,下面的示例用一对ArrayList实现了一个Map。

 

    其中put(),get()等方法都遵循了Map规范。

    这里,这个被称为MapEntry的十分简单的类可以保存和读取键与值,它在entrySet()中用来产生键值对Set。但是这并不是一个恰当的实现,因为它创建了键和值的副本。entrySet()的恰当实现应该是在Map中提供试图,而不是副本,并且这个视图允许对原始映射表进行修改(副本就不行)。

 

3.为速度而散列

    ImplMap.java说明创建一种新的Map并不难。但是他不是很快,它的问题在于对键的查询,键没有按照任何特定顺序保存,所以只能使用简单的线性查询,而线性查询时最慢的查询方式。

    散列的价值在于速度:散列使得查询快速进行。解决方案之一可以是:先将键排序,然后通过binerySearch()查询。但是散列更进一步:它将键保存在某处,以便能够很快找到。存储一组元素最快的数据结构是数组,所以使用它来表示键的信息(而不是键本身),但是数组不能调整容量,这里的解决方案就是,数组并不保存键本身,而是通过键对象生成一个数字,将其作为数组的下标,这个数字就是散列码,由hashCode()生成。

    为解决数组容量被固定的问题,不同的键可以产生相同的下标。也就是说,可能会有冲突。因此,数组多大就不重要了,任何键总能在数组中找到他的位置。

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

    来实现一个简单的散列Map吧:

    输出:

    散列表中的"槽位"(slot)通常称为桶位(bucket),因此我们将表示实际散列表的数组命名为bucket。为使散列分布均匀,桶的数量通常使用质数。注意,为了能够自动处理冲突,使用的是LinkedList的数组;每个新的元素只是直接添加到list末尾的某个特定桶中。即使Java不允许你创建泛型数组,那你也可以创建指向这种数组的引用。这里,向上转型为这种数组很方便,这样可以阻止在后面的代码中进行额外的转型。

    对于put(),hashCode()将针对键而被调用,并且其结果强制取正。为了使产生的数字适合bucket数组的大小,取模操作符将按照该数组的尺寸取模。get()与put()使用相同的取索引方式。

    注意,这个实现并不意味着对性能进行了调优;它只是想要展示散列映射表执行的各种操作。如果你浏览一下java.util.HashMap的源代码,你就会看到一调过优的实现。同样,为了简单,SimpleHashMap使用了与SlowMap相同的方式来实现entrySet(),这个方法有些过于简单,不能用于通用的Map。

4.覆盖hashCode()

    明白如何散列以后,编写自己的hashCode()就更有意义了。

    首先,你无法控制bucket数组的下标值的产生。这个值依赖于具体的HashMap对象的容量,而容量的改变与容器的充满程度和负载因子有关。hashCode()生成的结果,经过处理以后就是桶位的下标。

    设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该生成同样的值。所以,如果你的hashCode()方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode()就会产生一个不同的散列码,相当于产生一个不同的键。

    此外,也不应该使hashCode()依赖于具有唯一性的对象信息,尤其是使用this的值。这很糟糕!因为这样做无法生成一个新的键,使之与put()中原始的键值对中的键相同。所以,应该使用对象内有意义的标识符。

    以String类为例。String有个特点:如果程序中有多个String对象,都包含相同的字符串序列,那么这些String对象都映射到同一块内存区域。所以new String("Hello")生成的两个实例,虽然是相互独立的,但是对它们使用hashCode()应该生成同样的结果。对于String而言,hashCode()明显是基于String的内容的。

    要想使hashCode()实用,它必须速度快,并且必须是有意义。也就是说,它必须基于对象的内容生成散列码。但是散列码不必是独一无二的(应该更关心生成速度,而不是唯一性),但是通过hashCode()和equals(),必须能够完全确定对象的身份。

    因为在生成桶的下标之前,hashCode()还需要做进一步的处理,所以散列码的生成范围并不重要,只要int即可。

    好的hashCode()应该产生分布均匀的散列码。如果散列码都集中在一块,那么HashMap和HashSet在某些区域的负载会很重,这样就不如分布均匀的散列函数快。

   

   

   

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值