day10:HashSet与其他集合

📚 学习目标

  • 理解HashSet的实现原理
  • 掌握HashSet的常用方法
  • 理解TreeSet与自然排序
  • 理解LinkedHashSet的特点
  • 掌握各集合类的选择和使用
  • 理解Collections工具类的使用

一、HashSet概述

1.1 HashSet简介

什么是HashSet?

HashSet是基于HashMap实现的Set集合,存储不重复的元素。

继承体系
java.lang.Object
  └── java.util.AbstractCollection<E>
        └── java.util.AbstractSet<E>
              └── java.util.HashSet<E>
实现接口
  • Set:集合接口
  • Cloneable:支持克隆
  • Serializable:支持序列化

1.2 HashSet特点

特点说明
无序性不保证元素的顺序
元素唯一不允许存储重复元素
允许null允许存储一个null元素
非线程安全多线程环境下需要手动同步
底层实现基于HashMap
查询快基于哈希表,contains时间复杂度O(1)

二、HashSet源码分析

2.1 核心成员变量

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    
    // 底层使用HashMap存储元素
    private transient HashMap<E,Object> map;
    
    // 空的Object对象,作为HashMap的value
    private static final Object PRESENT = new Object();
}

2.2 构造方法

public class HashSet<E> {
    // 底层创建一个空的HashMap
    public HashSet() {
        map = new HashMap<>();
    }
    
    // 指定初始容量
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
    
    // 指定初始容量和负载因子
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    
    // 使用集合初始化
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
}

2.3 常用方法实现

// add方法
public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}

// remove方法
public boolean remove(Object o) {
    return map.remove(o) == PRESENT;
}

// contains方法
public boolean contains(Object o) {
    return map.containsKey(o);
}

// size方法
public int size() {
    return map.size();
}

// isEmpty方法
public boolean isEmpty() {
    return map.isEmpty();
}

// clear方法
public void clear() {
    map.clear();
}

三、HashSet常用方法

3.1 添加元素

import java.util.HashSet;

public class HashSetAddDemo {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>();
        
        // 1. add(E e):添加元素
        boolean added1 = set.add("Java");
        boolean added2 = set.add("Python");
        boolean added3 = set.add("Java");  // 重复元素
        
        System.out.println("添加Java: " + added1);  // true
        System.out.println("添加Python: " + added2);  // true
        System.out.println("添加重复Java: " + added3);  // false
        
        System.out.println("HashSet: " + set);
        
        // 2. addAll(Collection c):添加集合
        HashSet<String> otherSet = new HashSet<>();
        otherSet.add("C++");
        otherSet.add("Go");
        
        set.addAll(otherSet);
        System.out.println("addAll后: " + set);
    }
}

3.2 删除元素

import java.util.HashSet;

public class HashSetRemoveDemo {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>();
        set.add("Java");
        set.add("Python");
        set.add("C++");
        set.add("Go");
        
        System.out.println("原始集合: " + set);
        
        // 1. remove(Object o):删除元素
        boolean removed = set.remove("Python");
        System.out.println("删除Python: " + removed);  // true
        System.out.println("删除后: " + set);
        
        // 2. removeAll(Collection c):删除集合中的所有元素
        HashSet<String> toRemove = new HashSet<>();
        toRemove.add("C++");
        toRemove.add("Ruby");
        
        boolean changed = set.removeAll(toRemove);
        System.out.println("removeAll: " + changed);
        System.out.println("removeAll后: " + set);
        
        // 3. clear():清空集合
        set.clear();
        System.out.println("clear后: " + set);
        System.out.println("isEmpty: " + set.isEmpty());
    }
}

3.3 查询元素

import java.util.HashSet;

public class HashSetQueryDemo {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>();
        set.add("Java");
        set.add("Python");
        set.add("C++");
        set.add("Go");
        
        // 1. contains(Object o):判断是否包含元素
        System.out.println("contains(\"Java\"): " + set.contains("Java"));
        System.out.println("contains(\"Rust\"): " + set.contains("Rust"));
        
        // 2. isEmpty():判断是否为空
        System.out.println("isEmpty: " + set.isEmpty());
        
        // 3. size():获取元素个数
        System.out.println("size: " + set.size());
        
        // 4. 遍历
        System.out.println("\n遍历方式1:forEach");
        for (String item : set) {
            System.out.println(item);
        }
        
        System.out.println("\n遍历方式2:Iterator");
        java.util.Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

四、HashSet去重原理

4.1 去重机制

HashSet通过hashCode()和equals()两个方法判断元素是否重复。

去重流程
1. 添加元素A
   ↓
2. 计算A的hashCode()
   ↓
3. 根据hashCode()找到存储位置
   ↓
4. 如果该位置为空,直接存入
   ↓
5. 如果该位置不为空,遍历链表/红黑树
   ↓
6. 比较A与已有元素的equals()
   ↓
7. 如果equals()返回true,拒绝添加
   ↓
8. 否则,添加成功

4.2 自定义对象去重

import java.util.HashSet;
import java.util.Objects;

public class HashSetDeduplication {
    public static void main(String[] args) {
        HashSet<Student> set = new HashSet<>();
        
        set.add(new Student(1, "张三"));
        set.add(new Student(2, "李四"));
        set.add(new Student(1, "张三"));  // 会去重吗?
        
        System.out.println("集合大小: " + set.size());  // 取决于是否重写hashCode和equals
        
        for (Student s : set) {
            System.out.println(s);
        }
    }
}

class Student {
    private int id;
    private String name;
    
    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return id == student.id && Objects.equals(name, student.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
    
    @Override
    public String toString() {
        return "Student{id=" + id + ", name='" + name + "'}";
    }
}

五、TreeSet

5.1 TreeSet概述

什么是TreeSet?

TreeSet是基于红黑树实现的Set集合,可以对元素进行排序。

特点
特点说明
有序按照自然顺序或自定义顺序排序
元素唯一不允许存储重复元素
非线程安全多线程环境下需要手动同步
底层实现基于TreeMap(红黑树)
不允许null不允许存储null(除非Comparator允许)

5.2 自然排序

import java.util.TreeSet;

public class TreeSetNaturalOrder {
    public static void main(String[] args) {
        TreeSet<Integer> set = new TreeSet<>();
        
        // 添加元素
        set.add(5);
        set.add(2);
        set.add(8);
        set.add(1);
        set.add(9);
        
        System.out.println("TreeSet: " + set);
        System.out.println("first: " + set.first());
        System.out.println("last: " + set.last());
        System.out.println("lower(5): " + set.lower(5));
        System.out.println("higher(5): " + set.higher(5));
        
        // 遍历(有序)
        System.out.println("\n遍历:");
        for (int num : set) {
            System.out.println(num);
        }
    }
}

5.3 自定义排序

import java.util.TreeSet;

public class TreeSetCustomOrder {
    public static void main(String[] args) {
        // 方式1:使用Lambda表达式(Java 8+)
        TreeSet<String> set1 = new TreeSet<>((s1, s2) -> s2.compareTo(s1));
        set1.add("Apple");
        set1.add("Banana");
        set1.add("Cherry");
        System.out.println("降序: " + set1);
        
        // 方式2:使用Comparator接口
        TreeSet<String> set2 = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
        set2.add("apple");
        set2.add("BANANA");
        set2.add("Cherry");
        System.out.println("忽略大小写: " + set2);
        
        // 方式3:自定义Comparator
        TreeSet<Student> studentSet = new TreeSet<>(new StudentComparator());
        studentSet.add(new Student(3, "张三"));
        studentSet.add(new Student(1, "李四"));
        studentSet.add(new Student(2, "王五"));
        
        System.out.println("\n学生按年龄排序:");
        for (Student s : studentSet) {
            System.out.println(s);
        }
    }
}

// 学生类
class Student {
    private int age;
    private String name;
    
    public Student(int age, String name) {
        this.age = age;
        this.name = name;
    }
    
    public int getAge() { return age; }
    public String getName() { return name; }
    
    @Override
    public String toString() {
        return "Student{age=" + age + ", name='" + name + "'}";
    }
}

// 自定义比较器
import java.util.Comparator;
class StudentComparator implements Comparator<Student> {
    @Override
    public int compare(Student s1, Student s2) {
        return s1.getAge() - s2.getAge();
    }
}

六、LinkedHashSet

6.1 LinkedHashSet概述

什么是LinkedHashSet?

LinkedHashSet是HashSet的子类,基于LinkedHashMap实现,可以维护元素的插入顺序。

特点
特点说明
有序保持元素的插入顺序
元素唯一不允许存储重复元素
允许null允许存储一个null元素
底层实现基于LinkedHashMap

6.2 使用示例

import java.util.LinkedHashSet;

public class LinkedHashSetDemo {
    public static void main(String[] args) {
        LinkedHashSet<String> set = new LinkedHashSet<>();
        
        set.add("Java");
        set.add("Python");
        set.add("C++");
        set.add("Java");  // 重复,不会添加
        set.add("Go");
        
        System.out.println("LinkedHashSet: " + set);
        System.out.println("保持插入顺序: " + set);
        
        // LinkedHashSet vs HashSet
        System.out.println("\n对比HashSet:");
        java.util.HashSet<String> hashSet = new java.util.HashSet<>();
        hashSet.add("Java");
        hashSet.add("Python");
        hashSet.add("C++");
        hashSet.add("Go");
        System.out.println("HashSet: " + hashSet);  // 无序
    }
}

七、各Set集合对比

7.1 对比表

对比项HashSetTreeSetLinkedHashSet
底层实现HashMapTreeMapLinkedHashMap
顺序无序有序(自然/自定义)插入顺序
查找效率O(1)O(log n)O(1)
插入删除效率O(1)O(log n)O(1)
null支持允许一个null不允许null允许一个null
线程安全

7.2 选择建议

场景推荐使用
只关心元素唯一,不关心顺序HashSet
需要对元素排序TreeSet
需要保持插入顺序LinkedHashSet
需要频繁查询HashSet
需要范围操作TreeSet

八、Collections工具类

8.1 排序操作

import java.util.*;

public class CollectionsSortDemo {
    public static void main(String[] args) {
        // 1. sort(List<T> list):自然排序
        List<Integer> list1 = new ArrayList<>();
        list1.add(5);
        list1.add(2);
        list1.add(8);
        list1.add(1);
        
        Collections.sort(list1);
        System.out.println("sort: " + list1);
        
        // 2. sort(List<T> list, Comparator<? super T> c):自定义排序
        List<String> list2 = new ArrayList<>();
        list2.add("Banana");
        list2.add("Apple");
        list2.add("Cherry");
        
        Collections.sort(list2, Collections.reverseOrder());
        System.out.println("降序: " + list2);
        
        // 3. reverse(List<?> list):反转
        Collections.reverse(list1);
        System.out.println("reverse: " + list1);
        
        // 4. shuffle(List<?> list):随机打乱
        Collections.shuffle(list1);
        System.out.println("shuffle: " + list1);
        
        // 5. swap(List<?> list, int i, int j):交换
        Collections.swap(list1, 0, list1.size() - 1);
        System.out.println("swap: " + list1);
    }
}

8.2 查找操作

import java.util.*;

public class CollectionsSearchDemo {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 3, 5, 7, 9, 11);
        
        // 1. binarySearch(List<?> list, T key):二分查找(必须先排序)
        int index = Collections.binarySearch(list, 7);
        System.out.println("binarySearch(7): " + index);
        
        // 2. max(Collection<?> coll):获取最大元素
        System.out.println("max: " + Collections.max(list));
        
        // 3. min(Collection<?> coll):获取最小元素
        System.out.println("min: " + Collections.min(list));
        
        // 4. frequency(Collection<?> c, Object o):统计出现次数
        List<String> list2 = Arrays.asList("A", "B", "A", "C", "A");
        System.out.println("frequency(A): " + Collections.frequency(list2, "A"));
    }
}

8.3 同步控制

import java.util.*;

public class CollectionsSyncDemo {
    public static void main(String[] args) {
        // 1. synchronizedList:创建线程安全的List
        List<String> syncList = Collections.synchronizedList(new ArrayList<>());
        
        // 2. synchronizedSet:创建线程安全的Set
        Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
        
        // 3. synchronizedMap:创建线程安全的Map
        Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
        
        // 注意:迭代时需要加锁
        synchronized (syncList) {
            for (String s : syncList) {
                System.out.println(s);
            }
        }
    }
}

8.4 不可变集合

import java.util.*;

public class CollectionsImmutableDemo {
    public static void main(String[] args) {
        // 1. emptyList():空List
        List<String> emptyList = Collections.emptyList();
        // emptyList.add("test");  // UnsupportedOperationException
        
        // 2. singletonList(T o):单元素List
        List<String> singletonList = Collections.singletonList("唯一");
        System.out.println("singletonList: " + singletonList);
        
        // 3. unmodifiableList(List<? extends T> list):不可变List
        List<String> modifiable = new ArrayList<>();
        modifiable.add("A");
        List<String> unmodifiable = Collections.unmodifiableList(modifiable);
        // unmodifiable.add("B");  // UnsupportedOperationException
        
        // 4. Map的不可变集合
        Map<String, Integer> singletonMap = Collections.singletonMap("key", 1);
        Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(new HashMap<>());
    }
}

九、实践案例

9.1 数组去重

import java.util.*;

public class ArrayDeduplication {
    public static void main(String[] args) {
        // 方法1:使用HashSet去重
        Integer[] arr = {1, 2, 3, 2, 4, 3, 5, 1, 6, 5};
        
        Set<Integer> set = new HashSet<>(Arrays.asList(arr));
        System.out.println("去重后: " + set);
        
        // 方法2:保持顺序的去重
        LinkedHashSet<Integer> linkedSet = new LinkedHashSet<>(Arrays.asList(arr));
        System.out.println("保持顺序去重: " + linkedSet);
        
        // 方法3:统计重复元素
        Map<Integer, Integer> countMap = new HashMap<>();
        for (Integer num : arr) {
            countMap.merge(num, 1, Integer::sum);
        }
        System.out.println("元素统计: " + countMap);
    }
}

9.2 找出两个数组的交集和并集

import java.util.*;

public class SetOperations {
    public static void main(String[] args) {
        Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));
        Set<Integer> set2 = new HashSet<>(Arrays.asList(4, 5, 6, 7, 8));
        
        // 并集
        Set<Integer> union = new HashSet<>(set1);
        union.addAll(set2);
        System.out.println("并集: " + union);
        
        // 交集
        Set<Integer> intersection = new HashSet<>(set1);
        intersection.retainAll(set2);
        System.out.println("交集: " + intersection);
        
        // 差集(set1 - set2)
        Set<Integer> difference = new HashSet<>(set1);
        difference.removeAll(set2);
        System.out.println("差集(set1-set2): " + difference);
        
        // 对称差集(set1 ∪ set2 - set1 ∩ set2)
        Set<Integer> symmetricDifference = new HashSet<>(union);
        symmetricDifference.removeAll(intersection);
        System.out.println("对称差集: " + symmetricDifference);
    }
}

9.3 成绩统计系统

import java.util.*;

public class ScoreStatistics {
    public static void main(String[] args) {
        // 准备数据
        List<Integer> scores = Arrays.asList(
            85, 92, 78, 95, 88, 76, 91, 83, 90, 72,
            88, 95, 81, 77, 89, 94, 86, 79, 92, 85
        );
        
        // 使用TreeSet进行统计
        TreeSet<Integer> scoreSet = new TreeSet<>(scores);
        
        System.out.println("========== 成绩统计 ==========");
        System.out.println("学生人数: " + scores.size());
        System.out.println("最高分: " + scoreSet.last());
        System.out.println("最低分: " + scoreSet.first());
        System.out.println("平均分: " + 
            String.format("%.2f", scores.stream()
                .mapToInt(Integer::intValue).average().orElse(0)));
        
        // 分数段统计
        System.out.println("\n分数段统计:");
        System.out.println("90-100分: " + scoreSet.subSet(90, 101).size() + "人");
        System.out.println("80-89分: " + scoreSet.subSet(80, 90).size() + "人");
        System.out.println("70-79分: " + scoreSet.subSet(70, 80).size() + "人");
        System.out.println("60-69分: " + scoreSet.subSet(60, 70).size() + "人");
        System.out.println("60分以下: " + scoreSet.headSet(60).size() + "人");
        
        // 及格率
        long passCount = scores.stream().filter(s -> s >= 60).count();
        System.out.println("\n及格率: " + 
            String.format("%.2f%%", (double) passCount / scores.size() * 100));
    }
}

十、常见问题与注意事项

10.1 HashSet vs HashMap

public class HashSetVsHashMap {
    public static void main(String[] args) {
        // HashSet内部使用HashMap存储
        // key: 元素值
        // value: 固定的PRESENT对象
        
        HashSet<String> set = new HashSet<>();
        set.add("Java");
        set.add("Python");
        
        // 实际上等价于
        HashMap<String, Object> map = new HashMap<>();
        map.put("Java", new Object());
        map.put("Python", new Object());
        
        // 不能像Map一样使用get方法获取元素
        // set.get("Java");  // 错误,没有这个方法
        
        // 应该使用contains方法
        System.out.println("contains(\"Java\"): " + set.contains("Java"));
    }
}

10.2 TreeSet元素要求

public class TreeSetRequirement {
    public static void main(String[] args) {
        TreeSet<Student> set = new TreeSet<>();
        
        // 错误:Student类没有实现Comparable接口
        // set.add(new Student(1, "张三"));
        
        // 解决方案1:让Student实现Comparable接口
        // 解决方案2:创建TreeSet时传入Comparator
        TreeSet<Student> set2 = new TreeSet<>((s1, s2) -> s1.getAge() - s2.getAge());
        set2.add(new Student2(20, "张三"));
        set2.add(new Student2(18, "李四"));
        
        System.out.println(set2);
    }
}

class Student2 {
    private int age;
    private String name;
    
    public Student2(int age, String name) {
        this.age = age;
        this.name = name;
    }
    
    public int getAge() { return age; }
    
    @Override
    public String toString() {
        return "Student{age=" + age + ", name='" + name + "'}";
    }
}

十一、课后作业

必做题

  1. 数组去重练习

    • 使用HashSet对数组去重
    • 使用LinkedHashSet保持顺序去重
    • 统计重复元素及其次数
  2. Set集合操作

    • 实现交集、并集、差集
    • 实现对称差集
  3. TreeSet排序练习

    • 创建自定义对象TreeSet
    • 实现自然排序和自定义排序

选做题

  1. 通讯录去重

    • 使用HashSet对联系人去重
    • 保持插入顺序
  2. 成绩排名系统

    • 使用TreeSet实现成绩排名
    • 支持查询前N名、后N名

十二、学习总结

今日要点

  1. HashSet

    • 基于HashMap实现
    • 元素唯一,无序
    • 通过hashCode和equals去重
  2. TreeSet

    • 基于TreeMap实现(红黑树)
    • 元素唯一,有序
    • 支持自然排序和自定义排序
  3. LinkedHashSet

    • 基于LinkedHashMap实现
    • 元素唯一,保持插入顺序
  4. Collections工具类

    • 排序、查找、同步控制、不可变集合

选择建议

  • 需要唯一性 → HashSet
  • 需要排序 → TreeSet
  • 需要顺序 → LinkedHashSet

下一课预告:泛型机制

学习时间:建议2-3小时(理论1小时 + 实践1-2小时)

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐