HashSet详解

本文详细解析了HashSet的实现原理,包括其基于HashMap的存储结构、元素添加的hash()和equals()方法、线程不安全特性以及扩容和链表转红黑树的机制。还通过实例展示了HashSet的添加、删除操作,并探讨了元素存储顺序和重复问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashSet详解

HashSet的全面说明

  • HashSet 实现了Set接口

  • HashSet 实际上是HashMap

    • 执行 Set set = new HashSet();

public HashSet() {
    map = new HashMap<>();
}
  • 可以存放null值,但是只能有一个null

  • HashSet不能保证元素的存取顺序一致

  • 不能有重复的元素

  • HashSet线程不安全

  • 没有带索引的方法,所以不能通过普通for循环进行遍历

HashSet的案例说明

案例1:

  • 在执行add方法后,会返回一个boolean值

  • 如果添加成功,返回true,否则返回false

  • 可以通过 remove 指定删除哪个对象

public class Demo07 {
    public static void main(String[] args) {
        Set set = new HashSet();
​
        System.out.println(set.add("join"));
        System.out.println(set.add("lucy"));
        System.out.println(set.add("join"));
        System.out.println(set.add("jack"));
        System.out.println(set.add("Rose"));
​
        set.remove("join");
        
        System.out.println(set);
    }
}

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA56iL5bqP54y_6L-b5YyW54mI,size_14,color_FFFFFF,t_70,g_se,x_16


案例2:

public class Demo08 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add("lucy");
        hashSet.add("lucy");
        hashSet.add(new Dog("tom"));
        hashSet.add(new Dog("tom"));
        
        System.out.println(hashSet);
​
    }
}
class Dog {
    private String name;
​
    public Dog(String name) {
        this.name = name;
    }
​
    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA56iL5bqP54y_6L-b5YyW54mI,size_16,color_FFFFFF,t_70,g_se,x_16

HashSet底层机制说明

  • HashSet 底层是HashMap,HashMap底层是(数组+链表+红黑树)

模拟简单的数组+链表结构

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA56iL5bqP54y_6L-b5YyW54mI,size_12,color_FFFFFF,t_70,g_se,x_16

public class Demo01 {
    public static void main(String[] args) {
        Node[] table = new Node[16];
​
        Node john = new Node("john", null);
        table[2] = john;
​
        Node jack = new Node("jack", null);
        john.next = jack;
​
        Node rose = new Node("Rose", null);
        jack.next = rose;
​
        Node lucy = new Node("lucy", null);
        table[3] = lucy;
        System.out.println("table="+table);
    }
}
class Node {
    Object item; //存放数据
    Node next; //指向下一个节点
​
    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }
​
}

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA56iL5bqP54y_6L-b5YyW54mI,size_15,color_FFFFFF,t_70,g_se,x_16

分析HashSet的添加元素底层是如何实现(hash()+equals())

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA56iL5bqP54y_6L-b5YyW54mI,size_20,color_FFFFFF,t_70,g_se,x_16

  1. HashSet 的底层是 HashMap

  2. 添加一个元素时,先得到hash值,会转成 -> 索引值

  3. 找到存储数据表table,看这个索引位置是否已经存放了元素

  4. 如果没有存放,则直接添加

  5. 如果存放了,调用 equals() 比较,如果相同,就放弃添加,如果不相同,则添加到最后

  6. 在java8中,如果一条 链表 的元素个数 大于 TREEIFY_THRESHOLD(默认是8)(链表准备添加第九个的时候) ,并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树)



public class Demo02 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add("java");
        hashSet.add("php");
        hashSet.add("java");
        System.out.println("hashSet="+hashSet);
    }
}

  • 1.执行 HashSet hashSet = new HashSet();

public HashSet() {
    map = new HashMap<>();
}
  • 2.执行 hashSet.add("java");

public boolean add(E e) { //e = "java"
    //PRESENT -> private static final Object PRESENT = new Object();
    //PRESENT的目的是为了占位
    return map.put(e, PRESENT)==null;
}
  • 3.执行 return map.put(e, PRESENT)==null;

public V put(K key, V value) { //key = "java" value = PRESENT 共享
    return putVal(hash(key), key, value, false, true);
}
  • 4.先执行 hash(key)

    • 得到对应的hash值

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 5.再执行 return putVal(hash(key), key, value, false, true);

    • transient Node<K,V>[] table;Node<K,V>[] tab;

    • if ((tab = table) == null || (n = tab.length) == 0)

    • tabletab是引用赋值,所以两者的地址相同

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //定义了辅助变量
    Node<K,V>[] tab; Node<K,V> p; int n, i; 
    //table 就是HashMap的一个数组,类型是Node[]
    //transient Node<K,V>[] table;
    //if语句表示如果当前table是null或者tab的大小为0,则执行下面的语句
    if ((tab = table) == null || (n = tab.length) == 0)
    //执行 resize(),创建了大小为16的table数组
        n = (tab = resize()).length;
    //(1)根据key得到的hash值去计算该key应该存放到tab表的哪个索引位置
    //并且把这个位置的对象,赋给p
    //(2)判断p是否为null
    //(2.1)如果p为null,表示还没有存放元素,就创建一个Node(key="java",value="PRESENT")
    //(2.2)就放在该位置 tab[i] = newNode(hash, key, value, null);
    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;
}

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA56iL5bqP54y_6L-b5YyW54mI,size_16,color_FFFFFF,t_70,g_se,x_16

  • 6.执行 hashSet.add("php");

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

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  • 8.先执行 hash(key)

    • 得到对应的hash值

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 9.再执行 return 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)
        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;
}

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA56iL5bqP54y_6L-b5YyW54mI,size_20,color_FFFFFF,t_70,g_se,x_16

  • 10.执行 hashSet.add("java");

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

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  • 12.先执行 hash(key)

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 13.再执行 return 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)
        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;
        //如果当前索引位置对应的链表的第一个元素的hash值和准备添加的key的hash值一样
        //并且满足下面两个条件之一:
        //(1)准备加入的key和p指向的Node节点的key是同一个对象
        //(2)p指向的Node节点的key的equals()方法和准备加入的key比较后相同
        if (p.hash == hash && 
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //再判断p是不是一颗红黑树
        //如果是一颗红黑树,就调用putTreeVal,来进行添加
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else { //如果table对应的索引位置,已经是一个链表,就使用for循环比较
               //(1)依次和该链表的每一个元素比较后(从链表的第二个元素开始比较),都不相同,则 
               //     加入到该链表的最后
               //   注意:在把元素添加到链表后,立即判断该链表是否已经到达8个节点,
               //   如果到达了8个节点,就调用 treeifyBin(tab, hash); 如果满足
               //   if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //64
               //       resize();,
               //   则先对table扩容;如果该if语句不满足,则对当前这个链表进行树化(转成红黑树)
               //(2)依次和该链表的每一个元素的比较过程中,如果有相同的情况,则直接break
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //TREEIFY_THRESHOLD = 8;
                    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;
}

分析HashSet的扩容和转成红黑树的机制

  1. HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12

  2. 如果table数组使用到了临界值12,就会扩容到 16 * 2 = 32,新的临界值就是 32 * 0.75 = 24

  3. 在java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(转成红黑树),否则仍然采用数组扩容机制。




public class Demo04 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
​
        for (int i = 1; i <= 12; i++) {
            hashSet.add(new A(i));
        }
        System.out.println("hashSet="+hashSet);
    }
}
class A {
    private int n;
​
    public A(int n) {
        this.n = n;
    }
​
    @Override
    public int hashCode() {
        return 200;
    }
}



  • 执行 hashSet.add(new A(1)); 时,数组的大小扩容到16

  • 执行 hashSet.add(new A(9)); 时,数组的大小扩容到32

  • 执行 hashSet.add(new A(10)); 时,数组的大小扩容到64

  • 执行 hashSet.add(new A(11)); 时,将该链表进行树化(转成红黑树)

 




public class Demo04 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
​
        for (int i = 1; i <= 7; i++) {
            hashSet.add(new A(i));
        }
        for (int i = 1; i <= 6; i++) {
            hashSet.add(new B(i));
        }
​
    }
}
class A {
    private int n;
​
    public A(int n) {
        this.n = n;
    }
    @Override
    public int hashCode() {
        return 200;
    }
}
class B {
    private int n;
​
    public B(int n) {
        this.n = n;
    }
    @Override
    public int hashCode() {
        return 100;
    }
}
  • 无论是在table数组上还是在链表上加入元素,size都会加1,size(元素的个数)的大小大于临界值后,就会对数组进行扩容。

if (++size > threshold)
    resize();
  • 执行到 hashSet.add(new A(1)); 时,数组的大小扩容到16

  • 执行到 hashSet.add(new B(6)); 时,数组的大小扩容到32

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值