Java进阶之集合框架(Set)

news2024/9/21 8:26:21

基本内容】

     二、Set接口(接上一章)

     Set是Java集合框架中不允许有重复元素的无序集合,其典型的实现类是HashSet,它完全是遵循Set接口特性规范实现的,无序且不允许元素重复;而Set接口下的实现类还有LinkedHashSet和TreeSort,前者实现了有序的Se,后者实现了对元素的排序。Set接口及其实现类的完整继承关系如下图:

图片

     从上述继承图可以看出,HashSet只是继承了抽象类AbastractSet和实现了Set接口,是比较典型的Set接口的实现,而LinkedHashSet和TreeSet则都实现了SequencedSet接口,以此保证元素的有序。下面对三个实现类作具体介绍。

       HashSet

       HashSet的特性基本上等同Set接口的特性:无序且不能元素重复。从HashSet的命名来看,其底层存储实现和哈希表有关,那HashSet实现类是如何实现元素不能重复的呢?我们不妨看看HashSet的源代码:


public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    @java.io.Serial
    static final long serialVersionUID = -5024744406713321676L;

    transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    static final Object PRESENT = new Object();

    /**
     * Constructs a new, empty set; the backing {@code HashMap} instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

    /**
     * Constructs a new set containing the elements in the specified
     * collection.  The {@code HashMap} is created with default load factor
     * (0.75) and an initial capacity sufficient to contain the elements in
     * the specified collection.
     *
     * @param c the collection whose elements are to be placed into this set
     * @throws NullPointerException if the specified collection is null
     */
    public HashSet(Collection<? extends E> c) {
        map = HashMap.newHashMap(Math.max(c.size(), 12));
        addAll(c);
    }
    ......
    
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
  }

     从上述HashSet的源代码定义可以看出,HashSet底层是采用Java集合框架双列集合HashMap来存储数据元素的,而我们知道HashMap底层是主要采用哈希表实现,那HashSet是如何通过HashMap实现元素不能重复的呢?我们继续看源代码:

   public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
    
    ......
    
    public boolean contains(Object o) {
        return map.containsKey(o);
    }

    /**
     * Adds the specified element to this set if it is not already present.
     * More formally, adds the specified element {@code e} to this set if
     * this set contains no element {@code e2} such that
     * {@code Objects.equals(e, e2)}.
     * If this set already contains the element, the call leaves the set
     * unchanged and returns {@code false}.
     *
     * @param e element to be added to this set
     * @return {@code true} if this set did not already contain the specified
     * element
     */
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

    /**
     * Removes the specified element from this set if it is present.
     * More formally, removes an element {@code e} such that
     * {@code Objects.equals(o, e)},
     * if this set contains such an element.  Returns {@code true} if
     * this set contained the element (or equivalently, if this set
     * changed as a result of the call).  (This set will not contain the
     * element once the call returns.)
     *
     * @param o object to be removed from this set, if present
     * @return {@code true} if the set contained the specified element
     */
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

     从上述HashSet增删元素的方法实现中,我们可以看到HashSet是把元素作为内部HashMap的键,把一个不可变的常量对象PRESENT作为内部HashMap的值;我们都知道HashMap的键是哈希值是唯一的,如果HashSet添加的新元素和HashMap的某个键重复,HashMap的键自然会被覆盖,因而不会出现重复;而HashSet获取元素的时候,也是获取内部HashMap的键的列表。至于HashMap的键如何保持唯一性,不是还会有哈希冲突吗?相关知识请看后续Java集合框架相关文章。

      总之,HashSet通过内部HashMap的键实现了元素的唯一性和不重复,同时因为底层采用哈希表实现,通过哈希键值能快速定位元素,因而具有高效的随机访问和快速查找能力。

       LinkedHashSet

      LinkedHashSet是HashSet的子类,所以自然具备HashSet快速查找元素和不允许重复元素的特性,但是为了保证元素的有序性,LinkedHashSet通过内置了一个双向链表来维护所有元素,从而可以利用迭代器快速遍历元素和保证元素存取的有序性。那LinkedHashSet内部是如何实现这样一个双向链表的呢?其实双向链表的实现不在LinkedHashSet的源代码中,双向链表的功能是通过其内部的LinkedHashMap来实现的,我们看LinkedHashSet的源代码:


public class LinkedHashSet<E>
    extends HashSet<E>
    implements SequencedSet<E>, Cloneable, java.io.Serializable 
 {
    ......
     /**
     * Constructs a new, empty linked hash set with the specified initial
     * capacity and load factor.
     *
     * @apiNote
     * To create a {@code LinkedHashSet} with an initial capacity that accommodates
     * an expected number of elements, use {@link #newLinkedHashSet(int) newLinkedHashSet}.
     *
     * @param      initialCapacity the initial capacity of the linked hash set
     * @param      loadFactor      the load factor of the linked hash set
     * @throws     IllegalArgumentException  if the initial capacity is less
     *               than zero, or if the load factor is nonpositive
     */
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

    /**
     * Constructs a new, empty linked hash set with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @apiNote
     * To create a {@code LinkedHashSet} with an initial capacity that accommodates
     * an expected number of elements, use {@link #newLinkedHashSet(int) newLinkedHashSet}.
     *
     * @param   initialCapacity   the initial capacity of the LinkedHashSet
     * @throws  IllegalArgumentException if the initial capacity is less
     *              than zero
     */
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }

    /**
     * Constructs a new, empty linked hash set with the default initial
     * capacity (16) and load factor (0.75).
     */
    public LinkedHashSet() {
        super(16, .75f, true);
    }
    
    ......
    
 }

     从上述代码中可以看出,LinkedHashSet的构造函数都调用了基类的构造函数,这个基类正是HashSet,我们再回头看看HashSet的构造函数代码,其实现如下:

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
 }

      HashSet的构造函数之一正是新创建了一个LinkedHashMap,正是这个LinkedHashMap实现了双向链表的功能从而有效地支持了LinkedHashSet元素存取的有效性,同时也保留了基于哈希键值快速定位元素的特性以及唯一性。从后续的代码也可以看出,LinkedHashSet的其他操作都是通过这个内部的LinkedHashMap来进行的。代码如下:


    @SuppressWarnings("unchecked")
    LinkedHashMap<E, Object> map() {
        return (LinkedHashMap<E, Object>) map;
    }

    /**
     * {@inheritDoc}
     * <p>
     * If this set already contains the element, it is relocated if necessary so that it is
     * first in encounter order.
     *
     * @since 21
     */
    public void addFirst(E e) {
        map().putFirst(e, PRESENT);
    }

    /**
     * {@inheritDoc}
     * <p>
     * If this set already contains the element, it is relocated if necessary so that it is
     * last in encounter order.
     *
     * @since 21
     */
    public void addLast(E e) {
        map().putLast(e, PRESENT);
    }

    /**
     * {@inheritDoc}
     *
     * @throws NoSuchElementException {@inheritDoc}
     * @since 21
     */
    public E getFirst() {
        return map().sequencedKeySet().getFirst();
    }

    /**
     * {@inheritDoc}
     *
     * @throws NoSuchElementException {@inheritDoc}
     * @since 21
     */
    public E getLast() {
        return map().sequencedKeySet().getLast();
    }

    /**
     * {@inheritDoc}
     *
     * @throws NoSuchElementException {@inheritDoc}
     * @since 21
     */
    public E removeFirst() {
        return map().sequencedKeySet().removeFirst();
    }

    /**
     * {@inheritDoc}
     *
     * @throws NoSuchElementException {@inheritDoc}
     * @since 21
     */
    public E removeLast() {
        return map().sequencedKeySet().removeLast();
    }

    /**
     * {@inheritDoc}
     * <p>
     * Modifications to the reversed view are permitted and will be propagated to this set.
     * In addition, modifications to this set will be visible in the reversed view.
     *
     * @return {@inheritDoc}
     * @since 21
     */
    public SequencedSet<E> reversed() {
        class ReverseLinkedHashSetView extends AbstractSet<E> implements SequencedSet<E> {
            public int size()                  { return LinkedHashSet.this.size(); }
            public Iterator<E> iterator()      { return map().sequencedKeySet().reversed().iterator(); }
            public boolean add(E e)            { return LinkedHashSet.this.add(e); }
            public void addFirst(E e)          { LinkedHashSet.this.addLast(e); }
            public void addLast(E e)           { LinkedHashSet.this.addFirst(e); }
            public E getFirst()                { return LinkedHashSet.this.getLast(); }
            public E getLast()                 { return LinkedHashSet.this.getFirst(); }
            public E removeFirst()             { return LinkedHashSet.this.removeLast(); }
            public E removeLast()              { return LinkedHashSet.this.removeFirst(); }
            public SequencedSet<E> reversed()  { return LinkedHashSet.this; }
            public Object[] toArray() { return map().keysToArray(new Object[map.size()], true); }
            public <T> T[] toArray(T[] a) { return map().keysToArray(map.prepareArray(a), true); }
        }

        return new ReverseLinkedHashSetView();
    }

     包括实现SequencedSet接口的反向操作,间接都是通过内部的LinkedHashMap来最终完成的。至于LinkedHashMap内部如何实现双向链表的功能,我们暂时把它当做一个黑盒,在后面的Java集合框架章节中去深入了解。

      总之,LinkedHashSet是通过内部的LinkedHashMap保留了基类HashSet的特性,同时又实现了元素存取的有序性,但是因为维护内部的双向链表,在性能上逊色HashSet。

       它的应用场景:1. 在一个流式处理数据的应用中,需要对元素进行去重和保持原有进入顺序;2. 在一个多线程爬虫程序中,需要对爬取到的URL进行去重操作,就可以使用LinkedHashSet来避免并发修改异常。

      TreeSet

      TreeSet是Set接口家族中的一员,它和HashSet以及LinkedHashSet一样,不允许元素重复,同样无法保证线程安全,具体区别如下表:

      TreeSet底层是通过红黑树实现了对元素的排序,但是是通过内部的TreeMap来实现的,红黑树的具体逻辑在TreeMap代码中实现,这点HashSet、LinkedHashSet和TreeSet三者都是一样的,依赖的都是对应的Map集合类。

       TreeSet默认是自然排序,按元素的大小进行排序,具体规则如下:

1.对于数值类型:Integer、Double,默认按照从小到大的顺序进行排序。
2.对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序。

       参考代码如下:


import java.util.TreeSet;

public class TreeSetExample {
    public static void main(String[] args) {
        TreeSet<Integer> set = new TreeSet<>();

        // 添加元素
        set.add(20);
        set.add(10);       
        set.add(30);
        set.add(40);

        // 尝试添加重复元素
        boolean isAdded = set.add(20); // 返回 false

        // 获取第一个和最后一个元素
        int first = set.first(); // 返回 10
        int last = set.last(); // 返回 40

      
        // 遍历 TreeSet
        for (Integer num : set) {
            System.out.println(num);
        }  // 输出顺序为:10 ,20, 30 ,40
    }
}

      自然排序主要应用于Jdk内置的数据类型,对于用户自定义类型需要采用定制排序,定制排序有两种方式:方法一、放入TreeSet集合的元素需要实现接口java.lang.Comparable接口,例如以下代码:


public class TreeSetTest04 {

    public static void main(String[] args) {

        Person1 p1 = new Person1(32);
        Person1 p2 = new Person1(20);
        Person1 p3 = new Person1(25);
        TreeSet<Person1> ts = new TreeSet<>();
        ts.add(p1);
        ts.add(p2);
        ts.add(p3);
        for (Person1 x:
                ts) {
            System.out.println(x);
        }
    }

}

/**
 * 放在TreeSet集合中的元素需要实现java.lang.Comparable接口
 * 并且实现compareTo方法。equals可以不写
 */
class Person1 implements  Comparable<Person1>{
    int age;
    public Person1(int age){
        this.age = age;
    }
    // 重写toString方法
    @Override
    public String toString() {
        return "Person1{" +
                "age=" + age +
                '}';
    }
    /**
     * 需要在这个比较的方法中编写比较的逻辑或者比较的规则,按照什么进行比较
     * 拿着参数k和集合中的每一个key进行比较,返回值可能是大于0 小于0 或者等于0
     * 比较规则最终还是由程序员指定的; 例如按照年龄升序,或者按照年龄降序。
     * @param o
     * @return
     */
    @Override
    public int compareTo(Person1 o) { // c1.compareTo(c2)
        return this.age-o.age; // >0 =0 <0 都有可能
    }
}

   方法二,在构造器TreeSet的时候给它传一个实现了Comparator比较器接口的对象,例如以下代码:


public class TreeSetTest06 {

    public static void main(String[] args) {
        // 此时创建TreeSet集合的时候,需要使用这个比较器。
        // TreeSet<WuGui> wuGui = new TreeSet<>(); // 这样不行,没有通过构造方法传递一个比较器进去。
        // 给构造方法传递一个比较器
        TreeSet<WuGui> wuGui = new TreeSet<>(new WuGuiComparator()); // 底层源码可知其中一个构造方法的参数为比较器
        // 大家可以使用匿名内部类的方式
        wuGui.add(new WuGui(100));
        wuGui.add(new WuGui(10));
        wuGui.add(new WuGui(1000));
        for (WuGui wugui:
             wuGui) {
            System.out.println(wugui);
        }
    }
}
class WuGui {
    int age;
    public WuGui(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "WuGui{" +
                "age=" + age +
                '}';
    }
}
// 单独再这里编写一个比较器
// 比较器实现java.util.Comparator接口 (Comparable是java.lang包下的。Comparator是java.util包下的。)
class WuGuiComparator implements Comparator<WuGui>{
    @Override
    public int compare(WuGui o1, WuGui o2) {
        // 指定比较规则
        // 按照年龄排序
        return o1.age-o2.age;
    }
}

      两种方法适应场景如下:


1.比较规则经常变换: Comparator 接口的设计符合OCP原则(可切换)

2.比较规则较固定: Comparable

    如果一个TreeSet集合两种比较方法都实现了,则以方法二比较器接口优先。

【注意事项

1.自然排序的注意事项:如果字符串里的字符比较多,那么它就是从首字母开始,挨个比较的,要注意的是,此时跟字符串的长度是没有什么关系的,例如 "aaa" 和 "ab",在比较的时候,首先比第一个字母,发现第一个字母都是 a;继续往后来比第二个字母,第二个字母就不一样了,这个时候就已经能确定大小关系了,'a' 比 b 大,此时后面的就不会再看了。

码农爱刷题

为计算机编程爱好者和从业人士提供技术总结和分享 !为前行者蓄力,为后来者探路!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2146867.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Submariner 部署全过程

Submariner 部署全过程 部署集群配置 broker 集群&#xff1a; pod-cidr&#xff1a;11.244.0.0/16 service-cidr 11.96.0.0/12 broker 172.100.0.109 node 172.100.0.108 集群 1&#xff08; pve3 &#xff09;&#xff1a; pod-cidr&#xff1a;10.244.0.0/16 service-…

nodejs 011: nodejs事件驱动编程 EventEmitter 与 IPC

在 Node.js 和许多 JavaScript 环境中&#xff0c;EventEmitter 是一个非常重要的类&#xff0c;用于处理事件驱动编程。EventEmitter 是一个能够发射&#xff08;emit&#xff09;和监听&#xff08;on&#xff09;事件的对象。它常用于创建和处理事件机制&#xff0c;使得程序…

Dubbo与SpringCloud的区别和优缺点

经常会有同学问我&#xff0c;Dubbo和SpringCloud的选择。甚至也经常会有面试官就这个问题刨根问底。 说实话&#xff0c;其实我不太喜欢回答这个问题&#xff0c;本质上来讲&#xff0c;Dubbo的SpringCloud可以算是完全不同赛道的两种东西&#xff0c;就好像问大家西瓜和土豆我…

【Java】多线程前置知识 初识Thread

多线程前置知识 & 初识Thread 冯诺依曼体系结构初步认识存储设备CPU指令 操作系统初识操作系统内核态和用户态 进程/任务进程是什么进程的管理进程的调度虚拟内存地址进程间的通信 线程线程的出现线程是什么线程可能出现的问题线程与进程的联系和区别 协程初识Thread类Thre…

VSCode引用Eigen库无法识别问题解决

VSCode引用Eigen库无法识别问题解决 问题解决 问题 在Ubuntu下使用vscode开发C/C项目时引用了Eigen库&#xff0c;出现Eigen::Vector3d无法识别的问题&#xff0c;提示"no definition found for Vector3d"。但是程序可以正常编译通过。 解决 将 intelli Sense Engi…

【学习资料】袋中共36个球,红白黑格12个,问能一次抽到3个红4个白5个黑的概率是多少?

1、公式计算 1.1 题目1 袋中共 36 36 36个球&#xff0c; 红 \fcolorbox{red}{#FADADE}{\color{red}{红}} 红​ 白 \fcolorbox{white}{#808080}{\color{white}{白}} 白​ 黑 \fcolorbox{#808080}{#0D0D0D}{\color{#808080}{黑}} 黑​各 12 12 12个&#xff0c;问能一次抽到 3…

事件循环异步代码输出顺序题目【基础考核】

简单的事件循环&#xff0c;一道异步代码执行输出顺序问题? 第一题 setTimeout(() > {console.log("A")Promise.resolve().then(() > { console.log("B"); });}, 1000);Promise.resolve().then(() > { console.log("c"); });new Prom…

JSON.parseArray 内存溢出

实际上我的JSON如下&#xff1a; 如果用以下代码&#xff1a;JVM的内存直接飙到内存溢出&#xff0c;报错OutOfMemoryError: Java heap space Object oo JSON.parseArray(json, TestVo.class) 如果我换成了这样&#xff0c;就没事&#xff1a; Object oo JSON.parseObject(…

1.2 测试基础

欢迎大家订阅【软件测试】 专栏&#xff0c;开启你的软件测试学习之旅&#xff01; 文章目录 前言1 测试分类1.1 按生产阶段划分1.2 按代码可见度划分1.3 其他测试 2 质量模型 前言 在软件开发过程中&#xff0c;测试是确保产品质量的重要环节。本文详细讲解了软件测试分类以及…

Python Email库:发送与接收邮件完整指南!

Python Email库如何集成&#xff1f;怎么优化Python Email库性能&#xff1f; Python作为一种强大的编程语言&#xff0c;提供了丰富的库来处理电子邮件&#xff0c;其中最著名的就是Python Email库。AokSend将深入探讨如何使用Python Email库来发送和接收邮件&#xff0c;帮助…

SpringCloud config native 配置

SpringCloud config native 配置 1.概述 最近项目使用springCloud 框架&#xff0c;使用config搭建git作为配置中心。 在私有化部署中&#xff0c;出现很多比较麻烦的和鸡肋的设计。 每次部署都需要安装gitlab 有些环境安装完gitlab&#xff0c;外面不能访问&#xff0c;不给开…

适合运动的骨传导耳机哪款好?分享五款性能卓越骨传导耳机

面对琳琅满目的骨传导耳机市场&#xff0c;是不是既兴奋又迷茫&#xff1f;别怕&#xff0c;我来给你支几招&#xff01;选耳机&#xff0c;最重要的是适合自己&#xff0c;别被各种噱头和价格差异绕晕了头。价格高低与品质好坏并非绝对正比&#xff0c;关键看性价比和个人需求…

Google SERP API 对接说明

Google SERP API 对接说明 Google SERP&#xff08;Search Engine Results Page&#xff09;是用户在Google搜索引擎中输入查询后看到的结果页面。它显示自然搜索结果、广告、特色摘要、知识图谱以及图片、视频等多种内容&#xff0c;旨在为用户提供最相关的信息。 本文将详细…

物联网开发+充电桩管理系统+充电桩系统源码

简述 SpringBoot 框架&#xff0c;充电桩平台充电桩系统充电平台充电桩互联互通协议云快充协议1.5新能源汽车电动自行车公交车-四轮车充电充电源代码充电平台源码Java源码无加密项目 介绍 云快充协议云快充1.5协议云快充协议开源代码云快充底层协议云快充桩直连桩直连协议充…

鸿蒙应用生态构建的核心目标

保护开发者和用户利益的同时维护整体系统的安全性&#xff0c;对生态构建者是至关重要的。以开发者为中心&#xff0c;构建端到端应用安全能力&#xff0c;保护应用自身安全、运行时安全&#xff0c;保障开发者权益&#xff0c;是鸿蒙应用生态构建的核心目标。 应用生命周期主要…

大数据-137 - ClickHouse 集群 表引擎详解2 - MergeTree 存储结构 一级索引 跳数索引

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

CAD图1

文章目录 选择直线工具选择圆形选中圆形 选择直线工具 画一条十字中心线 选择圆形 以十字中心为起点画一个半径为 53 的圆形 选中圆形 选中圆形&#xff0c;捕捉右侧圆形焦点

【北京迅为】《STM32MP157开发板使用手册》- 第四十章 二值信号量实验

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

②MODBUS TCP 转 RS485(RS485与TCP数据双向互传)MODBUS TCP与MODBUS RTU互转(无需编程 独立通道)

型号&#xff1a;1路总线TCP网关&#xff08;单网口&#xff09; MS-A1-5011 1路总线TCP网关&#xff08;双网口&#xff09; MS-A2-5011 2路总线TCP网关&#xff08;单网口&#xff09; MS-A1-5021 2路总线TCP网关&#xff08;双网口&#xff09; MS-A2-5021 4路总…

怎样把PPT上顽固的图标删了

例如&#xff1a; 解决&#xff1a; 首先打开下载好的PPT模板&#xff0c;然后在视图选项卡里面找到幻灯片母版。 进入幻灯片母版后&#xff0c;找到第一页母版页就会看到LOGO了&#xff0c;这时使用鼠标就可以选中删除啦。