zoukankan      html  css  js  c++  java
  • (强制)要求覆写equals必须覆写hashCode(原理分析)

    hashCode和equals

    hashCode和equals用来标识对象,两个方法协同工作可用来判断两个对象是否相等。众所周知,根据生成的哈希将数据散列开来,可以使存取元素更快。对象通过调用Object.hashCode()生成哈希值 ;由于不可避免会存在哈希值冲突 的情况 ,因此当hashCode相同时,还需要再调用equals进行一次值的比较;但是若hashCode不同,将直接判定Object不同,跳过equals,这加快了冲突处理效率 。Object类定义中对hashCode和equals要求如下:

    (1)如果两个对象的equals的结果是相等的,则两个对象的hashCode的返回值结果也必要是相同的。

    (2)任何时候覆写equals,都必须同时覆写hashCode。

    在Map和Set集合中,用到这两个方法时,首先判断hashCode的值,如果hash相等,则再判断equals的结果,HashMap的get判断代码如下

    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
    	return (e = getNode(hash(key), key)) ==null ? null :e.value;
    }
    

    if条件表达式中的e.hash == hash是先决条件,只有相等才会执行&&后的部分。如果不相等,则&&后面的equals根本不会被执行。equals不相等时并不强制要求hashCode也不相等,但一个优秀的哈希算法应尽可能地让元素均匀分布,降低冲突概率,即在equals不相等时hashCode也不相等,这样&&或||短路 操作一旦生效,会极大提高程序的执行效率。如果自定义对象作为Map的键 ,那么必须覆写hashCode和equals。此外,因为Set存储的是不可重复的对象。依据hashCode和equals进行判断,所以Set存储的自定义对象也必须覆写这两个方法。此时如果覆写了equals,而没有覆写hashCode,具体会有什么影响,让我们通过以下代码深入体会:

    public class EqualsObject{
    	privite int id;
    	privite String name;
    	
    	public EqualsObject(int id, String name){
    		this.id = id;
    		this.name = name;
    	}
    	
    	@Override
    	public boolean equals(Object obj){
    		// 如果为null,或者并非同类,则直接返回false(第一处)
    		if (obj == null || this.getClass() != obj.class()){
    			return false;
    		}
    		
    		// 如果引用指向同一个对象,则返回true
    		if(this == obj){
    			return true;
    		}
    		
    		// 需要强制类型转换来获取EqualsObject的方法
    		EqualsObject temp = (EqualsObject)obj;
    		// 本示例判断标准是两个属性值相等,逻辑随业务场景不同而不同
    		if (temp.getId() == this.id && name.equals(temp.getName())){
    			return true;
    		}
    		return false;
    	}
    }
    

    第一处说明:首先判断两个对象的类型是否相同,如果不匹配则直接返回false 。此处使用getClass的方式,就是严格限制了只有EqualsObject对象本身才可以执行equals操作。

    这里并没有覆写hashCode,那么把这个对象放到Set集合中去:

    Set<EqualsObject> hashSet = new HashSet<>();
    EqualsObject a = new EqualsObject(1,"one");
    EqualsObject b = new EqualsObject(1,"one");
    EqualsObject c = new EqualsObject(1,"one");
    
    hashSet.add(a);
    hashSet.add(b);
    hashSet.add(c);
    System.out.println(hashSet.size());
    

    输出结果是3,。虽然这些对象显而易见是相同的 ,但在HashSet操作中,应该只剩下一个,为什么结果是3呢?因为如果不覆写hashCode(),即使equals()相等也毫无意义,Object.hashCode()的实现是默认为每一个对象生成不同的int值,它本身是Native 方法,一般与对象内存地址有关。下面查看C++的源码实现:

    VM_ENTRY(jint, JVM_IHashCode(JNIEnv* env,jobject handle))
    	JVMWrapper("JVM_IHashCode");
    	return handle == NULL ? 0 : objectSynchronizer::FashHashCode
    		(THREAN,JNIHandles::resolve_non_null(handle));
    VM_END
    

    ObjectSynchronizer的核心代码如下,从代码分析角度也印证了hashCode就是根据对象的地址进行相关计算得到int类型数值的:

    mark = monitor->header();
    assert(mark->is_neutral(),"invariant");
    hash = mark->hash();
    
    intptr_t hash() const{
        return mask_bits(value() >> hash_shift, hash_mask);
    }
    

    因为EqualsObject没有覆写hashCode,所以得到的是一个与对象地址相关的唯一值,回到刚才的HashSet集合上 ,如果想存储不重复的元素,那么需要在EqualsObject类中覆写hashCode();

    @Override
    public int hashCode(){
    	return id + name.hashCode();
    }
    

    EqualsObject的name属性是String类型,String覆写了hashCode(),所以可以直接调用。equals()的实现方式与类的具体逻辑 有关,但又各部相同,因而应尽量分析源码来确定其判断结果,比如下列代码:

    public class ListEquals(){
    	public static void main(String[] args){
    		LinkedList<Integer> linkedList = new LinkedList<Integer>();
    		linkedList.add(1);
    		ArrayList<Integer> arrayList = new ArrayList<Integer>();
    		arrayList.add(1);
    		
    		if (arrayList.equals(linkedList)){
    			System.out.println("equals is true");
    		} else {
    			System.out.println("equals is false");
    		}
    	}
    }
    

    两个不同的集合类,输出的结果是equals is true。因为ArrayList的equals()只进行了是否为List子类的判断 ,接着调用了equalsRange()方法:

    boolean equalsRange(List<?> other,int form,int to){
    	final Object[] es = elementData;
    	//用var变量接受linkedList的遍历器(第一处)
    	var oit = other.iterator();
    	for(; form < to; form++){
    		//如果linkedList没有元素,则equals结果直接为false;
    		//如果linkList有元素,则再对应的下标进行值的比较(第二处)
    		if (!oit.hasNext() || !Objects.equals(es[form], oit.next())){
    			return false;
    		}
    	}
    	// 如果ArrayList已经遍历完,而linkList还有元素,则equals结果为false
    	return !oit.hasNext;
    }
    

    第一处说明:局部变量类型推断(Local Variable Type Inference)是JDK10引入的变量命名机制 ,一改Java是强类型语言的传统形象,这是Java致力于未来体积跟小、面向生产效率的新语言特性,减少累赘的语法规则,当然这仅仅是一个语法糖 ,Java仍然是一种静态语言。在初始化阶段,在处理var变量的时候,编译器会检测右侧代码的返回类型,并将其类型用于左侧,如下所示:

    var a = "String";
    // 输出:class java.lang.String
    System.out.println(a.getClass());
    var b = Integer.valueOf(7);
    // 输出:class java.lang.Integer
    System.out.println(b.getClass());
    // 编译出错。虽然是var,但是依然存在类型限定
    b = 3.0;
    

    b在第一次赋值时,类型推断为Integer,所以在第二次赋值为double时编译出错。如果一个方法内频繁地使用var,则会大大降低可读性,这是一个权衡,建议当用var定义变量时,尽量不要超过两个 。

    第二处说明:尽量避免通过实例对象引用来调用equals方法,否则容易抛出空指针异常。推荐使用JDK7引入的Objects的equals 方法,源码如下,可以有效地防止equals调用时产生NPE问题:

    public static boolean equals(Object a,Object b){
    	return (a==b) || (a != null && a.equals(b));
    }
    
  • 相关阅读:
    高级(线性)素数筛
    Dijkstra(迪杰斯特拉)算法
    简单素数筛
    【解题报告】 POJ1958 奇怪的汉诺塔(Strange Tower of Hanoi)
    4 jQuery Chatting Plugins | jQuery UI Chatbox Plugin Examples Like Facebook, Gmail
    Web User Control Collection data is not storing
    How to turn on IE9 Compatibility View programmatically in Javascript
    从Javascrip 脚本中执行.exe 文件
    HtmlEditorExtender Ajax
    GRIDVIEW模板中查找控件的方式JAVASCRIPT
  • 原文地址:https://www.cnblogs.com/zhangguangxiang/p/14232504.html
Copyright © 2011-2022 走看看