Java基础 - Set (一)

本文详细介绍了Java中的Set接口及其实现类HashSet的工作原理,包括其无序性、不允许重复元素的特性。通过模拟HashMap的实现,展示了HashSet在添加元素时的hash计算和碰撞处理过程,特别提到了扩容和链表树化的条件。文章还探讨了HashSet添加元素的具体步骤,包括构造HashMap、计算hash值、插入节点等,并给出了源码分析。
摘要由CSDN通过智能技术生成

Set接口基本介绍

  • 无序(添加和取出的顺序不一样),没有索引
  • 不允许重复元素,最多包含一个null
  • 实现类有HashSet,TreeSet,LinkedHashSet等。

Set常用方法

和List一样,都是Collection的子接口,因此常用方法和Collection一样

可以使用迭代器和增强for来遍历,不能使用索引遍历。

取出的顺序虽然和添加的不一样,但是顺序是固定的。(不论迭代多少次)

HashSet

  • HashSet实现了Set接口
  • HashSet底层数据结构是HashMap
/**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }
  • 可以存放null值,但是只能有一个null
  • HashSet不保证元素是有序的,取决于hash后,再确定索引的结果。(即,不保证存放元素的顺序和取出顺序一致)
  • 不能有重复的元素/对象
public static void main(String[] args) {

        Set set = new HashSet();
        set.add("1");
        set.add("1");
        set.add(new Employee("张三", "23"));
        set.add(new Employee("张三", "23"));

        System.out.println(set);
        
        set = new HashSet();
        set.add(new String("2"));
        set.add(new String("2"));

        System.out.println(set);
        
        
}

看上面的代码,add("1")执行了两次,但是只有一个字符串被add进了对象

下面的两个对象,虽然属性值都相同,但是却不是一个对象,所以两个都被add进去了。

而最下面的两个相同字符串,却只能被保存进一个,是因为什么呢?这就要等到我了解到HashSet的底层去重原理时再来补充吧。今天就这样,这个端午三天假期我过得很happy,做了很多平时没时间去做的事情,很充实。

端午安康~

模拟一个HashMap

继续昨天的学习:

先模拟一个HashMap,真正的HashMap底层是(数组+链表+红黑树)

    @SuppressWarnings("all")
    public static void main(String[] args) {

        Node node = new Node("红豆", null);
        Node node2 = new Node("超群", null);
        Node node3 = new Node("婉嫔", null);
        Node node4 = new Node("阿悦", null);
        
        Node[] table = new Node[16];
        table[5] = node;
        System.out.println("table" + table);
        
        node.next = node2;
        System.out.println("table" + table);
        
        table[6] = node3;
        node2.next = node4;
        System.out.println("table" + table);

    }

我们可以打个断点来看一下table里的情况。

现在可以看到数组中一共有16个元素,索引为5的元素里的next下还有元素,这样就形成一个单链表。而索引为6的元素,next为null。

差不多是这个样子,真正的HashSet底层的扩容机制是下面这样的:(先说结论,之后分析源码)

  • HashSet底层是HashMap
  • 添加一个元素时,先得到hash值 - 会转成索引值
  • 找到存储数据表table,看这个索引位置是否已经存放的有元素
  • 如果没有,直接加入
  • 如果有,调用equals比较(这个方法可以咱们自己重写来控制比如String类就重写了),如果相同,就放弃添加;如果不相同,则添加到最后
  • 在Java8中,如果一条链表的元素个数大于或等于TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)

好的各位,刚看这里大家可能还是一头雾水,接下来就去进行源码的分析:

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

韩顺平的课分析HashSet底层源码时有四个视频,看来这里真的很复杂

首先创建一个类

public class HashSetDemo2_ {

    @SuppressWarnings({"all"})
    public static void main(String[] args) {

        HashSet set = new HashSet();
        set.add("java");
        set.add("php");
        set.add("java");

        System.out.println("set=" + set);
        
    }
}

运行这段代码后,set对象里会有[java,php],那么HashSet是如何把java字符串添加进去的呢?

首先执行这个构造器,创建了一个HashMap对象

接着执行HashMap的put方法

private static final Object PRESENT = new Object();

PRESENT我理解为是一个占位对象,每次执行add方法时都会传入一个新的

之后执行HashMap的putVal方法

稍等一下,这里会先执行hash方法,传入我们要添加的对象,也就是key,来算出对应的hash值

如果key为null,则直接返回0,这也解释了为什么HashSet中如果有null,则查询时null的索引为0。

如果key不为null,则通过后面的算法来计算出key对应的hash值,右移16位是为了防止hash碰撞

这里如果面试官问了这块得到的hash值是不是就是hashCode,我们要果断的回答不是,因为这里做了处理。

接下来执行最复杂最重要的方法putVal

由于 tab为null 或者 大小=0,所以会进入到下一步,也就是第一次扩容,到16个空间,去执行resize方法,

首先由于table为null,则oldTab也为null

这样oldCap为0,oldThr为0,newCap,newThr都为0

由于oldCap为0,不大于0,所以进入到这一步,这一步会给newCap赋值为一个默认值DEFAULT_INITIAL_CAPACITY为16,newThr为临界值,由(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)算出,DEFAULT_LOAD_FACTOR为0.75,这个是负载因子,最后得到的临界值为12。

此时threshold临界值为12

用newCap(16)创建了一个新的Node数组,并赋给了table,这样table就是一个size为16的数组,最终返回table

接下来得到n为16

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);


/**
  * 这段代码的作用是
  * (1) i = (n - 1) & hash => 根据key得到的hash 去计算该key应该存放到table表的
  *  哪个索引位置
  * 并把这个位置的对象,赋给 p
  * (2) 接着去判断p是否为null
  * (2.1) 如果p为null,表示还没有存放元素,此时创建一个Node(key ="java",value = PRESENT)
  * (2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null);
**/

接着向下执行,可以看到tab中对应的索引已经存入了这个新的Node结点

有hash值,key就是我们传入的对象,value为PRESENT,next为null。

执行++size,如果这个值 > 临界值则进行下一次扩容。(这个下一次说)

这个afterNodeInsertion(evict)方法其实没有实现,是留给比如LinkedHashMap类去实现的,可以进行比如对于元素进行排序等操作。

最后返回null,就代表add成功了,否则返回oldValue,代表该元素之前已经存在了。

走回到这里,返回true代表添加成功。

此时,java字符串已被成功add进set对象中。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值