HashSet源码分析

HashSet是Set接口的一个实现类,它是一个容器类,里面的每个元素都是一个对象,且具有无序性和唯一性,
无序性:容器中的元素是无序的,不能通过类似于数组索引的方式获得。
唯一性:容器中不存在两个或多个元素具有完全一致的属性。
而equals()和hashCode()方法是保证这两个性质的前提,接下来通过分析源码来进行探究如何保证HashSet的这两个性质。

无序性

首先,从HashSet.java源文件中可以知道,HashSet底层实际上是通过HashMap存储数据,默认情况下这个哈希表的容量为16,负载因子为0.75,
在这里插入图片描述
HashSet中通过将每个元素作为key值,以一个公共的Object对象作为value值,构成一个键值对存储在HashMap中,也就是说,此时HashMap中只有key值才有真正意义,
在这里插入图片描述
在HashMap中每一个键值对通过一个Node<K,V>对象进行存储,
在这里插入图片描述
Node对象中的key、value属性分别表示键值对的key和value,hash属性表示key值通过一个hash(Object)方法得到的hash值,为了将多个键值对的Node对象进行有效存储以及管理,HashMap中将每个Node对象的hash属性进行一个变换,然后将变换后的值作为数组的索引存储在一个Node数组中(JDK8中这个数组的默认长度也就是刚刚所说的哈希表容量16,并且采用单例设计模式中懒汉式的思想,即首次使用时才进行初始化),
在这里插入图片描述
在HashMap中将Node对象的hash属性与数组长度减一进行&位运算((table.length - 1) & hash)作为数组的索引,由于通过这种方式会存在多个Node对象映射到同一个索引上的情况,因此通过Node的next属性将这些具有相同索引值的Node对象串联起来。

因此在HashMap中每个键值对以下图类似的结构进行存储(下图为一个简化的例子,实际上在JDK8中当一个链表的长度超过8并且数组长度大于64时,会将此链表转化为一棵红黑树进行存储),
在这里插入图片描述
虽然使用了数组结构,但是并不能通过一个索引值得到具体的一个键值对,而这个数组的索引也是为了便于通过key值快速查找对应的value值,因此这个结构中键值对之间逻辑上也不存在先后顺序,因此也就保证了HashSet中元素的无序性。

这时可能会问,既然HashSet中元素具有无序性,那么使用System.out.println(HashSet)或迭代器输出一个HashSet时,又是以怎样的顺序输出的呢?
实际上,在输出时是按照HashMap中数组的索引顺序,对于相同索引的对象按照单链表顺序依次输出(后文有代码验证)。

唯一性

为了保证HashSet中元素的唯一性,那么每加入一个元素,就需要与已有的元素进行比较,在数据量较大的情况下,明显这种方法是低效的,为了解决这个问题,就要涉及到上文所说的Node对象的hash属性了。

上文说到HashSet将每个元素作为key值保存在HashMap中,因此只要保证HashMap中key值的唯一性(而根据HashMap的实际意义也必须要求key值具有唯一性),也就保证了HashSet中元素的唯一性。那么具体又是如何实现的呢?下面通过两种情况进行探讨。
情况一:在HashMap存在一个key值与待插入的key属于同一个类的同一个对象,或者属于同一个类的不同对象,但是它们之间具有完全一致的属性值(存在equals(Object)相等);
情况二:其他(不存在equals(Object)相等)。

情况一:存在equals(Object)相等

由于Node对象的hash属性通过HashMap中的hash(Object)方法得到,而这个方法默认通过使用对象的hashCode()然后进行位运算得到,
在这里插入图片描述
下图是Object类中关于hashCode()方法的要求,
在这里插入图片描述
该方法的描述中明确要求,对于自定义的类重写该方法,如果两个对象的属性值之间完全相同(基于对象的equals(Object)方法),那么该方法返回相同的结果,否则返回不同的结果。
因此在情况一的条件下,待插入的key值就与HashMap中某一个key具有相同的hash值,因此也就会映射到数组的相同索引上,这时再通过key对象的equals(Object)方法与数组该索引上的链表中的所有key进行比较,此时比较一旦发现相同的key,表示插入重复的元素,就会插入失败。
在这里插入图片描述

情况二:不存在equals(Object)相等

这种情况下,按照hashCode()的约定,HashMap中找不到一个已有的key与待插入的key具有相同的hash值,此时只需通过将待插入key的hash值计算得到索引,然后将该键值对插入数组的该索引位置即可,即使再经过将hash值转为数组索引后,可能数组中该索引对应的值非null,再使用key对象的equals(Object)方法该索引上的所有key依次进行比较即可,如果存在相等的key(当然根据我这里对情况二的划分,这个比较过程中不会存在相等的key),则插入失败,否则在链表的尾部插入新的键值对对象。
在这里插入图片描述

上面两种情况中,首先计算待插入键值对的key的hash值,通过hash值转换得到数组的索引,将比较的范围缩小到具有相同的索引范围中,再使用key对象的equals(Object)与这个索引上的链表中的每个key进行比较,遇到相同的插入失败,否则在尾部插入。整个过程与HashSet中每个元素比较判断是否存在重复相比,可以极大提高时间效率。但是这个过程的关键是按照约定,重写hashCode()和equals()方法。

最后通过模仿HashMap中索引的计算方式,通过一段代码验证HashSet中元素的输出顺序是以hash值的索引顺序进行输出。

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

public class Test {
    static class MyClass{
        String str;
        int num;

        public MyClass(String str, int num){
            this.str = str;
            this.num = num;
        }

        @Override
        public String toString() {
            return "MyClass{" +
                    "str='" + str + '\'' +
                    ", num=" + num +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            MyClass myClass = (MyClass) o;

            if (num != myClass.num) return false;
            return str != null ? str.equals(myClass.str) : myClass.str == null;
        }

        @Override
        public int hashCode() {
            int result = str != null ? str.hashCode() : 0;
            result = 31 * result + num;
            return result;
        }
    }

    public static void main(String[] args) {
        Set set = new HashSet();
        set.add(123);
        set.add("abc");
        set.add(new MyClass("hello", 18));
        set.add(567);
        set.add(true);
        set.add(90);
        set.add(new MyClass("hello", 18));

        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            Object object = iterator.next();
            int hashCode;
            int hash = (hashCode = object.hashCode()) ^ (hashCode >>> 16);
            int index = (16 - 1) & hash;//HashMap中数组的默认长度是16
            System.out.println(index + "\t" + object);
        }
    }
}

输出结果如下:

3	abc
7	567
10	MyClass{str='hello', num=18}
10	90
11	123
15	true
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值