【JavaSE全面教学】Java集合框架下Day13(2026年)

写在前面:这是JavaSE系列的第13篇。上一篇讲了List家族,今天来讲Set和Map。HashMap是面试中问得最多的集合类,底层原理必须搞懂。建议收藏,反复看。

文章目录

    • 一、Set集合:不可重复
      • [1.1 Set的特点](#1.1 Set的特点)
      • [1.2 HashSet](#1.2 HashSet)
      • [1.3 LinkedHashSet](#1.3 LinkedHashSet)
      • [1.4 TreeSet](#1.4 TreeSet)
      • [1.5 Set对比](#1.5 Set对比)
    • 二、Map集合:键值对
      • [2.1 Map的基本使用](#2.1 Map的基本使用)
      • [2.2 HashMap的常用方法](#2.2 HashMap的常用方法)
      • [2.3 HashMap的底层原理(面试重点)](#2.3 HashMap的底层原理(面试重点))
      • [2.4 HashMap的面试高频问题](#2.4 HashMap的面试高频问题)
      • [2.5 LinkedHashMap](#2.5 LinkedHashMap)
      • [2.6 TreeMap](#2.6 TreeMap)
      • [2.7 Map对比](#2.7 Map对比)
    • 参考资料
    • 三、总结

一、Set集合:不可重复

1.1 Set的特点

复制代码
Set:无序、不可重复
├── HashSet      → 基于HashMap,无序
├── LinkedHashSet → 基于LinkedHashMap,保持插入顺序
└── TreeSet      → 基于TreeMap,自动排序

1.2 HashSet

实际场景:在开发登录注册功能时,需要判断用户名是否已存在。用List需要遍历查找,效率低;用HashSet只需O(1)就能判断。

java 复制代码
import java.util.HashSet;
import java.util.Set;

Set<String> set = new HashSet<>();

// 添加元素(自动去重)
set.add("Java");
set.add("Python");
set.add("Java");  // 重复,不会添加
set.add("C++");
System.out.println(set);  // [Java, C++, Python](顺序不固定)

// 常用方法
set.size();           // 3
set.contains("Java"); // true
set.remove("Python"); // 删除
set.isEmpty();         // false
set.clear();          // 清空

// 遍历(和List一样)
for (String s : set) {
    System.out.println(s);
}

set.forEach(s -> System.out.println(s));

HashSet去重原理

踩坑提醒:自定义对象存入HashSet时,如果不重写hashCode和equals,去重会失效!因为默认的hashCode是对象地址,每个new出来的对象地址都不同。

java 复制代码
// HashSet底层是HashMap
// 添加元素时,先计算hashCode
// 如果hashCode相同,再调用equals比较
// hashCode和equals都相同,才认为是重复元素

class Student {
    String name;
    int age;
    
    // 必须重写hashCode和equals才能正确去重
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

Set<Student> set = new HashSet<>();
set.add(new Student("张三", 20));
set.add(new Student("张三", 20));  // 重复,不会添加
set.add(new Student("李四", 21));
System.out.println(set.size());  // 2

1.3 LinkedHashSet

java 复制代码
// LinkedHashSet:保持插入顺序
Set<String> set = new LinkedHashSet<>();
set.add("C");
set.add("A");
set.add("B");
System.out.println(set);  // [C, A, B](按插入顺序)

1.4 TreeSet

java 复制代码
import java.util.TreeSet;

// TreeSet:自动排序(自然排序)
TreeSet<Integer> set = new TreeSet<>();
set.add(30);
set.add(10);
set.add(20);
set.add(10);  // 重复,不添加
System.out.println(set);  // [10, 20, 30](自动排序)

// 自定义排序
TreeSet<String> set2 = new TreeSet<>(Comparator.reverseOrder());
set2.add("A");
set2.add("C");
set2.add("B");
System.out.println(set2);  // [C, B, A](降序)

// TreeSet的特有方法
TreeSet<Integer> nums = new TreeSet<>(Arrays.asList(1, 3, 5, 7, 9));
nums.first();    // 1(最小值)
nums.last();     // 9(最大值)
nums.lower(5);   // 3(严格小于5的最大值)
nums.floor(5);   // 5(小于等于5的最大值)
nums.higher(5);  // 7(严格大于5的最小值)
nums.ceiling(5); // 5(大于等于5的最小值)
nums.subSet(3, 7);    // [3, 5](3到7之间,不含7)
nums.headSet(5);      // [1, 3](小于5)
nums.tailSet(5);      // [5, 7, 9](大于等于5)

1.5 Set对比

特性 HashSet LinkedHashSet TreeSet
底层 HashMap LinkedHashMap TreeMap
有序性 无序 插入顺序 排序
null值 允许 允许 不允许
性能 O(1) O(1) O(log n)
去重依据 hashCode+equals hashCode+equals compareTo

二、Map集合:键值对

2.1 Map的基本使用

java 复制代码
import java.util.HashMap;
import java.util.Map;

Map<String, Integer> map = new HashMap<>();

// 添加键值对
map.put("Java", 1);
map.put("Python", 2);
map.put("C++", 3);
map.put("Java", 10);  // key重复,value覆盖

// 获取值
Integer value = map.get("Java");  // 10
Integer def = map.get("Go");      // null(key不存在)

// 安全获取
Integer v2 = map.getOrDefault("Go", 0);  // 0(key不存在返回默认值)

// 判断
map.containsKey("Java");  // true
map.containsValue(1);      // false(1已被覆盖为10)

// 删除
map.remove("C++");  // 删除指定key

// 遍历
// 方式1:entrySet(推荐)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + " → " + entry.getValue());
}

// 方式2:keySet
for (String key : map.keySet()) {
    System.out.println(key + " → " + map.get(key));
}

// 方式3:forEach(Java 8+)
map.forEach((key, val) -> System.out.println(key + " → " + val));

// 方式4:values
for (Integer v : map.values()) {
    System.out.println(v);
}

// 大小
map.size();       // 2
map.isEmpty();     // false
map.clear();       // 清空

2.2 HashMap的常用方法

java 复制代码
Map<String, Integer> map = new HashMap<>();

// putIfAbsent:key不存在才放入
map.putIfAbsent("Java", 1);  // 放入
map.putIfAbsent("Java", 10); // 不放入(key已存在)

// computeIfAbsent:key不存在才计算
map.computeIfAbsent("Python", k -> k.length());  // Python长度6

// computeIfPresent:key存在才计算
map.computeIfPresent("Java", (k, v) -> v + 1);  // Java的value+1

// merge:合并值
map.merge("Java", 1, Integer::sum);  // Java的value+1

// replace:替换
map.replace("Java", 100);  // 替换Java的value为100

// getOrDefault
map.getOrDefault("Go", 0);  // Go不存在,返回0

2.3 HashMap的底层原理(面试重点)

面试必问:HashMap的底层原理几乎是Java面试的"送分题",但很多人答不全。下面从数据结构、put过程、扩容机制三个维度彻底讲透。

java 复制代码
// HashMap底层:数组 + 链表 + 红黑树(JDK 8+)

// 数据结构
// 数组的每个位置叫一个"桶"(bucket)
// 每个桶可以存一个链表或红黑树

transient Node<K,V>[] table;  // 哈希桶数组

static class Node<K,V> {
    final int hash;     // 哈希值
    final K key;        // 键
    V value;            // 值
    Node<K,V> next;     // 下一个节点(链表)
}

// JDK 8之前:数组 + 链表
// JDK 8之后:数组 + 链表 + 红黑树(链表长度>=8时转红黑树)

put过程图解

复制代码
1. 计算key的hashCode
2. 对hashCode进行扰动处理(减少冲突)
3. 用(n-1) & hash计算桶的位置
4. 如果桶为空,直接放入
5. 如果桶不为空:
   a. 如果key相同,覆盖value
   b. 如果key不同,加入链表尾部
   c. 如果链表长度>=8且数组长度>=64,转红黑树

桶数组(table)
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│  0  │  1  │  2  │  3  │  4  │  5  │  6  │  7  │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│null │ [A] │null │ [B] │null │ [C] │null │null │
│     │  ↓  │     │  ↓  │     │  ↓  │     │     │
│     │ [D] │     │ [E] │     │ [F] │     │     │
│     │  ↓  │     │     │     │  ↓  │     │     │
│     │ [G] │     │     │     │ [H] │     │     │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

扩容机制

经验之谈 :如果知道数据量大小,一定要在创建HashMap时指定初始容量,避免频繁扩容影响性能。比如要存1000个元素,可以new HashMap<>(2048)(2的幂次方)。

java 复制代码
// 默认初始容量:16
// 默认负载因子:0.75
// 扩容阈值:16 * 0.75 = 12

// 当元素数量超过阈值时,扩容为原来的2倍
// 16 → 32 → 64 → 128 → ...

// 为什么负载因子是0.75?
// 太小:空间浪费
// 太大:哈希冲突增多,查询效率降低
// 0.75是时间和空间的平衡点

踩坑提醒:HashMap扩容时需要重新计算所有元素的哈希位置(rehash),这是一个非常耗时的操作。在性能敏感的场景下,避免HashMap频繁扩容。

2.4 HashMap的面试高频问题

问题1:hashCode和equals的关系?

java 复制代码
// 1. 两个对象equals为true,hashCode必须相同
// 2. 两个对象hashCode相同,equals不一定为true(哈希冲突)
// 3. 重写equals必须重写hashCode

// 正确的hashCode实现
@Override
public int hashCode() {
    return Objects.hash(name, age);
}

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return age == student.age && Objects.equals(name, student.name);
}

问题2:HashMap为什么线程不安全?

java 复制代码
// JDK 7:多线程扩容时可能产生环形链表,导致死循环
// JDK 8:多线程put可能导致数据丢失
// 解决方案:
// 1. ConcurrentHashMap(推荐)
// 2. Collections.synchronizedMap()
// 3. Hashtable(不推荐,锁粒度太大)

经验之谈:实际开发中,如果需要线程安全的Map,直接用ConcurrentHashMap。它使用分段锁(JDK 7)或CAS+synchronized(JDK 8),性能远优于Hashtable。

问题3:HashMap的key可以为null吗?

java 复制代码
// HashMap:key可以为null(只有一个,放在桶0)
// TreeMap:key不能为null(需要比较)
// ConcurrentHashMap:key和value都不能为null

2.5 LinkedHashMap

实际应用:LinkedHashMap的访问顺序模式是实现LRU缓存的基础。很多缓存框架(如Guava Cache、LruCache)都是基于它实现的。

java 复制代码
// LinkedHashMap:保持插入顺序
Map<String, Integer> map = new LinkedHashMap<>();
map.put("C", 3);
map.put("A", 1);
map.put("B", 2);
System.out.println(map);  // {C=3, A=1, B=2}(按插入顺序)

// 访问顺序(LRU缓存)
Map<String, Integer> lru = new LinkedHashMap<>(16, 0.75f, true);
lru.put("A", 1);
lru.put("B", 2);
lru.get("A");  // 访问A,A移到最后
lru.put("C", 3);
System.out.println(lru.keySet());  // [B, A, C](A被访问过,移到后面)

2.6 TreeMap

java 复制代码
// TreeMap:按key排序
TreeMap<String, Integer> map = new TreeMap<>();
map.put("C", 3);
map.put("A", 1);
map.put("B", 2);
System.out.println(map);  // {A=1, B=2, C=3}(按key排序)

// 自定义排序
TreeMap<String, Integer> map2 = new TreeMap<>(Comparator.reverseOrder());
map2.put("C", 3);
map2.put("A", 1);
System.out.println(map2);  // {C=3, A=1}(降序)

// TreeMap的特有方法
TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(1, "A");
treeMap.put(3, "C");
treeMap.put(5, "E");
treeMap.firstKey();    // 1
treeMap.lastKey();     // 5
treeMap.lowerKey(3);   // 1
treeMap.higherKey(3);  // 5
treeMap.subMap(1, 5);  // {1=A, 3=C}

2.7 Map对比

特性 HashMap LinkedHashMap TreeMap Hashtable
底层 数组+链表+红黑树 数组+链表+红黑树 红黑树 数组+链表
有序性 无序 插入/访问顺序 按key排序 无序
null key 允许1个 允许1个 不允许 不允许
null value 允许 允许 不允许 不允许
线程安全 不安全 不安全 不安全 安全
性能 O(1) O(1) O(log n) O(1)

参考资料

  1. Oracle官方文档 - Map接口
  2. 美团技术团队 - Java 8系列之重新认识HashMap

三、总结

今天我们学习了:

  • ✅ Set集合的使用和去重原理
  • ✅ Map集合的使用
  • ✅ HashMap的底层原理(数组+链表+红黑树)
  • ✅ HashMap的扩容机制
  • ✅ LinkedHashMap和TreeMap的特点

重点记忆

  1. HashSet去重依赖hashCode和equals
  2. HashMap底层是数组+链表+红黑树
  3. 负载因子0.75,扩容为2倍
  4. 重写equals必须重写hashCode
  5. ConcurrentHashMap是线程安全的HashMap

下一步预告

Day14我们将学习IO流与文件操作------字节流、字符流、缓冲流、序列化等。


互动话题:HashMap是面试必问题,你被问过哪些HashMap的面试题?欢迎在评论区分享!

如果这篇文章对你有帮助,欢迎点赞、收藏 !这是【JavaSE全面教学】系列的第13篇,关注我看完整套教程 👇


本文为【JavaSE全面教学】系列第13篇,持续更新中...

相关推荐
吃好睡好便好2 小时前
用if…end…语句计算分段函数
开发语言·人工智能·学习·算法·matlab
vx-程序开发2 小时前
基于机器学习的动漫可视化系统的设计与实现-计算机毕业设计源码08339
java·c++·spring boot·python·spring·django·php
风继续吹..2 小时前
C# 文件 IO 实操练习题 5道
开发语言·c#
LCG元3 小时前
RAG工程指南:从基础检索到生产部署全解析
java·运维·数据库
Dust-Chasing3 小时前
Claude Code源码剖析 - Phase3
开发语言·人工智能·学习
石榴树下的七彩鱼3 小时前
医疗票据 OCR 识别 API 多场景落地指南:医保结算 + 商保理赔 + 医疗信息化(附 Python/Java 完整示例)
java·python·ocr·石榴智能·医疗票据ocr·医保结算·ocrapi
C137的本贾尼3 小时前
Spring AI Alibaba 开箱:国产百炼大模型初体验
java·人工智能·spring
XS0301063 小时前
并发编程三
开发语言·c#
idingzhi3 小时前
A股量化策略日报(2026年05月22日)
android·开发语言·python·kotlin