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 集合存储字符串并进行遍历:

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 是无序的​(除非使用 LinkedHashSet 或 TreeSet),所以遍历顺序可能与插入顺序不同。
  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类型的整数并进行遍历:

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

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 包中,它只有一个方法:

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

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

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

  1. ​可以调用 Collections.sort() 或 Arrays.sort() 对 Student 对象列表进行排序​:
    List<Student> students = new ArrayList<>();
    // 添加学生对象...
    Collections.sort(students);  // 自动调用 compareTo 方法排序
  2. ​可以在 TreeSet 或 TreeMap 中自动排序​(因为它们依赖 Comparable 或 Comparator 进行排序)。
  3. ​可以方便地进行对象比较,例如在 if (student1.compareTo(student2) > 0) 这样的逻辑中使用。

​如何实现 Comparable<Student>?​​

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

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 是降序逻辑)。

测试类

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 代码实现

老师类

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 +
                '}';
    }
}

测试类

import java.util.Comparator;
import java.util.TreeSet;

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方法中定义了老师对象的比较规则。在创建老师对象并添加到集合后,遍历集合时老师对象按照年龄和姓名的指定规则(toString)进行排序。

4.5 两种比较方式总结

  1. 自然排序:自定义类实现Comparable接口,重写compareTo方法,集合根据该方法的返回值进行排序。这种方式适用于类本身具有自然的比较顺序,并且在多个地方都需要使用相同排序规则的情况。
  2. 比较器排序:创建TreeSet对象时传递一个实现了Comparator接口的对象,重写compare方法,集合依据此方法的返回值进行排序。当自然排序不能满足特定需求,或者需要针对不同场景使用不同排序规则时,比较器排序更为灵活。
  3. 使用选择:在实际使用中,通常优先考虑自然排序。当自然排序无法满足需求时,必须使用比较器排序来实现自定义的排序逻辑。

 

五、 HashSet集合

5.1 HashSet集合概述和特点

HashSet基于哈希表实现,具有以下特点:
1. 元素唯一性:不允许存储重复元素
2. 无序性:不保证元素的存储顺序
3. 无索引:不能通过索引访问元素
4. 高效性:基于哈希算法,提供常数时间复杂度的基本操作

5.2 HashSet集合的基本应用

存储字符串并遍历:

public class HashSetDemo {
    public static void main(String[] args) {
        //创建集合对象
        HashSet<String> set = new HashSet<>();
        
        //添加元素(重复元素会被自动去重)
        set.add("hello");
        set.add("world");
        set.add("java");
        set.add("world"); //重复元素不会被添加
        
        //遍历
        for(String s : set) {
            System.out.println(s);
        }
    }
}

输出:

world
java 
hello


 

 5.3 HashSet存储自定义对象

当存储自定义对象时,必须重写hashCode()和equals()方法以确保元素唯一性:

测试类:

import java.util.HashSet;

/**
 * 该类演示了如何使用 HashSet 存储自定义类型元素。
 * 为保证 HashSet 集合中的对象唯一,需要重写对象的 hashCode 和 equals 方法。
 */
public class HashSetDemo02 {
    public static void main(String[] args) {
        // 创建 HashSet 集合对象
        HashSet<Student> hs = new HashSet<>();

        // 创建学生对象
        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);
        Student s4 = new Student("王祖贤", 33); // 与 s3 相同

        // 添加到集合
        hs.add(s1);
        hs.add(s2);
        hs.add(s3);
        hs.add(s4); // 由于重写了 hashCode 和 equals 方法,该元素不会被添加

        // 遍历集合
        for (Student s : hs) {
            System.out.println(s.getName() + "," + s.getAge());
        }
    }
}

/**
 * 学生类,包含姓名和年龄属性。
 * 重写了 hashCode 和 equals 方法以确保在 HashSet 中的唯一性。
 */
class Student {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(name, age);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;//先检查传入对象是否为 null ,若为 null 则返回 false ;接着检查传入对象的类型和当前对象的类型是否一致,若不一致也返回 false 。
        Student student = (Student) o; // 进行类型转换
        return age == student.age && java.util.Objects.equals(name, student.name);//最后比较两个对象的 name 和 age 属性是否相等,若相等则返回 true ,否则返回 false 。
    }
}

结果输出:

王祖贤,33
张曼玉,35
林青霞,30

结论

泛型提供了编译时的类型安全检查,显著提高了代码的健壮性。Set系列集合通过不同的实现类(TreeSet和HashSet)满足了不同的应用需求:TreeSet适合需要排序的场景,而HashSet则更适合快速查找和去重。在实际开发中,应根据具体需求选择合适的集合类型,并注意自定义对象在存储时的特殊要求(如重写hashCode和equals方法)。理解这些集合的特性和实现机制对于编写高效、健壮的Java程序具有重要意义。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码银

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值