编程自学指南:java程序设计开发,Java Set 集合详解
一、课程信息
学习目标
- 理解 Set 集合的概念、特点和用途。
- 掌握 HashSet、TreeSet 和 LinkedHashSet 的使用方法和区别。
- 学会使用 Set 集合解决实际问题,如去重操作。
- 了解 Set 集合的内部实现原理。
课程重点
- Set 集合的特点和常用方法。
- HashSet、TreeSet 和 LinkedHashSet 的使用场景。
- 元素的唯一性判断机制。
课程难点
- TreeSet 的排序规则和自定义排序。
- HashSet 的哈希冲突处理机制。
二、课程导入
生活中的去重场景
在生活中,我们经常会遇到需要去重的情况。例如,在一个班级的学生名单中,可能会有重复的名字,我们需要将这些重复的名字去除,只保留唯一的名字。再比如,在统计一个网站的访问 IP 地址时,也需要去除重复的 IP 地址,以准确统计访问的用户数量。
编程中的去重需求
在 Java 编程中,我们也会经常遇到需要去重的场景。例如,从一个数组中去除重复的元素,或者从一个列表中去除重复的对象。使用普通的数组或列表来实现去重操作比较麻烦,而 Java 提供的 Set 集合可以很好地解决这个问题。
示例代码引入
import java.util.ArrayList;
import java.util.List;
public class DuplicateRemovalExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Alice");
// 手动去重比较麻烦
List<String> uniqueNames = new ArrayList<>();
for (String name : names) {
if (!uniqueNames.contains(name)) {
uniqueNames.add(name);
}
}
System.out.println(uniqueNames);
}
}
使用 Set 集合可以更简洁地实现去重:
import java.util.HashSet;
import java.util.Set;
public class SetDuplicateRemovalExample {
public static void main(String[] args) {
Set<String> names = new HashSet<>();
names.add("Alice");
names.add("Bob");
names.add("Alice");
System.out.println(names);
}
}
三、Set 集合概述
定义
Set 是 Java 集合框架中的一个接口,它继承自 Collection 接口。Set 集合中的元素是唯一的,不允许有重复的元素。
特点
- 元素唯一性:Set 集合中不允许存储重复的元素,如果尝试添加重复的元素,添加操作将失败。
- 无序性(部分实现):HashSet 和 LinkedHashSet 不保证元素的插入顺序,而 TreeSet 会对元素进行排序。
- 不支持索引访问:Set 集合没有像 List 集合那样的索引,不能通过索引来访问元素。
常用实现类
- HashSet:基于哈希表实现,不保证元素的顺序,元素的存储位置由元素的哈希值决定。
- TreeSet:基于红黑树实现,会对元素进行排序,默认按照元素的自然顺序排序,也可以通过自定义比较器进行排序。
- LinkedHashSet:基于哈希表和链表实现,既保证了元素的唯一性,又能保持元素的插入顺序。
四、HashSet 详解
内部实现原理
HashSet 内部使用哈希表(实际上是一个 HashMap)来存储元素。当向 HashSet 中添加元素时,首先会计算元素的哈希值,然后根据哈希值找到对应的桶(bucket),如果桶中没有元素,则直接将元素放入桶中;如果桶中已经有元素,则会调用元素的 equals()
方法来判断元素是否相等,如果相等则不添加,否则将元素添加到桶中。
示例代码
import java.util.HashSet;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
// 创建 HashSet 对象
Set<String> fruits = new HashSet<>();
// 添加元素
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
fruits.add("Apple"); // 重复元素,添加失败
// 遍历元素
for (String fruit : fruits) {
System.out.println(fruit);
}
// 判断元素是否存在
boolean containsApple = fruits.contains("Apple");
System.out.println("Contains Apple: " + containsApple);
// 删除元素
fruits.remove("Banana");
System.out.println("After removing Banana: " + fruits);
}
}
哈希冲突处理
当不同的元素计算出相同的哈希值时,就会发生哈希冲突。HashSet 使用链地址法来处理哈希冲突,即每个桶中存储一个链表,当发生哈希冲突时,将冲突的元素添加到链表中。
元素唯一性判断
HashSet 判断元素是否相等的规则是:首先比较元素的哈希值,如果哈希值不同,则认为元素不相等;如果哈希值相同,则调用元素的 equals()
方法进一步比较。因此,为了保证元素的唯一性,重写 equals()
方法时必须同时重写 hashCode()
方法。
import java.util.HashSet;
import java.util.Set;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + age;
return result;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class HashSetCustomObjectExample {
public static void main(String[] args) {
Set<Person> people = new HashSet<>();
people.add(new Person("Alice", 20));
people.add(new Person("Bob", 25));
people.add(new Person("Alice", 20)); // 重复元素,添加失败
for (Person person : people) {
System.out.println(person);
}
}
}
五、TreeSet 详解
内部实现原理
TreeSet 内部使用红黑树(一种自平衡的二叉搜索树)来存储元素。红黑树可以保证元素的有序性,并且插入、删除和查找操作的时间复杂度都是 O (log n)。
示例代码
import java.util.TreeSet;
import java.util.Set;
public class TreeSetExample {
public static void main(String[] args) {
// 创建 TreeSet 对象
Set<Integer> numbers = new TreeSet<>();
// 添加元素
numbers.add(3);
numbers.add(1);
numbers.add(2);
// 遍历元素
for (Integer number : numbers) {
System.out.println(number); // 输出:1 2 3
}
// 获取第一个和最后一个元素
Integer first = ((TreeSet<Integer>) numbers).first();
Integer last = ((TreeSet<Integer>) numbers).last();
System.out.println("First: " + first + ", Last: " + last);
}
}
自然排序和自定义排序
- 自然排序:如果元素实现了
Comparable
接口,TreeSet 会按照元素的自然顺序进行排序。例如,Integer
、String
等类都实现了Comparable
接口。
import java.util.TreeSet;
import java.util.Set;
class Student implements Comparable<Student> {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.score, other.score);
}
@Override
public String toString() {
return "Student{name='" + name + "', score=" + score + "}";
}
}
public class TreeSetNaturalOrderExample {
public static void main(String[] args) {
Set<Student> students = new TreeSet<>();
students.add(new Student("Alice", 80));
students.add(new Student("Bob", 90));
students.add(new Student("Charlie", 70));
for (Student student : students) {
System.out.println(student);
}
}
}
- 自定义排序:如果元素没有实现
Comparable
接口,或者需要按照自定义的规则进行排序,可以在创建 TreeSet 时传入一个Comparator
对象。
import java.util.Comparator;
import java.util.TreeSet;
import java.util.Set;
class Book {
private String title;
private double price;
public Book(String title, double price) {
this.title = title;
this.price = price;
}
public String getTitle() {
return title;
}
public double getPrice() {
return price;
}
@Override
public String toString() {
return "Book{title='" + title + "', price=" + price + "}";
}
}
public class TreeSetCustomOrderExample {
public static void main(String[] args) {
// 自定义比较器,按价格排序
Comparator<Book> priceComparator = Comparator.comparingDouble(Book::getPrice);
Set<Book> books = new TreeSet<>(priceComparator);
books.add(new Book("Java Programming", 50.0));
books.add(new Book("Python Basics", 30.0));
books.add(new Book("Data Structures", 70.0));
for (Book book : books) {
System.out.println(book);
}
}
}
六、LinkedHashSet 详解
内部实现原理
LinkedHashSet 是 HashSet 的子类,它在 HashSet 的基础上维护了一个双向链表,用于记录元素的插入顺序。因此,LinkedHashSet 既保证了元素的唯一性,又能保持元素的插入顺序。
示例代码
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetExample {
public static void main(String[] args) {
// 创建 LinkedHashSet 对象
Set<String> colors = new LinkedHashSet<>();
// 添加元素
colors.add("Red");
colors.add("Green");
colors.add("Blue");
colors.add("Red"); // 重复元素,添加失败
// 遍历元素
for (String color : colors) {
System.out.println(color); // 输出:Red Green Blue
}
}
}
七、HashSet、TreeSet 和 LinkedHashSet 的比较
比较项 | HashSet | TreeSet | LinkedHashSet |
---|---|---|---|
内部实现 | 哈希表 | 红黑树 | 哈希表 + 双向链表 |
元素顺序 | 无序 | 有序(自然排序或自定义排序) | 保持插入顺序 |
插入、删除、查找效率 | O(1) | O(log n) | O(1) |
适用场景 | 不需要元素顺序,注重插入、删除和查找效率 | 需要元素排序 | 需要保持元素插入顺序 |
八、课堂练习
练习 1
编写一个程序,使用 HashSet 去除数组中的重复元素,并输出去重后的元素。
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class Exercise1 {
public static void main(String[] args) {
Integer[] numbers = {1, 2, 3, 2, 4, 3, 5};
// 请完成代码
}
}
练习 2
编写一个程序,使用 TreeSet 对一组字符串进行排序,并输出排序后的结果。
import java.util.TreeSet;
import java.util.Set;
public class Exercise2 {
public static void main(String[] args) {
String[] words = {"apple", "banana", "cherry", "date"};
// 请完成代码
}
}
练习 3
编写一个程序,使用 LinkedHashSet 存储一组学生的姓名,并保持插入顺序,最后输出这些姓名。
import java.util.LinkedHashSet;
import java.util.Set;
public class Exercise3 {
public static void main(String[] args) {
String[] names = {"Alice", "Bob", "Charlie", "Alice"};
// 请完成代码
}
}
九、课程总结
重点回顾
- Set 集合的概念、特点和常用实现类。
- HashSet、TreeSet 和 LinkedHashSet 的内部实现原理、使用方法和区别。
- 元素的唯一性判断机制和排序规则。
注意事项
- 在使用 HashSet 时,要确保元素的
equals()
方法和hashCode()
方法正确重写。 - 在使用 TreeSet 时,元素必须实现
Comparable
接口或传入自定义的Comparator
对象。
十、课后作业
作业 1
编写一个程序,使用 HashSet 存储一组员工对象(包含员工编号、姓名和工资),并去除重复的员工对象。要求重写员工类的 equals()
方法和 hashCode()
方法。
作业 2
编写一个程序,使用 TreeSet 对一组学生对象(包含学生姓名和成绩)按照成绩从高到低进行排序,并输出排序后的结果。可以通过自定义比较器来实现。
作业 3
编写一个程序,使用 LinkedHashSet 存储一组商品对象(包含商品名称和价格),并保持商品的插入顺序,最后输出这些商品的信息。