Java 集合:泛型、Set 集合及其实现类详解

参考资料:java入门到飞起

Java;泛型;Set 集合;TreeSet;HashSet;数据结构

一、引言

在 Java 编程中,集合框架是一个重要的组成部分,它提供了丰富的数据结构和算法来存储和操作数据。泛型与 Set 集合及其实现类在 Java 集合框架中占据着关键位置,对于提高代码的类型安全性、优化数据存储和检索具有重要意义。

二、泛型

2.1 泛型概述

2.1.1 泛型的介绍

泛型是 JDK 5 中引入的重要特性,它为 Java 语言增添了编译时类型安全检测机制。通过使用泛型,程序员可以在编译阶段就发现类型不匹配的错误,而不是在运行时才出现难以调试的错误,从而提高了程序的稳定性和可靠性。

2.1.2 泛型的好处
  1. 提前检测问题:将原本在运行时期可能出现的类型问题提前到编译期间,大大降低了运行时错误的发生概率,提高了程序的健壮性。
  2. 避免强制类型转换 :使用泛型后,代码在获取和操作集合元素时无需进行显式的强制类型转换,使代码更加简洁、易读,同时也减少了因强制类型转换不当而引发的ClassCastException异常。
2.1.3 泛型的定义格式
  1. 单类型格式<类型>用于指定一种类型,尖括号内通常使用单个字母表示,常见的如<E>(表示元素类型,Element 的缩写)、<T>(表示一般类型,Type 的缩写)。例如,List<E>表示一个存储E类型元素的列表。
  2. 多类型格式<类型1,类型2...>用于指定多种类型,不同类型之间用逗号隔开,如<E,T><K,V>(常用于表示键值对中的键类型K和值类型V)。例如,Map<K, V>表示一个存储键值对的映射,其中键的类型为K,值的类型为V

三、Set 集合

3.1Set 集合概述和特点【应用】

Set 集合是 Java 集合框架中的一种重要类型,具有以下显著特点:

  1. 不允许重复元素:Set 集合中不能存储重复的元素,这使得 Set 集合在需要确保元素唯一性的场景中非常有用,例如去重操作。
  2. 无索引:Set 集合没有索引,这意味着无法像列表那样通过索引来访问元素,也不能使用普通的 for 循环进行遍历。

3.2Set 集合的使用【应用】

以下代码展示了如何使用 Set 集合存储字符串并进行遍历:

java 复制代码
public class MySet1 {
    public static void main(String[] args) {
        //创建集合对象
        Set<String> set = new TreeSet<>();
        //添加元素
        set.add("ccc");
        set.add("aaa");
        set.add("aaa");
        set.add("bbb");

        //Set集合是没有索引的,所以不能使用通过索引获取元素的方法
        //遍历集合
        Iterator<String> it = set.iterator();
        while (it.hasNext()) {
            String s = it.next();
            System.out.println(s);
        }

        // 如果需要删除元素,可以使用 iterator.remove()
        // 注意:不能直接使用 set.remove(),否则会抛出 ConcurrentModificationException
        // 示例:删除 "aaa"
        iterator = set.iterator(); // 重新获取迭代器
        while (iterator.hasNext()) {
            String s = iterator.next();
            if (s.equals("aaa")) {
                iterator.remove(); // 安全删除
            }
        }

        System.out.println("-----------------------------------");
        for (String s : set) {
            System.out.println(s);
        }
    }
}

在上述代码中,首先创建了一个TreeSet对象(TreeSetSet接口的一个实现类),然后向集合中添加了一些字符串元素,其中包含重复的元素 "aaa"。通过Iterator迭代器和增强 for 循环两种方式对集合进行遍历,输出集合中的元素。可以看到,重复元素 "aaa" 只出现了一次,体现了 Set 集合不允许重复元素的特性。

Iterator迭代器介绍

Iterator(迭代器)是Java集合框架中的一个重要接口,用于遍历集合中的元素。它提供了一种统一的方式来访问集合元素,而不需要暴露集合的内部表示。核心方法如下:

  • boolean hasNext():判断是否还有下一个元素
  • E next():返回下一个元素
  • void remove():删除上次调用next()返回的元素(可选操作)

关键点

  1. Set 的迭代器只能单向遍历(hasNext() + next()),不能反向遍历。
  2. 删除元素必须用 iterator.remove() ,直接使用 set.remove() 会抛出 ConcurrentModificationException
  3. Set 是无序的 (除非使用 LinkedHashSetTreeSet),所以遍历顺序可能与插入顺序不同。
  4. 增强 for 循环底层也是用 Iterator ,但不能在遍历时删除元素(除非用 Iterator)。

四、TreeSet 集合

4.1 TreeSet 集合概述和特点【应用】

TreeSetSet接口的一个实现类,除了具备 Set 集合的基本特点(不允许重复元素、无索引)外,还具有以下特性:

  1. 元素排序TreeSet可以将存储的元素按照特定规则进行排序。
    • 自然排序 :通过TreeSet()无参构造方法创建的集合,会根据元素的自然排序进行排序。所谓自然排序,是指元素所属的类实现Comparable接口,并在其中定义比较规则。
    • 比较器排序 :通过TreeSet(Comparator comparator)带参构造方法创建的集合,会根据指定的比较器进行排序。比较器是一个实现了Comparator接口的对象,在其compare(T o1, T o2)方法中定义元素的比较规则。

4.2 TreeSet 集合基本使用【应用】

以下代码展示了如何使用TreeSet集合存储Integer类型的整数并进行遍历:

java 复制代码
public class TreeSetDemo01 {
    public static void main(String[] args) {
        //创建集合对象
        TreeSet<Integer> ts = new TreeSet<Integer>();

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

        //遍历集合
        for (Integer i : ts) {
            System.out.println(i);
        }
    }
}

在上述代码中,创建了一个TreeSet集合对象ts,并向其中添加了一些Integer类型的整数,包括重复的数字 30。通过增强 for 循环遍历集合时,输出的元素按照从小到大的顺序排列,体现了TreeSet集合的排序特性,并且重复元素只保留了一个。

4.3 自然排序 Comparable 的使用【应用】

4.3.1 案例需求

存储学生对象并遍历,使用TreeSet集合的无参构造方法,要求按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序。

4.3.2 实现步骤
  1. 创建 TreeSet 集合 :使用空参构造创建TreeSet集合,因为无参构造方法使用自然排序对元素进行排序。
  2. 实现 Comparable 接口 :自定义的Student类需要实现Comparable接口,通过重写compareTo(T o)方法来定义元素的比较规则。
  3. 重写 compareTo 方法:在重写方法时,要按照指定的主要条件(年龄从小到大)和次要条件(年龄相同时按姓名字母顺序)编写比较逻辑。
4.3.3 代码实现

学生类

快速构建构造方法:快捷键:Alt+Insert

java 复制代码
public class Student implements Comparable<Student>{
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        //按照对象的年龄进行排序
        //主要判断条件: 按照年龄从小到大排序
        int result = this.age - o.age;
        //次要判断条件: 年龄相同时,按照姓名的字母顺序排序
        result = result == 0? this.name.compareTo(o.getName()) : result;
        return result;
    }
}
PS:Comparable是干嘛的?

Comparable 是 Java 提供的一个 泛型接口,定义在 java.lang 包中,它只有一个方法:

java 复制代码
public interface Comparable<T> {
    int compareTo(T o);  // 比较当前对象与参数对象 o 的大小
}

为什么 Student 类要实现 Comparable<Student>

如果 Student 类实现了 Comparable<Student>,那么:

  1. 可以调用 Collections.sort()Arrays.sort()Student 对象列表进行排序:

    java 复制代码
    List<Student> students = new ArrayList<>();
    // 添加学生对象...
    Collections.sort(students);  // 自动调用 compareTo 方法排序
  2. 可以在 TreeSetTreeMap 中自动排序(因为它们依赖 ComparableComparator 进行排序)。

  3. 可以方便地进行对象比较,例如在 if (student1.compareTo(student2) > 0) 这样的逻辑中使用。

如何实现 Comparable<Student>

你需要在 Student 类中 重写 compareTo() 方法,定义如何比较两个 Student 对象。例如:

java 复制代码
public class Student implements Comparable<Student> {
    private String name;
    private int age;
    private double score;

    // 构造方法、getter/setter 省略...

    @Override
    public int compareTo(Student other) {
        // 按分数升序排序
        return Double.compare(this.score, other.score);
        
        // 或者按姓名升序排序(字符串比较)
        // return this.name.compareTo(other.name);
        
        // 或者按年龄降序排序(注意负号)
        // return Integer.compare(other.age, this.age);
    }
}

示例说明:

  • Double.compare(this.score, other.score):比较两个学生的分数(升序)。
  • this.name.compareTo(other.name):按姓名字典序排序(升序)。
  • Integer.compare(other.age, this.age):按年龄降序排序(因为 other.age - this.age 是降序逻辑)。

测试类

objectivec 复制代码
public class MyTreeSet2 {
    public static void main(String[] args) {
        //创建集合对象
        TreeSet<Student> ts = new TreeSet<>();
        //创建学生对象
        Student s1 = new Student("zhangsan", 28);
        Student s2 = new Student("lisi", 27);
        Student s3 = new Student("wangwu", 29);
        Student s4 = new Student("zhaoliu", 28);
        Student s5 = new Student("qianqi", 30);
        //把学生添加到集合
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
        //遍历集合
        for (Student student : ts) {
            System.out.println(student);
        }
    }
}

在上述代码中,Student类实现了Comparable接口,并在compareTo方法中定义了排序规则。在测试类中,创建了TreeSet集合并添加了多个学生对象,遍历集合时,学生对象按照年龄从小到大排序,年龄相同的按照姓名字母顺序排序。

4.4 比较器排序 Comparator 的使用【应用】

4.4.1 案例需求

存储老师对象并遍历,创建TreeSet集合使用带参构造方法,要求按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序。

4.4.2 实现步骤
  1. 使用带参构造创建 TreeSet 集合TreeSet集合的带参构造方法使用比较器排序对元素进行排序。
  2. 实现 Comparator 接口 :让集合构造方法接收一个实现了Comparator接口的对象,在其compare(T o1, T o2)方法中定义元素的比较规则。
  3. 重写 compare 方法:按照指定的主要条件和次要条件编写比较逻辑。
4.4.3 代码实现

老师类

java 复制代码
public class Teacher {
    private String name;
    private int age;

    public Teacher() {
    }

    public Teacher(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Teacher{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试类

java 复制代码
public class MyTreeSet4 {
    public static void main(String[] args) {
        //创建集合对象
        TreeSet<Teacher> ts = new TreeSet<>(new Comparator<Teacher>() {
            @Override
            public int compare(Teacher o1, Teacher o2) {
                //o1表示现在要存入的那个元素
                //o2表示已经存入到集合中的元素

                //主要条件
                int result = o1.getAge() - o2.getAge();
                //次要条件
                result = result == 0? o1.getName().compareTo(o2.getName()) : result;
                return result;
            }
        });
        //创建老师对象
        Teacher t1 = new Teacher("zhangsan", 23);
        Teacher t2 = new Teacher("lisi", 22);
        Teacher t3 = new Teacher("wangwu", 24);
        Teacher t4 = new Teacher("zhaoliu", 24);
        //把老师添加到集合
        ts.add(t1);
        ts.add(t2);
        ts.add(t3);
        ts.add(t4);
        //遍历集合
        for (Teacher teacher : ts) {
            System.out.println(teacher);
        }
    }
}

在上述代码中,测试类通过TreeSet的带参构造方法传入一个匿名内部类,该内部类实现了Comparator接口,并在compare方法中定义了老师对象的比较规则。在创建老师对象并添加到集合后,遍历集合时老师对象按照年龄和姓名的指定规则进行排序。

4.5 两种比较方式总结

  1. 自然排序 :自定义类实现Comparable接口,重写compareTo方法,集合根据该方法的返回值进行排序。这种方式适用于类本身具有自然的比较顺序,并且在多个地方都需要使用相同排序规则的情况。
  2. 比较器排序 :创建TreeSet对象时传递一个实现了Comparator接口的对象,重写compare方法,集合依据此方法的返回值进行排序。当自然排序不能满足特定需求,或者需要针对不同场景使用不同排序规则时,比较器排序更为灵活。
  3. 使用选择:在实际使用中,通常优先考虑自然排序。当自然排序无法满足需求时,必须使用比较器排序来实现自定义的排序逻辑。
相关推荐
碎梦归途1 小时前
23种设计模式-行为型模式之备忘录模式(Java版本)
java·jvm·设计模式·软考·备忘录模式·软件设计师·行为型模式
越城1 小时前
算法效率的钥匙:从大O看复杂度计算 —— C语言数据结构第一讲
c语言·开发语言·数据结构·算法
菠萝崽.2 小时前
安装docker,在docker上安装mysql,docker上安装nginx
java·mysql·nginx·docker·软件工程·springboot·开发
极小狐2 小时前
极狐GitLab 议题权重有什么作用?
开发语言·数据库·chrome·c#·gitlab
狐凄3 小时前
Python实例题:使用Pvthon3编写系列实用脚本
java·网络·python
董先生_ad986ad5 小时前
C# 中的 `lock` 关键字本质
开发语言·c#
Lxinccode5 小时前
Java查询数据库表信息导出Word-获取数据库实现[1]:KingbaseES
java·数据库·word·获取数据库信息·获取kingbasees信息
元亓亓亓5 小时前
Java后端开发day36--源码解析:HashMap
java·开发语言·数据结构
sd21315125 小时前
RabbitMQ 复习总结
java·rabbitmq
道剑剑非道5 小时前
QT 打包安装程序【windeployqt.exe】报错c000007d原因:Conda巨坑
开发语言·qt·conda