Java 集合:泛型、Set 集合及其实现类详解
参考资料:java入门到飞起
Java;泛型;Set 集合;TreeSet;HashSet;数据结构
一、引言
在 Java 编程中,集合框架是一个重要的组成部分,它提供了丰富的数据结构和算法来存储和操作数据。泛型与 Set 集合及其实现类在 Java 集合框架中占据着关键位置,对于提高代码的类型安全性、优化数据存储和检索具有重要意义。
二、泛型
2.1 泛型概述
2.1.1 泛型的介绍
泛型是 JDK 5 中引入的重要特性,它为 Java 语言增添了编译时类型安全检测机制。通过使用泛型,程序员可以在编译阶段就发现类型不匹配的错误,而不是在运行时才出现难以调试的错误,从而提高了程序的稳定性和可靠性。
2.1.2 泛型的好处
- 提前检测问题:将原本在运行时期可能出现的类型问题提前到编译期间,大大降低了运行时错误的发生概率,提高了程序的健壮性。
- 避免强制类型转换:使用泛型后,代码在获取和操作集合元素时无需进行显式的强制类型转换,使代码更加简洁、易读,同时也减少了因强制类型转换不当而引发的
ClassCastException
异常。
2.1.3 泛型的定义格式
- 单类型格式:
<类型>
用于指定一种类型,尖括号内通常使用单个字母表示,常见的如<E>
(表示元素类型,Element 的缩写)、<T>
(表示一般类型,Type 的缩写)。例如,List<E>
表示一个存储E
类型元素的列表。 - 多类型格式:
<类型1,类型2…>
用于指定多种类型,不同类型之间用逗号隔开,如<E,T>
或<K,V>
(常用于表示键值对中的键类型K
和值类型V
)。例如,Map<K, V>
表示一个存储键值对的映射,其中键的类型为K
,值的类型为V
。
三、Set 集合
3.1Set 集合概述和特点【应用】
Set 集合是 Java 集合框架中的一种重要类型,具有以下显著特点:
- 不允许重复元素:Set 集合中不能存储重复的元素,这使得 Set 集合在需要确保元素唯一性的场景中非常有用,例如去重操作。
- 无索引: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
对象(TreeSet
是Set
接口的一个实现类),然后向集合中添加了一些字符串元素,其中包含重复的元素 “aaa”。通过Iterator
迭代器和增强 for 循环两种方式对集合进行遍历,输出集合中的元素。可以看到,重复元素 “aaa” 只出现了一次,体现了 Set 集合不允许重复元素的特性。
Iterator迭代器介绍
Iterator(迭代器)是Java集合框架中的一个重要接口,用于遍历集合中的元素。它提供了一种统一的方式来访问集合元素,而不需要暴露集合的内部表示。核心方法如下:
boolean hasNext()
:判断是否还有下一个元素E next()
:返回下一个元素void remove()
:删除上次调用next()
返回的元素(可选操作)
关键点
-
Set
的迭代器只能单向遍历(hasNext()
+next()
),不能反向遍历。 - 删除元素必须用
iterator.remove()
,直接使用set.remove()
会抛出ConcurrentModificationException
。 - Set 是无序的(除非使用
LinkedHashSet
或TreeSet
),所以遍历顺序可能与插入顺序不同。 - 增强 for 循环底层也是用 Iterator,但不能在遍历时删除元素(除非用
Iterator
)。
四、TreeSet 集合
4.1 TreeSet 集合概述和特点【应用】
TreeSet
是Set
接口的一个实现类,除了具备 Set 集合的基本特点(不允许重复元素、无索引)外,还具有以下特性:
- 元素排序:
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 实现步骤
- 创建 TreeSet 集合:使用空参构造创建
TreeSet
集合,因为无参构造方法使用自然排序对元素进行排序。 - 实现 Comparable 接口:自定义的
Student
类需要实现Comparable
接口,通过重写compareTo(T o)
方法来定义元素的比较规则。 - 重写 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;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}@Overridepublic 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>
,那么:
- 可以调用
Collections.sort()
或Arrays.sort()
对Student
对象列表进行排序:List<Student> students = new ArrayList<>(); // 添加学生对象... Collections.sort(students); // 自动调用 compareTo 方法排序
- 可以在
TreeSet
或TreeMap
中自动排序(因为它们依赖Comparable
或Comparator
进行排序)。 - 可以方便地进行对象比较,例如在
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 省略...@Overridepublic 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 实现步骤
- 使用带参构造创建 TreeSet 集合:
TreeSet
集合的带参构造方法使用比较器排序对元素进行排序。 - 实现 Comparator 接口:让集合构造方法接收一个实现了
Comparator
接口的对象,在其compare(T o1, T o2)
方法中定义元素的比较规则。 - 重写 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;}@Overridepublic String toString() {return "Teacher{" +"name='" + name + '\'' +", age=" + age +'}';}
}
测试类:
public class MyTreeSet4 {public static void main(String[] args) {//创建集合对象TreeSet<Teacher> ts = new TreeSet<>(new Comparator<Teacher>() {@Overridepublic 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 两种比较方式总结
- 自然排序:自定义类实现
Comparable
接口,重写compareTo
方法,集合根据该方法的返回值进行排序。这种方式适用于类本身具有自然的比较顺序,并且在多个地方都需要使用相同排序规则的情况。 - 比较器排序:创建
TreeSet
对象时传递一个实现了Comparator
接口的对象,重写compare
方法,集合依据此方法的返回值进行排序。当自然排序不能满足特定需求,或者需要针对不同场景使用不同排序规则时,比较器排序更为灵活。 - 使用选择:在实际使用中,通常优先考虑自然排序。当自然排序无法满足需求时,必须使用比较器排序来实现自定义的排序逻辑。