hashcode和equals及哈希算法理解

因为会设计到很多equal的知识,所以先深入理解一下equals().

1.equals()

Object类中的默认equals()方法和==是没有区别的,都是判断两个对象是否指向同一个引用,内存地址是否相同,即是否就是一个对象。而string类和integer等,都需要重写equals()方法,用来判断两个对象的值是否相等,而不是内存地址是否相同。所以,如果元素要存储到HashSet集合中,必须覆盖equals方法。一般情况下,如果定义的类会产生很多对象,比如人,学生,书,

通常都需要覆盖equals。建立对象判断是否相同的依据

这边有个细节,当我们创建类的时候,默认继承object里面的equal,而集合里面有方法比如contains(),还有remove(),判断是否包含某个元素和移除元素,它得底层也是通过equal来判断的,所以一定要注意根据自己的需求来重新定义equals。其他的集合对象里面也是这样,只是数据结构不同,判断结构稍有差距。

2.hashCode(以hashset为例)

HashSet: 内部数据结构是哈希表 ,是不同步的。
如何保证该集合的元素唯一性呢?
是通过对象的hashCode和equals方法来完成对象唯一性的。
如果对象的hashCode值不同,那么不用判断equals方法,就直接存储到哈希表中。 
如果对象的hashCode值相同,那么要再次判断对象的equals方法是否为true。
如果为true,视为相同元素,不存。如果为false,那么视为不同元素,就进行存储。

                

                为什么要使用hashcode这种方法呢?

Set元素无序,但元素不可重复。要想保证元素不重复,两个元素是否重复应该依据什么来判断呢?用Object.equals方法。

但若每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说若集合中

已有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。于是Java采用

了哈希表的原理。

当Set接收一个元素时根据该对象的内存地址算出hashCode ,这样根据hashcode来将元素放到相应的位置,这也是它为

什么是无序的原因,但这样大大提高了hashset的效率,只有当hashcode的值一样时,才需要调用equals()。

所以:如果元素要存储到HashSet集合中,必须覆盖hashCode方法和equals方法。一般情况下,如果定义的类会产生很多对象,

比如人,学生,书,通常都需要覆盖equals,hashCode方法。建立对象判断是否相同的依据


首先我们来看第一个例子:创建一个student对象,包含name和age两个属性。

public class HashSetTest {

	public static void main(String[] args) {
		HashSet hs=new HashSet();
		
		hs.add(new Student("wujie1", 21));
		hs.add(new Student("wujie2", 22));
		hs.add(new Student("wujie3", 23));
		hs.add(new Student("wujie14", 24));
		hs.add(new Student("wujie1", 21));
		
		Iterator iterator=hs.iterator();
		while(iterator.hasNext()){
			Student student=(Student)iterator.next();
			System.out.println(student.getName()+"..."+student.getAge());
		}
	}

}


那么我们知道,set集合对象中元素是唯一的,那按理,第一条和最后一条是重复的,只应该留下一个,那为什么两个都留下来了呢?

第一个原因就是euqals方法,我们知道,set通过equal来判断两个对象是否相等,而在object中,euqal的作用是和==一样的,就是判断两个对象是否相等而不是相同,就是是否指向同一个引用,显然,我们New了五个不同的对象,所以在内存中他们的地址都是不同的,所以equals判断是五个不同的对象,当然都存了进来。所以我们得把判断是否相等得依据封装到equals()方法中。

第二个原因就是hashcode,我们并没有重写hashcode方法,还是用默认的hashcode的方法。

所以我们在student重写两个方法

@Override
	public int hashCode() {
//		System.out.println(this+".......hashCode");
		
		return name.hashCode()+age*27;
//		return 100;
	}
	@Override
	public boolean equals(Object obj) {
		
		if(this == obj)
			return true;
		if(!(obj instanceof Student
			throw new ClassCastException("类型错误");
		
//		System.out.println(this+"....equals....."+obj);
		Student p = (Student)obj;
		
		
		
		return this.name.equals(p.name) && this.age == p.age;
	}

首先给每个对象算出hash值,如果相等了,在调用equals方法。

在参考别人的博客时(http://blog.csdn.net/jiangwei0910410003/article/details/22739953),还有一个发现很好玩,就自己去测了一下。将equals方法直接返回false,hashcode不变,那按理,添加最后一个s1的时候先判断hashcode是否相同,因为时同一个对象,所以肯定相同,那之后调用equals是返回false,应该添加进去啊?为什么打印的size是3不是4呢?

public static void main(String[] args) {
		HashSet hs=new HashSet();
		Student s1=new Student("wujie5", 25);
		hs.add(s1);
		hs.add(new Student("wujie2", 22));
		hs.add(new Student("wujie3", 23));
		hs.add(s1);
		System.out.println(hs.size());
		/*Iterator iterator=hs.iterator();
		while(iterator.hasNext()){
			Student student=(Student)iterator.next();
			System.out.println(student.getName()+"..."+student.getAge());
		}*/
	}

因为Hashset是基于Hashmap实现的,它的add方法也是基于hashmap的put方法实现的,所以我们来看hashmap的add方法。

 public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

首先是判断hashCode是否相等,不相等的话,直接跳过,相等的话,然后再来比较这两个对象是否相等或者这两个对象的equals方法,因为是进行的或操作,所以只要有一个成立即可,那这里我们就可以解释了,其实上面的那个集合的大小是3,因为最后的一个r1没有放进去,以为r1==r1返回true的,所以没有放进去了。所以集合的大小是3,如果我们将hashCode方法设置成始终返回false的话,这个集合就是4了。所以指向同一个引用的对象,只可能被放进去一次。


还有一个很严重的问题就是Hashcode造成的内存泄漏。看代码。

public static void main(String[] args) {
		HashSet<Student> hs=new HashSet<Student>();
		Student s1=new Student("wujie5", 25);
		hs.add(s1);
		hs.add(new Student("wujie2", 22));
		hs.add(new Student("wujie3", 23));
		//hs.add(s1);
		s1.setAge(27);
		System.out.println("删除前的大小"+hs.size());
		hs.remove(s1);
		System.out.println("删除前的大小"+hs.size());
}

我们remove了一个,那集合的size应该还剩2,但是测试结果size还是3.这就是大问题了,不用的对象结果还在内存当中,那这样时间长了内存肯定会满了。为什么会这样呢?以下为remove源码。hashset的emove方法同样是以hashmap的remove方法为基础的,我们直接看hashmap的remove()。

 public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

这边又出现了一个removeEntryKey().再看。

final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }
  • 我们看到,在调用remove方法的时候,会先使用对象的hashCode值去找到这个对象,然后进行删除,这种问题就是因为我们在修改了r3对象的y属性的值,又因为RectObject对象的hashCode方法中有y值参与运算,所以r3对象的hashCode就发生改变了,所以remove方法中并没有找到r3了,所以删除失败。即r3的hashCode变了,但是他存储的位置没有更新,仍然在原来的位置上,所以当我们用他的新的hashCode去找肯定是找不到了。

上面的这个内存泄露告诉我一个信息:如果我们将对象的属性值参与了hashCode的运算中,在进行删除的时候,就不能对其属性值进行修改,否则会出现严重的问题。



猜你喜欢

转载自blog.csdn.net/qq_37891064/article/details/79590529
今日推荐