关闭

HashSet中消失的元素和多出来的元素

标签: Java基础
341人阅读 评论(0) 收藏 举报
分类:

首先给出一个辅助类:

package disappearElementInHashset;

public class Person {
	private String name;

	public Person(String name) {
		super();
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return "Person [name=" + name + "]";
	}

}

需要注意的是,这个类没有重写Object类的hashCode方法和equals方法。接下来给出一个测试类引出我们的主题:

package disappearElementInHashset;

import java.util.HashSet;
import java.util.Iterator;

public class Test {

	public static void main(String[] args) {
		HashSet<Person> personSet = new HashSet<>();
		Person me = new Person("liyuncong");
		personSet.add(me);
		me.setName("wang");
		personSet.add(me);
		
		Iterator<Person> iterator = personSet.iterator();
		while(iterator.hasNext()) {
			System.out.println(iterator.next());
		}
                System.out.println(personSet.size());
 }

}

测试类的打印结果:

Person [name=wang]

1


我们往HashSet里放了两个元素,可是为什么只有一个元素呢,而且只有后面放进去的那个元素?(!!!提醒一下,后面这段解释是不对的)其实这是很好理解,因为集合中不能存在相等的元素,而me在修改前后是相等的,之所以me在修改前后是相等的,是因为Person并没有重写Object类的equals方法,Person实例的相等性比较,只是比较地址而已,而me指向的对象的值虽然被修改了,但是me始终都指向同一块空间。因此,第二个me并没有放进去。从HasHSet中打印出来的值成了me指向对象修改的值,是因为HashSet中保存的引用和me指向同一块地方。

这个解释听上去很有道理,但其实是不完整的。接下来,我们往Person里加入自定义的hashCode方法和equals方法。如下:

package disappearElementInHashset;

public class Person {
	private String name;

	public Person(String name) {
		super();
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return "Person [name=" + name + "]";
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	
}

这时,我们再次运行上面的测试类,结果如下:

Person [name=wang]
Person [name=wang]
2

HashSet里面放了两个一样的元素,惊到我了。我们都敢确定,打印出来的两个对象是相等的,(它们不仅值相等,而且地址页相等,因为它们就是同一个对象,自始至终都只有一个对象,流浪在外的只是指向这个对象的多个指针),可是怎么都放进去了呢?这时,我觉得有必要看看HashSet的add方法的源码了。因为HashSet是基于HashMap实现的,于是,我们只需要看看HashMap的源码就行了,源码如下:

	public V put(K key, V value) {
		// 如果table之前为空,就创建一个更大的table
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 如果key为null,就用对应的put方法
        if (key == null)
            return putForNullKey(value);
        // 计算key的哈希值
        int hash = hash(key);
        // 根据哈希值和table长度计算该键值对应该存储在那个桶中
        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;
    }

这些明白了。HashSet是先通过对象的哈希值把对象找到相应的桶,然后再桶中寻找有没有相等对象。我们的程序中虽然自始至终都只有一个对象,时时刻刻都与自身相等,但是修改前后的哈希值却不同,第一次把指向对象的引用放在了一个桶中,而第二次却是在另一个桶中寻找是否已经存在相等的元素,就这样,程序中的结果产生了。

在最初的程序中,之所以只放进去了一个元素,是因为Object类的hashCode方法通过地址计算哈希值,地址没变,哈希值也就没变,于是第二个引用是试图放在和第一个引用同一个桶中,自然失败了。

程序中这样的问题其实是非常隐蔽的,所以,编码时,要关注各种引用之间千丝万缕的联系,还有关注自己类中equals方法和hashCode方法的定义。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:196671次
    • 积分:3663
    • 等级:
    • 排名:第8868名
    • 原创:153篇
    • 转载:0篇
    • 译文:21篇
    • 评论:50条
    博客专栏
    最新评论