说明:以下习题围绕今日授课核心内容——Iterator 迭代器 与 Map 集合(双列集合) 展开,涵盖 for 循环与迭代器遍历对比、Map 键值对基本操作、Map 两种遍历方式(keySet+get / entrySet)、自定义类型作为 HashMap 的 Key(重写 hashCode/equals)、HashMap 底层原理应用等知识点。
题目分为 基础练习(迭代器与 Map 基本操作,3 题)和 进阶挑战(Map 综合应用,2 题)。
以下练习围绕今日授课核心内容展开,由浅入深。
难度:⭐
知识点:Collection 继承 Iterable、iterator() / hasNext() / next()、for+get 遍历与迭代器遍历对比
场景描述:所有单列集合都继承自 Iterable 接口,都具备迭代能力。通过迭代器,可以统一地遍历 ArrayList 和 LinkedList,且与集合的内部结构无关。
题目要求:
在 IteratorDemo 类的 main 方法中,完成以下操作:
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
思考题:
hasNext() 和 next() 分别做什么?next() 调用后迭代器指针会发生什么变化?for + get(i) 遍历效率低,而迭代器遍历效率高?(提示:get(i) 每次从头遍历,O(n²))难度:⭐⭐
知识点:HashMap 创建、put() / get() / containsKey() / containsValue() / size() / isEmpty() / remove() / clear()、键不可重复
场景描述:Map 是双列集合,以「键值对」为单位存储。键(Key)不可重复,值(Value)可以重复。模拟一个"学生信息登记表"——用学号作为键,姓名作为值。
题目要求:
在 MapDemo 类的 main 方法中,完成以下操作:
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
思考题:
containsKey() 和 containsValue() 的底层实现有什么区别?(提示:containsKey 走哈希定位,containsValue 需遍历)keySet() 返回的是 Set 而不是 List?(提示:键不可重复)难度:⭐⭐
知识点:keySet() + get(key)(方式一)、entrySet() + getKey() / getValue()(方式二)、Map.Entry 内部类型
场景描述:Map 没有索引,不能像 List 那样用 for + get(i) 直接遍历。有两种标准遍历方式:keySet + get(先取键再反查值)和 entrySet(直接取键值对)。
题目要求:
在 MapTraversalDemo 类的 main 方法中,完成以下操作:
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 | 直接:一次拿到键和值,效率更高 |
思考题:
get(key) 都要重新哈希查找)Map.Entry<String, Double> 是什么类型?它和 Map 是什么关系?(提示:Map 的内部类型)以下练习综合运用 Map 集合、哈希表原理、自定义类型等知识。
难度:⭐⭐⭐
知识点:HashMap 依赖 hashCode() 和 equals() 保证键唯一、自定义类型作为 Key、哈希表结构(数组+链表)
场景描述:HashMap 的键 Key 的唯一性依赖 hashCode() 和 equals() 两个方法。自定义类型想存入 HashMap 的 Key 中,必须重写这两个方法,否则两个属性相同的对象会被当作不同的键。
题目要求:
定义 Student 类(学号 + 姓名),作为 HashMap 的 Key,并重写 hashCode() 和 equals():
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 测试类:
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
原理分析(必答):
hashCode() 和 equals(),map.size() 会是多少?为什么?
hashCode() 基于对象地址,两个 new 出来的对象地址不同 → 哈希值不同 → 存到不同位置;equals() 默认用 == 比较地址,也判定不相等hashCode() 定位存储位置(数组下标),若位置已有元素,再用 equals() 比较是否相同——相同则不存(覆盖),不同则挂到链表上equals() 时必须同步重写 hashCode()?(提示:哈希表的一致性契约)难度:⭐⭐⭐⭐
知识点:Map 键值对建模、put / get / containsKey / 遍历、HashMap 统计思想、综合应用
场景描述:词频统计是 Map 最典型的应用场景——单词作为 Key,出现次数作为 Value。遍历文本时:单词没出现过就 put(word, 1),出现过就 put(word, 次数+1)。
题目要求:
编写 WordCountDemo 类,完成单词词频统计:
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
拓展思考:
map.put(word, map.getOrDefault(word, 0) + 1),能简化代码吗?getOrDefault 是什么意思?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());
}
}
}
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());
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);
// 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);
}
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 | 存储顺序 ≠ 添加顺序(哈希定位) |