Forráskód Böngészése

feat: 新增20260731课堂练习题——Iterator迭代器与Map集合专项练习(基础3题+进阶2题,含参考答案)

WanJL 3 hete
szülő
commit
39e4c89115
2 módosított fájl, 558 hozzáadás és 0 törlés
  1. 1 0
      README.md
  2. 557 0
      课堂练习题/20260731-迭代器与Map集合练习.md

+ 1 - 0
README.md

@@ -76,3 +76,4 @@
 | 2026-07-28 | [异常处理练习](./课堂练习题/20260728-异常处理练习.md) | 异常概念与 Throwable 体系、try-catch 多 catch 块捕获(ArrayIndexOutOfBoundsException / NullPointerException / ClassCastException / ArithmeticException)、多 catch 块顺序、throws 抛出、程序鲁棒性、自定义异常(继承 Exception)、异常处理在业务逻辑中的应用(用户注册验证 + 银行 ATM 取款) |
 | 2026-07-29 | [集合框架练习](./课堂练习题/20260729-集合框架练习.md) | ArrayList 基本操作(增删改查)、ArrayList 底层原理、包装类在集合中的应用、ArrayList 嵌套二维结构、冒泡排序、contains()/indexOf() 方法 |
 | 2026-07-30 | [LinkedList 双链表练习](./课堂练习题/20260730-LinkedList双链表练习.md) | LinkedList 作为 List 的基本操作、Deque 双端队列操作、Queue 队列 FIFO 操作、LinkedList vs ArrayList 性能对比实验、手写单链表增删改查 |
+| 2026-07-31 | [Iterator迭代器与Map集合练习](./课堂练习题/20260731-迭代器与Map集合练习.md) | Iterator 迭代器遍历(for vs 迭代器,hasNext/next)、HashMap 键值对基本操作(put/get/containsKey/containsValue/size/remove/clear)、Map 两种遍历方式(keySet+get / entrySet+getKey/getValue)、自定义类型作为 Key(重写 hashCode/equals)、HashMap 底层原理应用、单词词频统计 |

+ 557 - 0
课堂练习题/20260731-迭代器与Map集合练习.md

@@ -0,0 +1,557 @@
+# 2026-07-31 Iterator 迭代器与 Map 集合专项练习(课堂练习用)
+
+> **说明**:以下习题围绕今日授课核心内容——**Iterator 迭代器** 与 **Map 集合(双列集合)** 展开,涵盖 for 循环与迭代器遍历对比、Map 键值对基本操作、Map 两种遍历方式(keySet+get / entrySet)、自定义类型作为 HashMap 的 Key(重写 hashCode/equals)、HashMap 底层原理应用等知识点。
+>
+> 题目分为 **基础练习**(迭代器与 Map 基本操作,3 题)和 **进阶挑战**(Map 综合应用,2 题)。
+
+---
+
+# 第一部分:基础练习
+
+> 以下练习围绕今日授课核心内容展开,由浅入深。
+
+---
+
+## 练习 1:Iterator 迭代器遍历(for 循环 vs 迭代器)
+
+**难度**:⭐
+**知识点**:`Collection` 继承 `Iterable`、`iterator()` / `hasNext()` / `next()`、for+get 遍历与迭代器遍历对比
+
+**场景描述**:所有单列集合都继承自 `Iterable` 接口,都具备迭代能力。通过迭代器,可以**统一**地遍历 ArrayList 和 LinkedList,且与集合的内部结构无关。
+
+**题目要求**:
+
+在 `IteratorDemo` 类的 `main` 方法中,完成以下操作:
+
+```java
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+public class IteratorDemo {
+    public static void main(String[] args) {
+        // 1. 创建一个 ArrayList<String>,添加 5 个元素:"A", "B", "C", "D", "E"
+
+        // 2. 方式一:使用 for + size() + get(i) 遍历 ArrayList,并输出
+        //    提示:for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }
+
+        // 3. 方式二:通过 list.iterator() 获取迭代器对象
+        //    使用 while + hasNext() + next() 遍历,并输出
+        //    提示:Iterator<String> iterator = list.iterator();
+        //         while (iterator.hasNext()) { String element = iterator.next(); ... }
+
+        // 4. 再创建一个 LinkedList<String>,添加同样的 5 个元素
+
+        // 5. 使用迭代器遍历 LinkedList(同样的模板代码)
+        //    观察:LinkedList 没有 get(i) 高效随机访问,迭代器是它的最佳遍历方式
+    }
+}
+```
+
+**预期输出**:
+```
+========== for 循环遍历 ArrayList ==========
+A
+B
+C
+D
+E
+========== 迭代器遍历 ArrayList ==========
+A
+B
+C
+D
+E
+========== 迭代器遍历 LinkedList ==========
+A
+B
+C
+D
+E
+```
+
+**思考题**:
+1. `hasNext()` 和 `next()` 分别做什么?`next()` 调用后迭代器指针会发生什么变化?
+2. 为什么 LinkedList 用 `for + get(i)` 遍历效率低,而迭代器遍历效率高?(提示:`get(i)` 每次从头遍历,O(n²))
+3. 增强 for(foreach)的底层实现是什么?它和迭代器有什么关系?
+
+---
+
+## 练习 2:Map 集合基本操作(HashMap 键值对)
+
+**难度**:⭐⭐
+**知识点**:`HashMap` 创建、`put()` / `get()` / `containsKey()` / `containsValue()` / `size()` / `isEmpty()` / `remove()` / `clear()`、键不可重复
+
+**场景描述**:Map 是**双列集合**,以「键值对」为单位存储。键(Key)不可重复,值(Value)可以重复。模拟一个"学生信息登记表"——用学号作为键,姓名作为值。
+
+**题目要求**:
+
+在 `MapDemo` 类的 `main` 方法中,完成以下操作:
+
+```java
+import java.util.HashMap;
+import java.util.Map;
+
+public class MapDemo {
+    public static void main(String[] args) {
+        // 1. 创建一个 HashMap<String, String>,学号作为 Key,姓名作为 Value
+        //    提示:Map<String, String> map = new HashMap<>();
+
+        // 2. 登记 4 名学生:x001-张三, x002-李四, x003-王五, x004-赵六
+        //    使用 put() 方法
+
+        // 3. 输出集合大小(size()):"学生人数:4"
+
+        // 4. 根据学号查询姓名(get("x002")):"学号 x002 的学生:李四"
+
+        // 5. 判断是否包含某个键(containsKey("x001")):true
+        //    判断是否包含某个值(containsValue("王五")):true
+        //    判断是否包含不存在的键(containsKey("x999")):false
+
+        // 6. 重复键测试:put("x001", "张三丰")
+        //    再次输出"学号 x001 的学生":观察发生了什么?
+        //    —— 提示:同一个 Key 再次 put 会【覆盖】旧值,键不可重复
+
+        // 7. 移除一个学生(remove("x004")),输出移除后的集合大小
+
+        // 8. 判断集合是否为空(isEmpty()):false
+
+        // 9. 清空集合(clear()),再次判断 isEmpty():true
+    }
+}
+```
+
+**预期输出**:
+```
+学生人数:4
+学号 x002 的学生:李四
+包含键 x001:true
+包含值 王五:true
+包含键 x999:false
+重复键 put 后,学号 x001 的学生:张三丰   ← 旧值"张三"被覆盖
+移除 x004 后,学生人数:3
+集合为空?false
+清空后,集合为空?true
+```
+
+**思考题**:
+1. 为什么 Map 的 Key 不能重复?重复 put 会怎样?(提示:覆盖旧值)
+2. `containsKey()` 和 `containsValue()` 的底层实现有什么区别?(提示:containsKey 走哈希定位,containsValue 需遍历)
+3. 为什么 `keySet()` 返回的是 Set 而不是 List?(提示:键不可重复)
+
+---
+
+## 练习 3:Map 集合的遍历(keySet + get / entrySet)
+
+**难度**:⭐⭐
+**知识点**:`keySet()` + `get(key)`(方式一)、`entrySet()` + `getKey()` / `getValue()`(方式二)、`Map.Entry` 内部类型
+
+**场景描述**:Map 没有索引,不能像 List 那样用 `for + get(i)` 直接遍历。有两种标准遍历方式:**keySet + get**(先取键再反查值)和 **entrySet**(直接取键值对)。
+
+**题目要求**:
+
+在 `MapTraversalDemo` 类的 `main` 方法中,完成以下操作:
+
+```java
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class MapTraversalDemo {
+    public static void main(String[] args) {
+        // 1. 创建一个 HashMap<String, Double>,存储商品价格
+        //    "苹果"-3.5, "香蕉"-2.0, "西瓜"-6.8, "葡萄"-12.5
+
+        // 2. 方式一:keySet() + foreach + get(key) 遍历
+        //    输出格式:"key:苹果,value:3.5"
+        //    步骤:
+        //    a. Set<String> keys = map.keySet();
+        //    b. for (String key : keys) { Double value = map.get(key); ... }
+
+        // 3. 方式二:entrySet() + foreach + getKey()/getValue() 遍历
+        //    输出格式:"key:苹果,value:3.5"
+        //    步骤:
+        //    a. Set<Map.Entry<String, Double>> entries = map.entrySet();
+        //    b. for (Map.Entry<String, Double> me : entries) {
+        //           String key = me.getKey(); Double value = me.getValue(); ... }
+
+        // 4. 统计商品总价(遍历两种方式均可),输出:"商品总价:24.8"
+    }
+}
+```
+
+**预期输出**(顺序因 HashMap 无序而不定):
+```
+========== 方式一:keySet + get ==========
+key:香蕉,value:2.0
+key:苹果,value:3.5
+key:葡萄,value:12.5
+key:西瓜,value:6.8
+========== 方式二:entrySet ==========
+key:香蕉,value:2.0
+key:苹果,value:3.5
+key:葡萄,value:12.5
+key:西瓜,value:6.8
+========== 总价统计 ==========
+商品总价:24.8
+```
+
+**关键对比**:
+
+| 遍历方式 | 步骤 | 特点 |
+|----------|------|------|
+| 方式一:`keySet()` + `get(key)` | 取键集合 → 遍历键 → 反查值 | **间接**:每次 get 都是一次哈希查找 |
+| 方式二:`entrySet()` | 取键值对集合 → 遍历 → getKey/getValue | **直接**:一次拿到键和值,效率更高 |
+
+**思考题**:
+1. 方式二为什么比方式一高效?(提示:方式一每次 `get(key)` 都要重新哈希查找)
+2. `Map.Entry<String, Double>` 是什么类型?它和 Map 是什么关系?(提示:Map 的内部类型)
+3. 两次遍历的输出顺序是否与 put 的顺序一致?为什么?(提示:HashMap 无序)
+
+---
+
+# 第二部分:进阶挑战
+
+> 以下练习综合运用 Map 集合、哈希表原理、自定义类型等知识。
+
+---
+
+## 进阶 1:自定义类作为 HashMap 的 Key(重写 hashCode / equals)
+
+**难度**:⭐⭐⭐
+**知识点**:HashMap 依赖 `hashCode()` 和 `equals()` 保证键唯一、自定义类型作为 Key、哈希表结构(数组+链表)
+
+**场景描述**:HashMap 的键 Key 的唯一性依赖 `hashCode()` 和 `equals()` 两个方法。**自定义类型**想存入 HashMap 的 Key 中,**必须重写**这两个方法,否则两个属性相同的对象会被当作不同的键。
+
+**题目要求**:
+
+定义 `Student` 类(学号 + 姓名),作为 HashMap 的 Key,并重写 `hashCode()` 和 `equals()`:
+
+```java
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+public class Student {
+    private String id;    // 学号
+    private String name;  // 姓名
+
+    public Student(String id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+
+    // TODO: 补全 getter / setter 方法
+
+    // TODO: 重写 hashCode()
+    //   规则:属性值完全相同时,必须返回相同的哈希值
+    //   提示:return Objects.hash(id, name);
+
+    // TODO: 重写 equals(Object o)
+    //   规则:id 和 name 都相同时返回 true
+    //   提示:使用 Objects.equals(id, student.id) 判断
+
+    @Override
+    public String toString() {
+        return "Student{id='" + id + "', name='" + name + "'}";
+    }
+}
+```
+
+编写 `StudentKeyTest` 测试类:
+
+```java
+import java.util.HashMap;
+import java.util.Map;
+
+public class StudentKeyTest {
+    public static void main(String[] args) {
+        Map<Student, String> map = new HashMap<>();
+
+        // 1. 创建两个属性完全相同的 Student 对象
+        Student s1 = new Student("x001", "张三");
+        Student s2 = new Student("x001", "张三");
+
+        // 2. 判断 s1 和 s2 是否相等(不重写 equals 时是 false)
+        System.out.println("s1.equals(s2):" + s1.equals(s2));  // 期望 true
+
+        // 3. 判断 s1 和 s2 的哈希值是否相同
+        System.out.println("s1.hashCode():" + s1.hashCode());
+        System.out.println("s2.hashCode():" + s2.hashCode());  // 期望相同
+
+        // 4. 分别用 s1、s2 作为 Key 存入
+        map.put(s1, "学员A");
+        map.put(s2, "学员B");
+
+        // 5. 思考:如果不重写 hashCode/equals,size 是几?重写后 size 是几?
+        System.out.println("map 大小:" + map.size());  // 期望 1(s1、s2 视为同一个键)
+
+        // 6. 用 get 取出:new 一个属性相同的对象 s3 去查找
+        Student s3 = new Student("x001", "张三");
+        System.out.println("用 s3 查找:" + map.get(s3));  // 期望 "学员B"
+    }
+}
+```
+
+**预期输出**:
+```
+s1.equals(s2):true
+s1.hashCode():1416729608   ← 具体数值不重要,关键是 s2 相同
+s2.hashCode():1416729608
+map 大小:1
+用 s3 查找:学员B
+```
+
+**原理分析(必答)**:
+1. 如果不重写 `hashCode()` 和 `equals()`,`map.size()` 会是多少?为什么?
+   - 答:会是 2。因为默认 `hashCode()` 基于对象地址,两个 new 出来的对象地址不同 → 哈希值不同 → 存到不同位置;`equals()` 默认用 `==` 比较地址,也判定不相等
+2. HashMap 在存 Key 时,先调用哪个方法、后调用哪个方法?
+   - 答:先计算 `hashCode()` 定位存储位置(数组下标),若位置已有元素,再用 `equals()` 比较是否相同——相同则不存(覆盖),不同则挂到链表上
+3. 为什么重写 `equals()` 时必须同步重写 `hashCode()`?(提示:哈希表的一致性契约)
+
+---
+
+## 进阶 2:Map 综合应用 —— 单词词频统计器
+
+**难度**:⭐⭐⭐⭐
+**知识点**:Map 键值对建模、`put` / `get` / `containsKey` / 遍历、HashMap 统计思想、综合应用
+
+**场景描述**:词频统计是 Map 最典型的应用场景——**单词作为 Key,出现次数作为 Value**。遍历文本时:单词没出现过就 `put(word, 1)`,出现过就 `put(word, 次数+1)`。
+
+**题目要求**:
+
+编写 `WordCountDemo` 类,完成单词词频统计:
+
+```java
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.Set;
+
+public class WordCountDemo {
+    public static void main(String[] args) {
+        // 1. 定义一个字符串数组,模拟一段文本中的单词(含重复)
+        //    "apple", "banana", "apple", "orange", "banana", "apple", "grape"
+
+        // 2. 创建一个 HashMap<String, Integer>,单词 → 出现次数
+
+        // 3. 遍历单词数组,统计每个单词出现的次数
+        //    核心逻辑:
+        //    for (String word : words) {
+        //        if (map.containsKey(word)) {
+        //            map.put(word, map.get(word) + 1);   // 已出现过,次数 +1
+        //        } else {
+        //            map.put(word, 1);                    // 第一次出现
+        //        }
+        //    }
+
+        // 4. 使用 entrySet() 遍历输出统计结果
+        //    输出格式:"单词:apple,出现次数:3"
+
+        // 5. 进阶:找出出现次数最多的单词
+        //    提示:遍历 entrySet,用变量记录 maxCount 和对应单词
+
+        // 6. 拓展:使用 Scanner 从键盘输入一句话(用空格分隔),统计每个单词的出现次数
+        //    提示:String[] inputWords = scanner.nextLine().split(" ");
+    }
+}
+```
+
+**预期输出**:
+```
+========== 词频统计结果 ==========
+单词:grape,出现次数:1
+单词:banana,出现次数:2
+单词:orange,出现次数:1
+单词:apple,出现次数:3
+出现次数最多的单词:apple(3 次)
+```
+
+**测试用例**(Scanner 输入部分):
+```
+请输入一段英文(单词用空格分隔):I love java and I love coding
+I:2
+love:2
+java:1
+and:1
+coding:1
+```
+
+**拓展思考**:
+1. 如果把统计逻辑换成 `map.put(word, map.getOrDefault(word, 0) + 1)`,能简化代码吗?`getOrDefault` 是什么意思?
+2. 如果想按出现次数从高到低排序输出,可以怎么做?(提示:把 entrySet 转为 List 后用 Comparator 排序)
+3. 如果文本有 100 万个单词,HashMap 相比数组统计的优势在哪里?(提示:键是字符串不是数字索引,HashMap 用哈希直接定位)
+
+---
+
+# 参考答案要点
+
+## 练习 1 参考
+
+```java
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+public class IteratorDemo {
+    public static void main(String[] args) {
+        ArrayList<String> list = new ArrayList<>();
+        list.add("A");
+        list.add("B");
+        list.add("C");
+        list.add("D");
+        list.add("E");
+
+        System.out.println("========== for 循环遍历 ArrayList ==========");
+        for (int i = 0; i < list.size(); i++) {
+            System.out.println(list.get(i));
+        }
+
+        System.out.println("========== 迭代器遍历 ArrayList ==========");
+        Iterator<String> iterator = list.iterator();
+        while (iterator.hasNext()) {
+            System.out.println(iterator.next());
+        }
+
+        LinkedList<String> linkedList = new LinkedList<>();
+        linkedList.add("A");
+        linkedList.add("B");
+        linkedList.add("C");
+        linkedList.add("D");
+        linkedList.add("E");
+
+        System.out.println("========== 迭代器遍历 LinkedList ==========");
+        Iterator<String> iterator1 = linkedList.iterator();
+        while (iterator1.hasNext()) {
+            System.out.println(iterator1.next());
+        }
+    }
+}
+```
+
+## 练习 2 参考
+
+```java
+Map<String, String> map = new HashMap<>();
+map.put("x001", "张三");
+map.put("x002", "李四");
+map.put("x003", "王五");
+map.put("x004", "赵六");
+
+System.out.println("学生人数:" + map.size());
+System.out.println("学号 x002 的学生:" + map.get("x002"));
+
+System.out.println("包含键 x001:" + map.containsKey("x001"));
+System.out.println("包含值 王五:" + map.containsValue("王五"));
+System.out.println("包含键 x999:" + map.containsKey("x999"));
+
+map.put("x001", "张三丰");   // 键重复 → 覆盖旧值
+System.out.println("重复键 put 后,学号 x001 的学生:" + map.get("x001"));
+
+map.remove("x004");
+System.out.println("移除 x004 后,学生人数:" + map.size());
+
+System.out.println("集合为空?" + map.isEmpty());
+map.clear();
+System.out.println("清空后,集合为空?" + map.isEmpty());
+```
+
+## 练习 3 参考
+
+```java
+Map<String, Double> map = new HashMap<>();
+map.put("苹果", 3.5);
+map.put("香蕉", 2.0);
+map.put("西瓜", 6.8);
+map.put("葡萄", 12.5);
+
+System.out.println("========== 方式一:keySet + get ==========");
+Set<String> keys = map.keySet();
+for (String key : keys) {
+    Double value = map.get(key);
+    System.out.println("key:" + key + ",value:" + value);
+}
+
+System.out.println("========== 方式二:entrySet ==========");
+Set<Map.Entry<String, Double>> entries = map.entrySet();
+for (Map.Entry<String, Double> me : entries) {
+    System.out.println("key:" + me.getKey() + ",value:" + me.getValue());
+}
+
+// 总价统计(方式二)
+double total = 0;
+for (Map.Entry<String, Double> me : map.entrySet()) {
+    total += me.getValue();
+}
+System.out.println("========== 总价统计 ==========");
+System.out.println("商品总价:" + total);
+```
+
+## 进阶 1 参考
+
+```java
+// Student 类:重写 hashCode / equals
+@Override
+public int hashCode() {
+    return Objects.hash(id, 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 Objects.equals(id, student.id) && Objects.equals(name, student.name);
+}
+```
+
+## 进阶 2 参考(核心逻辑)
+
+```java
+String[] words = {"apple", "banana", "apple", "orange", "banana", "apple", "grape"};
+Map<String, Integer> map = new HashMap<>();
+
+for (String word : words) {
+    if (map.containsKey(word)) {
+        map.put(word, map.get(word) + 1);
+    } else {
+        map.put(word, 1);
+    }
+}
+
+// 使用 getOrDefault 简化
+for (String word : words) {
+    map.put(word, map.getOrDefault(word, 0) + 1);
+}
+
+// 找出出现次数最多的单词
+String maxWord = null;
+int maxCount = 0;
+for (Map.Entry<String, Integer> me : map.entrySet()) {
+    if (me.getValue() > maxCount) {
+        maxCount = me.getValue();
+        maxWord = me.getKey();
+    }
+}
+System.out.println("出现次数最多的单词:" + maxWord + "(" + maxCount + " 次)");
+```
+
+---
+
+# 涵盖知识点总览
+
+| 知识点 | 对应练习 | 说明 |
+|--------|---------|------|
+| Iterator 迭代器遍历 | 练习 1 | `iterator()` / `hasNext()` / `next()`、Iterable 接口 |
+| for + get 遍历 vs 迭代器 | 练习 1 | 索引遍历只适合 List,迭代器通用所有单列集合 |
+| HashMap 创建与键值对操作 | 练习 2 | `put` / `get` / `containsKey` / `containsValue` / `size` / `isEmpty` / `remove` / `clear` |
+| 键不可重复特性 | 练习 2 | 重复 put 覆盖旧值 |
+| Map 遍历方式一 | 练习 3 | `keySet()` + foreach + `get(key)`(间接遍历) |
+| Map 遍历方式二 | 练习 3 | `entrySet()` + foreach + `getKey()` / `getValue()`(直接遍历,效率更高) |
+| `Map.Entry` 内部类型 | 练习 3 | 一个 Entry 代表一个键值对 |
+| HashMap 键唯一性契约 | 进阶 1 | 依赖 `hashCode()` 定位 + `equals()` 比较,二者必须同时重写 |
+| 自定义类型作为 Key | 进阶 1 | 属性相同 → 哈希值相同 → equals 相同 → 视为同一键 |
+| 哈希表结构(数组+链表/红黑树) | 进阶 1 | 树化条件:数组长度>64 且链表节点>8;退化条件:节点<6 |
+| Map 综合统计应用 | 进阶 2 | 单词词频统计、`getOrDefault`、找最大值 |
+| HashMap 无序性 | 练习 2、3 | 存储顺序 ≠ 添加顺序(哈希定位) |
+