HashSet容器的重复性问题

大家都知道,HashSet这个容器是具有元素唯一性的,那我们就举一个例子验证一下:

public class Test {
	public static void main(String args[]) {	
		HashSet<Person> hs = new HashSet<>();			
		hs.add(new Person(28, "王长贵"));
		hs.add(new Person(35, "谢永强"));
		hs.add(new Person(23, "赵玉田"));
		hs.add(new Person(23, "赵玉田"));
		Iterator<Person> it = hs.iterator();
		while(it.hasNext()) {
			Person p = it.next();
			System.out.println(p.getName() + p.getAge() + "岁");
		}
	}
}

class Person{
	String name;
	int age;
	
	public Person(int age, String name) {
		this.age = age;
		this.name = name;
	}
	
	public String getName() {
		return name;
	}

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

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
}

在主方法的下面我们创建了一个类Person并设置了名字和年龄的构造方法还有getter、setter方法,之后在主方法里面创建了一个HashSet容器并在里面添加了几个对象(赵玉田是重复的),最后迭代打印出来,运行结果如下:

在这里插入图片描述
诶?为啥容器里面有俩玉田呢?要弄清楚这个问题,还要从HashSet存储流程说起,下面简单的介绍一下哈希表存储的原理:

简单说,哈希表里面还是一个数组,那数组要怎么查找呢?遍历(就是一个一个比对),这种方法是较慢的。为了解决这个问题,哈希表在存储数据的时候,是根据元素自身的特性来计算出一个位置,接着把这个元素存到那个位置里面去。这样做的好处是,当查询一个元素时,我们只需要再次计算出它的位置,之后直接到那个位置去找就可以了(不用一个一个查了),这种计算的方法就是哈希算法。

那么HashSet存储的流程就是:比如说现在往HashSet里面存一个对象,那么首先就会由哈希算法(也就是hashCode方法)计算出一个位置,如果这个位置已经有元素了,再通过对象的equals来判断内容是否相同。相同就不存了,这就是唯一性的原理。否则两个元素就是不同的,不同的元素有通过哈希算法计算出的位置相同,这种情况叫做哈希冲突,怎么解决冲突并不是我们要关心的,人家自有解决的办法。画一张图总结一下:

在这里插入图片描述
那么再回到最开始的那个问题,我们自定义的类Person是继承自Object的,Object有自己的hashCode方法,也有自己的equals方法,而Object的equals方法只是简单判断了两个对象的地址是否相同。我们知道,不同对象地址是不同的。暂且不说它们的hashCode是否相同,地址不同的话HashSet就将它们视为不重复了,我们创建了五个Person对象,五个地址都不相同,那么该存还得存,所以取出来的时候还是五个(赵玉田有两个)。

换一个角度去想这个问题,我们判断两个人是否相同的依据是姓名和年龄是否都相同,但是计算机不知道这个依据,它是按照自己的原则去判断的,所以会出现这样的结果。那么能否让计算机也按我们指定的规则去判断呢?

当然是可以的,根据上面讲述的原理,我们只需要重写Person类的hashCode和equals方法即可。

    @Override
	public int hashCode() {
		System.out.println("计算 " + name + " 的位置");
		return  name.hashCode() + age*39;  //尽量保证哈希值唯一所以年龄乘以一个数
	}

    @Override
	public boolean equals(Object arg0) {
		System.out.println("比较  " + name + " 的内容");
		if(arg0 == this) {
		    //如果待比较的对象是它自己,直接返回true即可
			return true;
		}
		if(!(arg0 instanceof Person)) {
		    //增加程序健壮性
			throw new ClassCastException("类型错误");
		}
		Person p = (Person) arg0;	
		//如果姓名年龄都一样则视为一个人
		return p.getAge()==age && p.getName().equals(name);
	}

在上面的代码中,hashCode方法里返回的哈希值是根据年龄和姓名特点计算出来的,equals方法中我们自己创建了比较的规则,当然了如果待比较的对象就是自己的话,那就不用比直接返回true即可;此外在这两个方法中都打印了一下来验证方法是否被执行。

再次运行程序,结果如下:

在这里插入图片描述
可以看到,每次调用add方法都会调用hashCode方法来计算位置,而当位置相同时(两个23岁的赵玉田hashCode计算结果相同)才会调用equals来比较内容,比较发现内容一样(这是我们自己定的比较规则)所以不存,故最后容器中只有3个元素,这样便实现了自定义的存放规则。

讲完了这个例子之后,我们最后再来看一个小插曲:

HashSet<String> hs = new HashSet<>();			
hs.add(new String("刘能"));
hs.add(new String("王大拿"));
hs.add(new String("刘大脑袋"));
hs.add(new String("王天来"));
hs.add(new String("王天来"));
Iterator<String> it = hs.iterator();
while(it.hasNext()) {
	String str = it.next();
	System.out.println(str);
}

这个例子里面在HashSet里面添加了五个用new关键字创建的字符串,其中有两个字符串都是王天来,运行结果如下:

在这里插入图片描述
诶?为啥只有4个呢?刚刚不是说不同对象地址不同吗,那么计算机应该将两个王天来其视为不重复阿,为啥容器里只有四个字符串呢?

别傻了,其实在String的源码中重写了equals方法:

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = length();
            if (n == anotherString.length()) {
                int i = 0;
                while (n-- != 0) {
                    if (charAt(i) != anotherString.charAt(i))
                            return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

可以看到是一个字符一个字符的比对,所以String的equals方法返回的是两个字符串的值是否相等(也就是String自己定义的规则),这回总算是真相大白了,那么关于HashSet重复性的问题就说到这里了,希望能给大家带来帮助,同时也欢迎大佬们在评论区留言批评指正。

猜你喜欢

转载自blog.csdn.net/weixin_44965650/article/details/106992144