Set源码解读

一、Set

1. Set 接口和 Colleection 接口一样
2. 元素无序,不可重复, 可以包含但只能包含一个null,
   2.1 虽然添加和取出的顺序不一致,但是取出的顺序是恒定不变的
   
3. 遍历: 不能用普通for循环,因为无序
    1. Iterator
    2. 增强for

二、HashSet

1. 重复元素

  • 是通过元素的hashcode和equals方法共同决定的
package com.erick.feature.collection.d02;

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

public class Demo02 {
    public static void main(String[] args) {
        Set set = new HashSet();
        System.out.println(set.add(new Dog("tom")));//ok
        System.out.println(set.add(new Dog("tom")));//ok
        System.out.println(set.add("tom"));//ok
        System.out.println(set.add("tom"));//no
        /*因为String重写了equals和hashcode方法*/
        System.out.println(set.add(new String("jack")));//ok
        System.out.println(set.add(new String("jack")));//no
    }
}

class Dog {
    public Dog(String name) {
        this.name = name;
    }

    private String name;
}

2. 模拟数组+双向链表

  • HashSet底层是HashMap
  • HashMap底层是数组+链表或红黑树,数组类型是链表或红黑树
  • 达到一定的界限,就会将链表树化成红黑树
// 底层结构是HashMap
public HashSet() {
    map = new HashMap<>();
}

在这里插入图片描述

package com.erick.feature.collection.d02;

import lombok.Setter;

public class Demo03 {
    public static void main(String[] args) {
        // 数组长度为16个
        Node[] table = new Node[16];

        Node first = new Node(null, "erick");
        Node second = new Node(null, "jack");
        Node third = new Node(null, "tom");
        
        // 挂载
        first.setNext(second);
        second.setNext(third);

        table[2] = first;

        table[3] = new Node(null, "cat");
    }
}

class Node {
    public Node(Node next, String item) {
        this.next = next;
        this.item = item;
    }

    @Setter
    private Node next;
    private String item;
}

3. HashSet添加元素

- HashSet底层是 HashMap
1. 添加一个元素时,先通过元素的hashCode得到元素hash值,转换成索引值(索引值和hash值是不同的)
   索引值会决定该元素存放在数组中的哪个位置
2. 找到存储数据的table数组,看该索引位置是否已经存放了元素
   2.1 如果没有,则直接加入
   2.2 如果有,则用添加元素对象的equals方法和已经存在的,链表的前面的元素依次比较
         相同,则放弃添加
         不同,则该新元素就会添加在table表该索引除后面(链表)

# 1. 添加方法
添加的所有的元素,如果hash值相同,但是equals不同,就会放在数组的同一个索引位置
通过链表的方式来添加

获取元素的时候,也会从链表中去取数据

# 2. Java8 扩容机制

- 一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是8),并且
  table的大小>= MIN_TREEIFY_CAPACITY(默认是64),链表为了增加性能,
  就会树化成红黑树
- 如果链表个数达到了,但是table长度没达到,table就会先进行扩容

- table数组长度默认是16,通过0.75的加载因子,当数组长度到达16*0.75
  的时候,就会进行2倍扩容,后续继续按照24*0.75进行扩容

4. HashSet源码解读

4.1 add初步

1. 底层是hashmap
      public HashSet() {
            map = new HashMap<>();
       }
   
2. dummy值填充key,所有的value都是这个dummy值
      private static final Object PRESENT = new Object();

      public boolean add(E e) {
      // 返回结果为空,代表添加成功
        return map.put(e, PRESENT)==null;
      }

 3. 加入到map中
       3.1 添加元素
       public V put(K key, V value) {
          return putVal(hash(key), key, value, false, true);
       }

       3.2 hash的时候,如果key为null,则hash为0
           - 否则就会用对象的hashcode加上无符号右移16位,避免hash碰撞几率
           - hash并不等于hashcode,hash决定元素放在数组的哪个索引位子
           - null元素就会放在第一位

       static final int hash(Object key) {
          int h;
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
       }

4.2 添加第一个元素

  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
             boolean evict) {
  /*1. 添加辅助变量*/
  Node<K,V>[] tab; Node<K,V> p; int n, i;

  /*2. table是hashmap的一个节点 transient Node<K,V>[] table;
       2.1 一上来table就是空,所以会先进行扩容
       2.2 resize(): 数组长度初始化为16,负载因子为0.75,
           如果数组长度达到16*0.75=12个,那么就会扩容2倍
           扩容原因:避免不同的hash值打到同一个索引位子上*/
  if ((tab = table) == null || (n = tab.length) == 0)
      n = (tab = resize()).length;

   /*3. hash值:3254254 (0x31A7EE)
   3.1 通过当前hash求到tab对应的索引位置,如果索引位置为空,则新创建一个节点到后面
   3.2 hash: 对应的key的hash,key:具体存放的数据, 
      value:Dummy的Object对象,null代表下一个节点*/

  if ((p = tab[i = (n - 1) & hash]) == null)
      tab[i] = newNode(hash, key, value, null);
  else {
     // 暂时忽略
     // 如果添加失败,则返回一个oldvalue,下面的都不会执行
  }

  // 修改次数加1,防止并发修噶
  ++modCount;
  /* 4. ++size: 
      4.1 每次新加一个元素,不管加到之前tab相同索引的后面,还是新添加到tab其他索引位置都会加一
      4.2 如果超过当前容量*负载因子,就会进行扩容*/
  if (++size > threshold)
      resize();

  // 空方法,来让HashMap的子类实现,比如有序
  // 模版方法模式
  afterNodeInsertion(evict);
  // 添加成功后,返回null
  return null;
}

4.2 添加第二个能添加进去的元素

  • 如果hascode的索引值和之前元素重复,但是equals不同,就会在该索引的后续链表继续加
  • 如果hascode的索引值和之前元素不重复,就会在新索引的位置添加

4.3 添加hashcode和equals相同的元素

    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;
     // 如果满足下面的两个条件,就不会添加
     // 1. 当前索引位子的hash和新添加元素的hash相同
     // 2. 当前索引位子的key和新添加元素的key相同,或者key通过equals相同
       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 {

        // table对应的索引位置,已经是一个链表,使用for循环比较
        * 1. 依次和该链表的每一个元素比较,都不相同,则加入到链表的最后
        *    如果相同了,就break
        *
        * 2. 在链表的最后,如果节点个数超过了8个,就会对该链表进行红黑树树化
        *   2.1 树化的条件:该链表个数达到8个,并且table表超过64个,
        *   2.2 如果table表没有超过64,则先对table表进行扩容
        *   2.3 table表一般默认是16个,由HashMap无参构造器来完成
        *   2.4 扩容的时候,是按照16*加载因子来扩容
           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;
}

5. HashSet扩容机制

1. HashSet底层是  HashMap
2. 第一次添加时候,table数组扩容到16,临界值是16*0.75(加载因子)
3. 后续如果达到临界值(该table的所有的节点的元素个数总和),就会扩容

# 扩容场景一
- 如果table数组使用到了12,就会扩容到16*2=32,新的临界值就是32*0.75

# 扩容场景二
- java8中,如果一条链表的元素个数达到8个,并且table大小达到64个
   就会对该链表进行树化(红黑树),
  《否则仍然采取数组扩容机制》 16*2

# 1.刚开始数组长度为16, 
# 2. 在数组某个索引上,先添加7个元素,到达第八个时候,数组扩容为32,
# 3. 添加第9个的时候,数组扩容为64,
# 4. 添加第10个的时候,就会将该链表 进行树化

# 扩容场景三
- table数组的所有节点的元素数量总和达到12, 就会扩容

6. HashSet最佳实践

  • 对象如果要在HashSet或者HashMap中存储时,一定要重写hashcode和equals方法

7. Hash碰撞问题

  • 一个对象,如果不同,最好落在不同的table的数组里面
  • 所以尽可能的不要让多个对象出现在一个链表
  • 避免过热或者过冷的问题

7.1 对象自定义hashcode的时候

7.2 HashSet在算hash值的时候

static final int hash(Object key) {
   int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

三、LinkedHashSet

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值