详述HashSet集合中add()方法存储自定义类型对象的执行过程

一、HashMap类中hash(Object key)方法分析

由于在上一篇博客中已详细分析过该方法,此处不再赘述。当传入参数为自定义类型时,由于自定义类中并没有重写hashCode()方法,所以运行时调用的还是Object类中的hashCode()方法,比较的是地址值是否相同。

二、详述HashSet集合中add()方法存储自定义类型对象的执行过程

下面是自定义类代码:

		public class Student{
			
			String id;
		
			public String getId() {
				return id;
			}
			public void setId(String id) {
				this.id = id;
			}
			
			public Student(String id) {
				this.id = id;
			}
			
			@Override
			public int hashCode() {
				return id.hashCode();
			}
			
			@Override
			public boolean equals(Object obj) {
				if (!(obj instanceof Student)) {
					throw new ClassCastException("类型转换错误!");
				}
				Student student = (Student) obj;
				return this.id.equals(student.id);
			}
		}

下面是测试代码:

		HashSet<Student> set = new HashSet<>();
		set.add(new Student("1"));
		set.add(new Student("1"));
		set.add(new Student("2"));

由于在上一篇博客中已详细分析过add()方法存储String类型对象的执行过程,而add()方法存储自定义类型对象的执行过程与之相同,此处不再重复叙述,只将不同之处做详细描述。下面是截取的HashMap类中的putVal方法的代码:

		//HashMap类中的putVal方法
	    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
	                   boolean evict) {
	        Node<K,V>[] tab; Node<K,V> p; int n, i;
	        if ((tab = table) == null || (n = tab.length) == 0)
	            n = (tab = resize()).length;
	        if ((p = tab[i = (n - 1) & hash]) == null)
	            tab[i] = newNode(hash, key, value, null);
	        else {
	            Node<K,V> e; K k;
	            if (p.hash == hash &&
	                ((k = p.key) == key || (key != null && key.equals(k))))
	                e = p;
	            else if (p instanceof TreeNode)
	                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
	            else {
	                for (int binCount = 0; ; ++binCount) {
	                    if ((e = p.next) == null) {
	                        p.next = newNode(hash, key, value, null);
	                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
	                            treeifyBin(tab, hash);
	                        break;
	                    }
	                    if (e.hash == hash &&
	                        ((k = e.key) == key || (key != null && key.equals(k))))
	                        break;
	                    p = e;
	                }
	            }
	            if (e != null) { // existing mapping for key
	                V oldValue = e.value;
	                if (!onlyIfAbsent || oldValue == null)
	                    e.value = value;
	                afterNodeAccess(e);
	                return oldValue;
	            }
	        }
	        ++modCount;
	        if (++size > threshold)
	            resize();
	        afterNodeInsertion(evict);
	        return null;
	    }

由于Student类中并没有重写hashCode()方法,所以在调用hash(Object key)时,比较的是传入对象的地址值,又测试代码中add()方法传入的三个对象的地址值都不相同,故hash值不同,由上一篇博客的分析可知,这三个对象都可以添加进HashSet集合中。但是如果我们希望比较的是Student类中的id是否相同,即实现不能重复添加id相同的Student对象的功能,又该怎么做呢?这时可以通过在自定义的Student类中重写hashCode()方法来实现。代码如下:

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

这时,在调用hash(Object key)时,就会调用Student类中的hashCode()方法,而该hashCode()方法实质上又是在调用id的hashCode方法,而id是String类型的,故实际上调用的是String类中的hashCode()方法,比较内容是否相同,即如果创建的Student对象中所传入的id内容相同,那么hash值相同。下面是对测试代码执行过程的详细分析:

1、第二行代码set.add(new Student(“1”))的执行过程:

1.程序执行 if ((tab = table) == null || (n = tab.length) == 0)语句,此处的table是在HashMap类中定义的成员变量,由于在声明时未显示初始化,HashMap类中的所有构造方法当中也没有对table变量赋值,故该值默认为null。故条件成立,执行if代码块中的n = (tab = resize()).length语句,resize()方法执行完后会返回一个默认初始长度为16的数组,故tab和table都指向该数组。
2.程序执行if ((p = tab[i = (n - 1) & hash]) == null)语句,此处的i值是根据所传key的hash值对tab数组空间的一个映射,也就是(n - 1) & hash为数组下标。此时tab[i]中没有数据,if语句成立,执行tab[i] = newNode(hash, key, value, null),创建一个Node对象(封装id="1"的Student对象)并存到tab[i]中,也就是说此时tab(table)指向的数组中已经存储了一个id="1"的Student对象。
3.程序跳过else语句执行下面代码,返回null。即putVal方法返回null,put方法也返回null,add方法返回true,添加成功。

2、第三行代码set.add(new Student(“1”))的执行过程:

1.程序执行 if ((tab = table) == null || (n = tab.length) == 0)语句,此处的tab(table)指向的数组中已经存储了一个id="1"的Student对象,不为空,故条件不成立,不执行if代码块中的内容。
2.程序执行if ((p = tab[i = (n - 1) & hash]) == null)语句,由之前的分析可知此处的hash值与执行第二行代码时的hash值相同,故i值不变,即tab[i]中存储的是id="1"的Student对象,变量p指向tab[i],故条件不成立,执行else代码块中的内容。
3.程序执行 if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))语句,p.hash == hash语句实质是在比较第一次传入的Student对象的hash值和本次传入的Student对象的hash值,由之前的分析可知为true,但是(k = p.key) == key中,k=p. key即k和p中存储的都是第一次传入的Student对象的地址,而key是本次传入的Student对象的地址,显然不同,故而执行key != null && key.equals(k),key!=null成立执行key.equals(k),由于key是自定义的Student类,并没有重写Object类中的equals方法,所以此时调用的仍然是Object类中的equals方法,比较地址值是否相同,结果为false,而如果要实现本例功能,此处结果必须为true,所以需要在自定义的Student类中重写equals方法,代码如下:

		@Override
		public boolean equals(Object obj) {
			Student student = (Student) obj;
			return this.id.equals(student.id);
		}

重写equals方法之后,此处实际上是调用Student类中的equals方法,而该方法实际是调用id的equals方法,而id是String类型的,故实际上调用的是String类中的equals()方法,比较内容是否相同,显然为true。条件成立,执行e = p。
4.程序执行if (e != null) {…}代码块中的内容,返回一个不为null的值,即putVal方法返回值不为null,put方法返回值也不为null,add方法返回false,添加失败。

3、第四行代码set.add(new Student(“2”))的执行过程:

1.程序执行 if ((tab = table) == null || (n = tab.length) == 0)语句,此处的tab(table)指向的数组中已经存储了一个id="1"的Student对象,不为空,故条件不成立,不执行if代码块中的内容。
2.程序执行if ((p = tab[i = (n - 1) & hash]) == null)语句,由于本次传入的Student的id与数组中存储的Student的id的内容不一样,所以hash值也不同,故此处得出的i值为新值,tab[i]为null,if语句成立,执行tab[i] = newNode(hash, key, value, null),创建一个Node对象(封装id="2"的Student对象)并存到tab[i]中。
3.程序跳过else语句执行下面代码,返回null。即putVal方法返回null,put方法也返回null,add方法返回true,添加成功。

三、细节补充

下面是测试代码:

		HashSet<Object> set = new HashSet<>();//将HashSet中的泛型改为Object类型
		set.add(new Dog("1"));//此处新建一个Dog类
		set.add(new Student("1"));

细节分析:第二行代码执行结束后,集合中会添加一个id="1"的Dog对象,执行第三行代码时,由之前的分析可知,程序会执行到 if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))语句,此时会执行key.equals(k)语句,调用Student类中的equals方法,此时程序会报错,因为Dog类无法强制转换成Student类,那这个问题该如何解决呢?只需在Student类中的equals方法中加入健壮性判断即可,如下代码:

			@Override
			public boolean equals(Object obj) {
				if (!(obj instanceof Student)) {
					throw new ClassCastException("类型转换错误!");
				}
				Student student = (Student) obj;
				return this.id.equals(student.id);
			}

猜你喜欢

转载自blog.csdn.net/IT_Helianthus/article/details/89714761
今日推荐