hashcode和equals

equals方法

超类Object中有这个equals()方法,该方法主要用于比较两个对象是否相等。该方法的源码如下:

public boolean equals(Object obj) {
    return (this == obj);
}

所有的对象都拥有标识(内存地址)和状态(数据),同时“==”比较两个对象的的内存地址,所以说使用Object的equals()方法是比较两个对象的内存地址是否相等,即若object1.equals(object2)为true,则表示equals1和equals2实际上是引用同一个对象。虽然有时候Object的equals()方法可以满足一些基本的要求,但是必须要清楚很大部分时间都是进行两个对象的比较,这个时候Object的equals()方法就不可以了,实际上JDK中,String、Math等封装类都对equals()方法进行了重写。下面是String的equals()方法:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = count;
        if (n == anotherString.count) {
        char v1[] = value;
        char v2[] = anotherString.value;
        int i = offset;
        int j = anotherString.offset;
        while (n-- != 0) {
            if (v1[i++] != v2[j++])
            return false;
        }
        return true;
        }
    }
    return false;
    }

对于这个代码段if (v1[i++] != v2[j++])return false;可以非常清晰的看到String的equals()方法是进行内容比较,而不是引用比较。至于其他的封装类都差不多。

在Java规范中,它对equals()方法的使用必须要遵循如下几个规则:

equals 方法在非空对象引用上实现相等关系:

  1. 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
  2. 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
  3. 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
  4. 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
  5. 对于任何非空引用值 x,x.equals(null) 都应返回 false。

在equals()中使用getClass进行类型判断

在覆写equals()方法时,一般都是推荐使用getClass来进行类型判断,不是使用instanceof。都清楚instanceof的作用是判断其左边对象是否为其右边类的实例,返回boolean类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。注意后面这句话:可以用来判断继承中的子类的实例是否为父类的实现。

public class Person {
    protected String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public Person(String name){
        this.name = name;
    }
    
    public boolean equals(Object object){
        if(object instanceof Person){
            Person p = (Person) object;
            if(p.getName() == null || name == null){
                return false;
            }
            else{
                return name.equalsIgnoreCase(p.getName());
            }
        }
        return false;
    }
}
public class Employee extends Person{
    private int id;
    
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Employee(String name,int id){
        super(name);
        this.id = id;
    }
    
    /**
     * 重写equals()方法
     */
    public boolean equals(Object object){
        if(object instanceof Employee){
            Employee e = (Employee) object;
            return super.equals(object) && e.getId() == id;
        }
        return false;
    }
}

上面父类Person和子类Employee都重写了equals(),不过Employee比父类多了一个id属性。测试程序如下:

public class Test {
    public static void main(String[] args) {
        Employee e1 = new Employee("a", 23);
        Employee e2 = new Employee("a", 24);
        Person p1 = new Person("a");
        
        System.out.println(p1.equals(e1)); //true
        System.out.println(p1.equals(e2)); //true
        System.out.println(e1.equals(e2)); //false
    }
}

对于那e1!=e2非常容易理解,因为他们不仅需要比较name,还需要比较id。但是p1即等于e1也等于e2,这是非常奇怪的,因为e1、e2明明是两个不同的类,但为什么会出现这个情况?首先p1.equals(e1),是调用p1的equals方法,该方法使用instanceof关键字来检查e1是否为Person类,这里再看看instanceof:判断其左边对象是否为其右边类的实例,也可以用来判断继承中的子类的实例是否为父类的实现。他们两者存在继承关系,肯定会返回true了,而两者name又相同,所以结果肯定是true。

所以出现上面的情况就是使用了关键字instanceof。故在覆写equals时推荐使用getClass进行类型判断。而不是使用instanceof。

hashCode的作用

在Java集合中有两类,一类是List,一类是Set他们之间的区别就在于List集合中的元素师有序的,且可以重复,而Set集合中元素是无序不可重复的。对于List好处理,但是对于Set而言要如何来保证元素不重复呢?通过迭代来equals()是否相等。数据量小还可以接受,当数据量大的时候效率可想而知(当然可以利用算法进行优化)。比如向HashSet插入1000数据,难道真的要迭代1000次,调用1000次equals()方法吗?hashCode提供了解决方案。怎么实现?先看hashCode的源码(Object)。

public native int hashCode();

它是一个本地方法,它的实现与本地机器有关,这里暂且认为他返回的是对象存储的物理位置(实际上不是,这里写是便于理解)。当向一个集合中添加某个元素,集合会首先调用hashCode方法,这样就可以直接定位它所存储的位置,若该处没有其他元素,则直接保存。若该处已经有元素存在,就调用equals方法来匹配这两个元素是否相同,相同则不存,不同则散列到其他位置。这样处理,当存入大量元素时就可以大大减少调用equals()方法的次数,极大地提高了效率。

所以hashCode在上面扮演的角色为寻域(寻找某个对象在集合中区域位置)。hashCode可以将集合分成若干个区域,每个对象都可以计算出他们的hash码,可以将hash码分组,每个分组对应着某个存储区域,根据一个对象的hash码就可以确定该对象所存储区域,这样就大大减少查询匹配元素的数量,提高了查询效率。

hashCode对于一个对象的重要性

对于List集合、数组而言,他就是一个累赘,但是对于HashMap、HashSet、HashTable而言,它变得异常重要。所以在使用HashMap、HashSet、HashTable时一定要注意hashCode。对于一个对象而言,其hashCode过程就是一个简单的Hash算法的实现,其实现过程对实现对象的存取过程起到非常重要的作用。

HashMap和HashTable这两种数据结构,虽然他们存在若干个区别,但是他们的实现原理是相同的,这里以HashTable为例阐述hashCode对于一个对象的重要性。

一个对象势必会存在若干个属性,如何选择属性来进行散列考验着一个人的设计能力。如果将所有属性进行散列,这必定会是一个糟糕的设计,因为对象的hashCode方法无时无刻不是在被调用,如果太多的属性参与散列,那么需要的操作数时间将会大大增加,这将严重影响程序的性能。但是如果较少属相参与散列,散列的多样性会削弱,会产生大量的散列“冲突”,除了不能够很好的利用空间外,在某种程度也会影响对象的查询效率。其实这两者是一个矛盾体,散列的多样性会带来性能的降低。

那么如何对对象的hashCode进行设计,存在这样一种解决方案:设置一个缓存标识来缓存当前的散列码,只有当参与散列的对象改变时才会重新计算,否则调用缓存的hashCode,这样就可以从很大程度上提高性能。

在HashTable计算某个对象在table[]数组中的索引位置,其代码如下:

int index = (hash & 0x7FFFFFFF) % tab.length;

为什么要&0x7FFFFFFF?因为某些对象的hashCode可能会为负值,与0x7FFFFFFF进行与运算可以确保index为一个正数。通过这步可以直接定位某个对象的位置,所以从理论上来说是完全可以利用hashCode直接定位对象的散列表中的位置,但是为什么会存在一个key-value的键值对,利用key的hashCode来存入数据而不是直接存放value呢?这就关系HashTable性能问题的最重要的问题:Hash冲突。

冲突的产生是由于不同的对象产生了相同的散列码,假如设计对象的散列码可以确保99.999999999%的不重复,但是有一种绝对且几乎不可能遇到的冲突是绝对避免不了的。hashcode返回的是int,它的值只可能在int范围内。如果存放的数据超过了int的范围呢?这样就必定会产生两个相同的index,这时在index位置处会存储两个对象,就可以利用key本身来进行判断。所以具有相索引的对象,在该index位置处存在多个对象,必须依靠key的hashCode和key本身来进行区分。

对于hashCode,遵循如下规则:

  1. 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。

  2. 如果两个对象根据equals(Object o)方法是相等的,则调用这两个对象中任一对象的hashCode方法必须产生相同的整数结果。

  3. 如果两个对象根据equals(Object o)方法是不相等的,则调用这两个对象中任一个对象的hashCode方法,不要求产生不同的整数结果。但如果能不同,则可能提高散列表的性能。

如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。

如果x.equals(y)返回“false”,那么x和y的hashCode()有可能相等,也有可能不等。

两者先用hashcode方法判断,再用equals方法判断。

整个处理流程是:

判断两个对象的hashcode是否相等,若不等,则认为两个对象不等,若相等,则比较equals。

若两个对象的equals不等,则可以认为两个对象不等,否则认为他们相等。

posted @ 2021-11-14 18:57  笑忘书丶  阅读(42)  评论(0)    收藏  举报