因为会设计到很多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的运算中,在进行删除的时候,就不能对其属性值进行修改,否则会出现严重的问题。