HashSet

本文详细介绍了HashSet的特点,包括其无序性和唯一性,并通过实例展示了HashSet如何处理自定义对象的去重问题。通过源码分析,解释了HashSet在哈希冲突时如何使用equals和hashCode方法确保唯一性。最后,讨论了HashSet的扩容机制,指出其与HashMap的扩容一致。
摘要由CSDN通过智能技术生成


HashSet

一、HashSet特点

1.无序

使用HashSet不能保持元素插入顺序

	HashSet<String> hashSet = new HashSet<>();
	
	hashSet.add("zhangsan");
    hashSet.add("lisi");
    hashSet.add("wangwu");
    
	for (String s : hashSet) {
    	System.out.println(s);
    }

输出结果如下:
在这里插入图片描述

2.唯一

HashSet中的元素不能重复,可以存储null

	HashSet<String> hashSet = new HashSet<>();
	
	hashSet.add("zhangsan");
    hashSet.add("lisi");
    hashSet.add("wangwu");
    hashSet.add("zhangsan");
    hashSet.add(null);
    hashSet.add(null);
    
    System.out.println("hashSet.size-->" + hashSet.size());
	for (String s : hashSet) {
    	System.out.println(s);
    }

输出结果如下:
在这里插入图片描述

二、hashcode和equal问题

1.学生类

这里使用学生类作为测试时数据,成员属性如下:

	private int id;	//学号
    private String name;	//姓名
    private int age;	//年龄

2.问题

创建五个学生类实例,存入HashSet中

	Student stu1 = new Student(1, "zhangsan", 20);
    Student stu2 = new Student(2, "lisi", 21);
    Student stu3 = new Student(3, "wangwu", 19);
    Student stu4 = new Student(4, "zhaoliu", 20);
    Student stu5 = new Student(1, "zhangsan", 20);

    hashSet.add(stu1);
    hashSet.add(stu2);
    hashSet.add(stu3);
    hashSet.add(stu4);
    hashSet.add(stu5);

打印输出hashSet的size及存储数据,结果如下:
在这里插入图片描述
从上图打印结果发现,hashSet的size=5,stu1和stu2的属性都是相同的,为什么size不是4呢?接下来从源码进行分析。

3.源码分析

	private transient HashMap<E,Object> map;

	public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

进入HashSet源码中发现,HashSet底层是通过HashMap实现的,HashMap的数据存储在HashMap的key中,接着进入HashMap查看put方法是如何对数据进行去重。

	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);	//通过hash方法计算key的hash值
    }

hash(key):

	static final int hash(Object key) {
        int h;
        //key.hashCode()使用的是Object的hashCode(),通常是通过将对象的内部地址转换为整数
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

通过debug功能,查看stu1和stu5的hash值,如下所示:
stu1:
stu1
stu5:
stu5
接着再看到 putVal(hash(key), key, value, false, true) 方法:

	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)
        	//获取table数组,table数组为Node<K, V>数组,用来存储键值对
            n = (tab = resize()).length;	
        if ((p = tab[i = (n - 1) & hash]) == null)
        	/*
        		(n - 1) & hash:数组下标最大值与hash值做与按位与运算,获取一个下标x,
        		若tab[x] == null,即未发生hash冲突,tab数组中没有与新插入的值相同的
        		元素,插入成功。
        	*/
            tab[i] = newNode(hash, key, value, null);
        else {	//发生hash冲突
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))	
                //hash值和key均相同,则判断存在该建
                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) { // 已存在该键,如果是HashMap,则用新值替换旧值
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

通过该方法,可以看出去重时先比较hash值(效率高),如果hash值不同,则认为没有重复,如果hash值相同,则通过equals方法进行比较。

4.解决方法

stu1和stu5的属性值都是相同的,我们认为stu1和stu5是同一个Student,所以stu1和stu5的hash值应该是相同的,其次,我们还需要重写 equals 方法(默认比较的是对象地址),如下:

public class Student {
    private int id;
    private String name;
    private int age;

    //Constructor
    //Getter and Setter
    //toString()

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return id == student.id && age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, age);
    }
}

再次测试,结果如下:
在这里插入图片描述

三、HashSet扩容机制

因为HashSet的底层就是HashMap,所以HashSet扩容机制与HashMap扩容一致------HashMap扩容机制

总结

一、HashSet特点

  1. 1.HashSet底层是一个HashMap
  2. HashSet不确保插入顺序
  3. HashSet不能存在重复数据
    (注:HashSet是线程非安全的)

二、equals和hashCode问题

  1. 使用HashSet存储自定义类时,需要重写equals方法和hashCode方法,具体实现根据实际需求
  2. equals 和 hashCode方法,重写了一种,另外一种也需要重写,保证当hash值相等时,obj.equals(other) == true;
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值