简析散列和散列码

在学习散列集集或者图(Map)时,我们也许对这两个方法并不陌生:equals()和hashCode();添加到散列集中的对象必须以正确地方式实现hashCode()方法才能确保集合中不出现重复的值。最近在阅读《Thinking in Java》这本书时,里面有对其系统地介绍,现将其总结一下:

初步了解散列码

在开发中,我们对于HashMap的使用并不陌生,但有时却也会犯错。我们先看一个例子:

//定义人这个类,设定一个编号
class Person {
    private int cardId;
    public Person(int cardId) {this.cardId= cardId;}

    @Override
    public String toString() {
        return "Person#" + cardId;  //返回编号
    }
}

//人的性别
class Sex {
    private int  i = (int) (Math.random() * 2);

    @Override
    public String toString() {
        return i == 0? "male" : "female";  //随机返回一个性别
    }
}

public class TestMap {
    public static void main(String[] args) throws Exception{
        detectSex(Person.class);
    }

    public static <T extends Person> void detectSex(Class<T> type) throws Exception {
        Constructor<T> person = type.getConstructor(int.class);
        Map<Person, Sex> map = new HashMap<>();  //创建一个HashMap用于存放人和性别的键值对
        for (int i = 0; i < 10; i++) {
            map.put(person.newInstance(i), new Sex());
        }
        System.out.println("map:" + map);
        Person p = person.newInstance(3);  //创建一个编号为3的人,并在map中查找此人性别
        System.out.println("查找某人性别" + p);
        if (map.containsKey(p))
            System.out.println(map.get(p));
        else
            System.out.println("无此人");
    }
}

输出结果:

map:{Person#7=female, Person#0=female, Person#9=male, Person#3=female, Person#5=male, Person#8=female, Person#2=male, Person#4=male, Person#1=male, Person#6=male}
查找Person#3性别
无此人

这个程序看起来简单,但却在结果上出现了错误。原因在于Person默认继承于Object类,所以使用了Object类的hashCode()方法来生成散列码(根据地址并通过一定的算法产生)。明显,第一次创建的和第二次创建的Person#3地址不同,散列码是不同的,因此无法找到此人。所以,我们要以正确地形式实现hashCode()方法。当然,在覆盖hashCode()方法时,也要覆盖equals()方法,因为这个方法也来自Object类,也需要以正确地方式实现。
这里提到equals()方法时,我们必须要注意以下几点:

1)自反性 对于任意x, x.equals(x)必定返回true。
2)对称性 对于任意x,y,x.equals(y)与y.equals(x)的返回值相同。
3)传递性 对于任意x,y,z,如果x.equals(y)与y.equals(z)返回值相同,那么x.equals(z)返回值也相同。
4)一致性 对于任意的x,y,无论x.equals(y)执行多少次,返回值要么是true,要么为false。
5)对于任意x != null, x.equals(null)返回false。
(作为学生党第一次感觉自己学的离散结构知识用上了T-T)

那么有了这些理念后再修改一下Person类:

class Person {
    private int cardId; 
    public Person(int cardId) {this.cardId= cardId;}

    @Override
    public String toString() {
        return "Person#" + cardId;
    }

    @Override
    public boolean equals(Object obj) {
        return  (obj instanceof Person && (((Person) obj).cardId == cardId)); 

    }

    @Override
    public int hashCode() {
        return cardId;  //把唯一的编号作为散列码
    }
}

再运行看结果:

map:{Person#0=male, Person#1=male, Person#2=female, Person#3=female, Person#4=male, Person#5=female, Person#6=male, Person#7=male, Person#8=female, Person#9=female}
查找Person#3性别
female

如果我们比较时至比较编号,并且把编号作为散列码,这样子覆盖hashCode()和equals()方法程序就正常输出结果了。

散列的实现方式

在我们了解散列的实现方式之前,首先要知道散列的意义是什么:散列不仅能够方便在某些情况下,如(Map),通过一个对象来查找另一个对象,它的价值更在于速度,即散列能使查询得以快速进行。
那么散列是如何实现的呢?我们知道存储一组元素最快的数据结构是数组,因此使用了数组来存储键的信息,该数组形成的表又称散列表,哈希表。数组并不保存键本身,而是保存通过算法将键对象转换过来的一个数字,而这个数字作为数组的下标,这个数字称为散列码。就像上面Person类中,覆盖了hashCode()方法,这个方法称为散列函数,用于产生散列码。
那么我们可能会想,如果说当前有很多个对象,即有很多键,数组又是固定大小的,会不会出现存储不下的现象?答案是这样的:不同的键值可以产生相同的下标,可能会有冲突(如果没有冲突,那么这个散列函数称为完美散列函数)。那么怎么样解决冲突呢?
解决方案是这样子的:冲突通常由外部链接处理,数组并不保存值,而是保存一个list,然后对list中的值使用equals()方法来查询(显然这里的查询会慢一点)。如果我们存储的数值比较少,不产生冲突的话,那么就可以直接通过数组下标确定位置,索引的速度就会相当快(这也是HashSet,HashMap索引快的原因)。我们可以看一个图理解一下:

关于String与equals()、hashCode()

我们知道,当我们执行下面的语句时:

String s = new String("scau");
String s1 = new String("scau");

这里会产生个对象,如图所示:

显然,当我们用”==”比较两个String对象是返回false,即两个对象是不相等的。但我们用equals()比较时,却能返回true(可以得知String覆盖了equals()方法)。但是它们却共同映射到同一块内存区域,它们的hashCode()产生相同的值。我们可以测试一下:

public static void main(String[] args) {
    String s = new String("scau");
    String s1 = new String("scau");
    System.out.println(s.hashCode() == s1.hashCode());
}

结果:true

Bingo!

你可能感兴趣的:(java,HashCode)