20260731-迭代器与Map集合练习.md 19 KB

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 方法中,完成以下操作:

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 方法中,完成以下操作:

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 方法中,完成以下操作:

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():

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

原理分析(必答):

  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 类,完成单词词频统计:

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 参考

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 参考

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 参考

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 参考

// 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 参考(核心逻辑)

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 存储顺序 ≠ 添加顺序(哈希定位)