Collections 与 Map:排序、统计和购物车建模
把键的唯一性转化为业务规则,用 Map 完成计数和嵌套结构。
继续上次小节 →Java 进阶线 · 线路目录
Java 进阶线
对象与类型设计3 站
文件、配置与并发4 站
网络、框架与复习3 站
本页导读
day07|java.util 包下的常用 API:集合(Collections、Map、Stream 流)
本日接着 day06 的集合往下走,由两块内容组成。第一块是 Collection 的补充知识:可变参数(让方法灵活接收个数不定的数据)、Collections 工具类(批量添加、打乱、排序)、以及把这三样东西串起来用的综合案例(斗地主)。第二块是 Map 双列集合:为什么需要”键值对”这种数据结构、Map 的常用方法与三种遍历方式、HashMap / LinkedHashMap / TreeMap 三种实现各自的特点与底层原理、最后用”集合的嵌套”表达一对多关系。学完后应能回答:Collections.sort() 的两种排序规则分别怎么写、Map 为什么不能直接用增强 for 遍历、自定义对象做 HashMap 的键为什么必须重写 hashCode() 与 equals()、TreeMap 排序排的是键还是值。
本日 PPT 目录(第 2 页 / 第 10 页):Collection 的其他相关知识(前置知识:可变参数 → Collections → 综合案例)→ Map 集合(概述 → 常用方法 → 遍历方式 → HashMap → LinkedHashMap → TreeMap → 补充知识:集合的嵌套)。
四点说明(供复习时核对):
- 课件第 1 页标题是「集合进阶(二)」,全篇共 56 页;本日 PPT 讲的就是
Collection的补充部分与整个Map体系。 - 课件文件名里写了「Stream 流」,但本日 56 页幻灯片逐页核对后没有任何 Stream 内容(目录页第 2 页也只有上面两块)。Stream 流实际在 day08 课件《java.io包下的常用API-io流》第 1–12 页,讲义归入 day08 的文章,本文不臆造,详见第 12 章。
- 课件的「常用方法」页在抽取文本中只留下
[表格]占位、没有表体。本文中的表格是按课件表格的原文还原的(含方法签名与说明文字)。 - 图片核对过程:候选图 25 张逐张打开查看后,只有第 9 页斗地主游戏界面与第 13 页电商购物车截图带有场景信息;其余分别是卡通人物/吉祥物/表情包(第 4、12、13、29、43、55 页)、纯装饰图标(第 20 页点赞手势、第 27 页对勾、第 43 页铅笔、“划重点”贴图),以及第 11 / 24 / 30 / 41 / 53 页的大字标题条(如”单列集合""Map 集合的遍历方式""集合的嵌套”)。按规范中”丢弃封面/章节分割页大字背景、卡通、表情包、纯装饰图标”的要求,本文只内嵌那两张场景截图;课件其余知识点均以文字/代码呈现(课堂演示代码在 IDE 中现场敲写,课件未配代码截图)。
1. 前置知识:可变参数
1.1 什么是可变参数
课件第 3 页的定义:可变参数就是一种特殊形参,定义在方法、构造器的形参列表里,格式是 数据类型... 参数名称;。
| 维度 | 内容 |
|---|---|
| 特点 | 可以不传数据给它;可以传一个或者同时传多个数据给它;也可以传一个数组给它 |
| 好处 | 常常用来灵活的接收数据 |
| 注意事项 1 | 可变参数在方法内部就是一个数组 |
| 注意事项 2 | 一个形参列表中可变参数只能有一个 |
| 注意事项 3 | 可变参数必须放在形参列表的最后面 |
课件第 4 页给的需求正是它的典型用法:定义一个求和方法,让它能算 1 个、2 个、3 个……n 个数据的和,甚至可以一个参数都不传就调用。如果不用可变参数,要么重载一堆方法,要么让调用方每次都自己 new 一个数组。
1.2 代码示例
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
// 目标:掌握可变参数的使用。
// 需求:求任意个整数数据的和。
sum(); // 可以不传参数
sum(10); // 可以传一个参数
sum(20, 15, 32, 100); // 可以传多个参数
sum(new int[]{10, 20, 30, 40}); // 也可以直接传一个数组
}
// 作用:接收数据非常灵活
// 注意事项:可变参数在形参列表中只能出现一个,且必须放到形参列表的最后面
public static void sum(int... nums) {
// 本质:可变参数在方法内部就是一个数组
System.out.println("个数:" + nums.length);
System.out.println("内容:" + Arrays.toString(nums));
}
}
2. Collections 工具类
2.1 定位
课件第 6 页:Collections 是一个用来操作集合的工具类,提供的是静态方法,所以直接用 Collections.方法名(...) 调用——注意它和接口 Collection 只差一个字母,前者是工具类,后者是单列集合的根接口。
2.2 常用静态方法(课件第 6 页)
| 方法名称 | 说明 |
|---|---|
public static <T> boolean addAll(Collection<? super T> c, T... elements) | 给集合批量添加元素 |
public static void shuffle(List<?> list) | 打乱 List 集合中的元素顺序 |
public static <T> void sort(List<T> list) | 对 List 集合中的元素进行升序排序 |
public static <T> void sort(List<T> list, Comparator<? super T> c) | 对 List 集合中元素,按照比较器对象指定的规则进行排序 |
第 6 页同时强调了一句重要限制:Collections 只能支持对 List 集合进行排序(sort、shuffle 的形参就是 List,Set 集合没有索引、本身又可能无序,排不了)。作业一第 2 题(双色球)就利用了这个限制:先把 HashSet 里的号码倒进 ArrayList,再用 Collections.sort() 排序。
2.3 两种排序方式(课件第 7 页)
排序方式 1:默认规则
| 方法名称 | 说明 |
|---|---|
public static <T> void sort(List<T> list) | 对 List 集合中元素按照默认规则排序 |
课件第 7 页的注意:本方法可以直接对自定义类型的 List 集合排序,但自定义类型必须实现了 Comparable 接口,指定了比较规则才可以。(否则编译不通过)
Comparable 的规则写成口诀就是:compareTo() 里把左边对象当成 this、右边对象当成参数 o,认为左 > 右就返回正整数、左 < 右就返回负整数、相等返回 0,集合就会按这个规则升序排列。
排序方式 2:比较器规则
| 方法名称 | 说明 |
|---|---|
public static <T> void sort(List<T> list, Comparator<? super T> c) | 对 List 集合中元素,按照比较器对象指定的规则进行排序 |
方式 2 把比较规则从类里搬到调用处,好处是不用为了换个排序字段就去改实体类,配合 Lambda 写起来更短。
2.4 代码示例
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 目标:掌握 Collections 集合工具类的使用。
*/
public class CollectionsTest1 {
public static void main(String[] args) {
// 1、public static <T> boolean addAll(Collection<? super T> c, T... elements):为集合批量添加数据
List<String> names = new ArrayList<>();
Collections.addAll(names, "张无忌", "赵敏", "小昭", "殷素素");
System.out.println(names);
// 2、public static void shuffle(List<?> list):打乱 List 集合中的元素顺序
Collections.shuffle(names);
System.out.println(names);
List<Student> students = new ArrayList<>();
Student s1 = new Student("赵敏", 19, 169.5);
Student s2 = new Student("周芷若", 18, 171.5);
Student s3 = new Student("周芷若", 18, 171.5);
Student s4 = new Student("小昭", 17, 165.5);
Collections.addAll(students, s1, s2, s3, s4);
// 3、public static <T> void sort(List<T> list):对 List 集合中的元素进行升序排序
// 方式一:让对象的类实现 Comparable 接口,重写 compareTo 方法,指定大小比较规则
Collections.sort(students);
System.out.println(students);
// 4、public static <T> void sort(List<T> list, Comparator<? super T> c)
// 方式二:指定 Comparator 比较器对象,再指定比较规则(这里按身高降序)
Collections.sort(students, (o1, o2) -> Double.compare(o2.getHeight(), o1.getHeight()));
System.out.println(students);
}
}
// 方式一:让自定义的类实现比较规则接口
public class Student implements Comparable<Student> {
private String name;
private int age;
private double height;
// s1.compareTo(s2):左边对象 s1 就是 this,右边对象 s2 就是参数 o
@Override
public int compareTo(Student o) {
// 制定比较规则:按照年龄排序
// 官方规则:认为左边 > 右边返回正整数;左边 < 右边返回负整数;相等返回 0(自动升序)
// return this.age - o.age; // 升序
return o.age - this.age; // 降序
}
public Student() {
}
public Student(String name, int age, double height) {
this.name = name;
this.age = age;
this.height = height;
}
// 此处省略 getName()/setName()、getAge()/setAge()、getHeight()/setHeight() 与 toString()
}
3. 综合案例:斗地主
3.1 业务需求(课件第 9 页)
在启动游戏房间的时候,应该提前准备好 54 张牌;接着,需要完成洗牌、发牌、对牌排序、看牌。
课件的业务分析:
- 总共有 54 张牌:点数
"3","4","5","6","7","8","9","10","J","Q","K","A","2"(13 个)× 花色"♠","♥","♣","♦"(4 个)= 52 张,再加上大小王"👲"、"🃏"。 - 斗地主:发出 51 张牌,剩下 3 张做为底牌。
3.2 实现思路
课件把”分析实现”拆成”先设计对象、再分步骤操作”,配套示例工程里落成了两个类:
Card(牌):一张牌有number(点数)、color(花色),再加一个size(大小权重,用来排序)。Room(房间):持有ArrayList<Card> allCards,在实例初始化块里造好 54 张牌,start()里依次做洗牌 → 发牌 → 排序 → 看牌。
发牌的思路是:把洗好的牌按索引轮流发给三个玩家(i % 3 决定发给谁),循环条件 i < allCards.size() - 3 保证最后 3 张不发出去,再用 subList() 取出底牌。
排列手牌时用到方式 2 的 Collections.sort(list, Comparator),比较的是牌的 size 权重,而不是点数/花色的字符串。示例工程里用 o2.getSize() - o1.getSize() 得到了从大到小的顺序。
3.3 代码示例
// 牌的实体类(示例工程用 Lombok 简化了 get/set 与构造器)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Card {
private String number; // 点数
private String color; // 花色
private int size; // 大小权重,用于排序
@Override
public String toString() {
return number + color;
}
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
// 房间对象
public class Room {
// 1、准备一副牌
private ArrayList<Card> allCards = new ArrayList<>();
// 2、初始化 54 张牌
{
// 3、准备点数:个数确定,类型确定
String[] numbers = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};
// 4、准备花色:个数确定,类型确定
String[] colors = {"♠", "♥", "♣", "♦"};
// 5、组装每一张牌对象
int size = 0;
for (String number : numbers) {
size++;
for (String color : colors) {
// 6、创建牌对象封装这张牌的数据
Card c = new Card(number, color, size);
// 7、加入到集合中去
allCards.add(c);
}
}
// 8、单独加入大小王
allCards.add(new Card("", "🃏", ++size));
allCards.add(new Card("", "👲", ++size));
System.out.println("新牌是:" + allCards);
}
public void start() {
// 9、洗牌
Collections.shuffle(allCards);
System.out.println("洗牌后:" + allCards);
// 10、发牌:定义三个玩家
List<Card> lhc = new ArrayList<>();
List<Card> ryy = new ArrayList<>();
List<Card> dfbb = new ArrayList<>();
// 发出去 51 张,最后 3 张留作底牌
for (int i = 0; i < allCards.size() - 3; i++) {
Card c = allCards.get(i);
if (i % 3 == 0) {
lhc.add(c);
} else if (i % 3 == 1) {
ryy.add(c);
} else {
dfbb.add(c);
}
}
// 拿到最后三张底牌
List<Card> lastThreeCards = allCards.subList(allCards.size() - 3, allCards.size());
// 抢地主:底牌归地主
ryy.addAll(lastThreeCards);
// 11、对牌排序
sortCards(lhc);
sortCards(ryy);
sortCards(dfbb);
// 12、看牌
System.out.println("啊冲:" + lhc);
System.out.println("啊盈:" + ryy);
System.out.println("啊败:" + dfbb);
}
public void sortCards(List<Card> cards) {
// 方式二:把比较规则写在调用处
Collections.sort(cards, new Comparator<Card>() {
@Override
public int compare(Card o1, Card o2) {
return o2.getSize() - o1.getSize();
}
});
}
}
public class Test {
public static void main(String[] args) {
// 目标:完成斗地主案例的实现。
// 1、每张牌是一个对象,设计一个牌类
// 2、设计一个房间类,用于创建房间对象、开启游戏
Room r = new Room();
r.start();
}
}
(补充说明)subList(from, to) 返回的是原 List 的一个视图,对它的结构修改会影响原集合;本案例只取出来读,所以安全。
4. Map 集合概述
4.1 什么是 Map 集合(课件第 12 页)
课件先在第 11 页用一组”脉劫 / 康帅傅 / 粤利粤”的山寨商品做引子,再给出 Map 的定义:
Map集合称为双列集合,格式:{key1=value1, key2=value2, key3=value3, ...},一次需要存一对数据做为一个元素。Map集合的每个元素”key=value”称为一个键值对 / 键值对对象 / 一个Entry对象,Map集合也被叫做”键值对集合”。Map集合的所有键是不允许重复的,但值可以重复;键和值是一一对应的,每一个键只能找到自己对应的值。
一句话总结课件的第 11–12 页对照:Collection 是单列集合,一次存一个元素;Map 是双列集合,一次存”键 + 值”两个对象,但整体仍是一个元素。
4.2 什么时候用 Map(课件第 13 页)
课件给的判断标准只有一句:需要存储一一对应的数据时,就可以考虑使用 Map 集合来做。
课件举的例子是购物车场景 {商品1=2, 商品2=3, 商品3=2, 商品4=3}——商品是键、数量是值,“同一件商品的数量”正是一一对应的数据:
4.3 Map 集合体系(课件第 14 / 33 / 45 / 48 页)
Map<K, V> ← 接口(双列集合的祖宗)
├── HashMap<K, V> ← 实现类,用得最多
├── LinkedHashMap<K, V> ← 实现类
└── TreeMap<K, V> ← 实现类
| 实现类 | (由键决定)特点 | 课件备注 |
|---|---|---|
HashMap | 无序、不重复、无索引 | 用的最多 |
LinkedHashMap | 有序、不重复、无索引 | 有序指保留添加顺序 |
TreeMap | 按照键的大小默认升序排序、不重复、无索引 | 只能对键排序 |
课件特别强调:Map 系列集合的特点都是由键决定的,值只是一个附属品,值是不做要求的。 这句话解释了为什么后面讲底层原理时,说的都是”键怎么算哈希、键怎么比较大小”。
4.4 小结自测(课件第 15 页)
Map集合是什么?——键值对集合。- 什么时候考虑使用?——需要存储一一对应的数据时。
- 有哪些实现类、各自特点?——
HashMap:键无序、不重复、无索引;LinkedHashMap:键有序、不重复、无索引;TreeMap:键排序、不重复、无索引。(三者的值都不做要求)
5. Map 的常用方法
5.1 为什么要先学 Map 的方法(课件第 17 页)
课件的解释是:Map 是双列集合的祖宗,它的功能是全部双列集合都可以继承过来使用的。 也就是说 HashMap、LinkedHashMap、TreeMap 的增删改查方法都来自 Map,先把这一套学会,后面三个实现类就只需要记”特点差别”。
5.2 常用方法(课件第 18 页)
| 方法名称 | 说明 |
|---|---|
public V put(K key, V value) | 添加元素 |
public int size() | 获取集合的大小 |
public void clear() | 清空集合 |
public boolean isEmpty() | 判断集合是否为空,为空返回 true,反之 |
public V get(Object key) | 根据键获取对应值 |
public V remove(Object key) | 根据键删除整个元素 |
public boolean containsKey(Object key) | 判断是否包含某个键 |
public boolean containsValue(Object value) | 判断是否包含某个值 |
public Set<K> keySet() | 获取全部键的集合 |
public Collection<V> values() | 获取 Map 集合的全部值 |
两个容易踩的点:
get()找不到键时返回null,但存进去的值为null时也返回null,所以不能只凭get() == null判断键是否存在,要判断键在不在应该用containsKey()。put()遇到已经存在的键会覆盖(课件示例注释原话:“后面重复的键会覆盖前面整个数据!”);HashMap的键和值都允许为null。
5.3 代码示例一:Map 集合的特点
import java.util.HashMap;
import java.util.Map;
public class MapDemo1 {
public static void main(String[] args) {
// 目标:掌握 Map 集合的特点
// 特点:按照键,无序、不重复、无索引;值不做要求,键和值都可以是 null
Map<String, Integer> map = new HashMap<>(); // 多态,一行经典代码
map.put("华为手表", 31);
map.put("iphone15", 1);
map.put("mete60", 10);
map.put("Java入门到跑路", 2);
map.put("iphone15", 31); // 后面重复的键会覆盖前面整个数据!
map.put("娃娃", 1);
map.put("雨伞", 10);
map.put(null, null);
// {Java入门到跑路=2, null=null, iphone15=31, 雨伞=10, mete60=10, 华为手表=31, 娃娃=1}
System.out.println(map);
}
}
5.4 代码示例二:常用 API 逐个演示
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapDemo2 {
public static void main(String[] args) {
// 目标:掌握 Map 集合的常用 API(重点)
Map<String, Integer> map = new HashMap<>();
map.put("手表", 2);
map.put("iphone", 31);
map.put("huawei", 365);
map.put("iphone", 1); // 覆盖上面 iphone=31
map.put("娃娃", 31);
map.put("Java入门", 1);
map.put(null, null);
System.out.println(map);
// map = {null=null, 手表=2, huawei=365, Java入门=1, 娃娃=31, iphone=1}
// 1、获取集合的大小(元素个数)
System.out.println(map.size()); // 6
// 2、清空集合
// map.clear();
// 3、判断集合是否为空
System.out.println(map.isEmpty());
// 4、根据键获取对应的值(重点)
System.out.println(map.get("手表")); // 2
System.out.println(map.get("手表2")); // 没有这个键就返回 null
// 5、根据键删除整个数据,返回删除数据对应的值(重点)
System.out.println(map.remove("娃娃")); // 31
System.out.println(map);
// 6、判断是否包含某个键(重点)
System.out.println(map.containsKey("娃娃")); // false
System.out.println(map.containsKey("huawei")); // true
System.out.println(map.containsKey(null)); // true
// 7、判断是否包含某个值
System.out.println(map.containsValue(1)); // true
System.out.println(map.containsValue("1")); // false(类型不同)
// 8、获取 Map 集合的全部键,返回一个 Set 集合
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println(key);
}
// 9、获取 Map 集合的全部值,返回一个 Collection 集合
Collection<Integer> values = map.values();
for (Integer value : values) {
System.out.println(value);
}
}
}
6. Map 集合的遍历方式
课件第 20 页把遍历方式归成三种:① 键找值;② 键值对(把”键值对”看成一个整体进行遍历,难度较大);③ Lambda(JDK 1.8 开始之后的新技术,非常的简单)。
6.1 方式一:键找值(先用 keySet 再 get)
需要用到的方法(课件第 21 页):
| 方法名称 | 说明 |
|---|---|
public Set<K> keySet() | 获取所有键的集合 |
public V get(Object key) | 根据键获取其对应的值 |
流程(课件第 22 页用”编号丈夫 ↔ 编号妻子”打比方):先从 Map 里拿出全部键(Set 集合,可以用增强 for 遍历),然后对每个键调一次 get() 把值找出来。缺点很明显:遍历一次要额外调用一次 get() 去查表。
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapDemo1 {
public static void main(String[] args) {
// 目标:掌握 Map 集合的遍历方式一:键找值
Map<String, Integer> map = new HashMap<>(); // 一行经典代码
map.put("iphone", 6);
map.put("小米", 3);
map.put("huawei", 3);
map.put("诺基亚", 31);
System.out.println(map); // {huawei=3, 诺基亚=31, iphone=6, 小米=3}
// 1、获取 map 集合的全部键
Set<String> keys = map.keySet();
// 2、根据键提取值
for (String key : keys) {
Integer value = map.get(key);
System.out.println(key + "====>" + value);
}
}
}
6.2 方式二:键值对(entrySet + Map.Entry)
课件第 24 页先演示了一个”走不通”的写法:想直接用增强 for 遍历 Map 本身——
for (元素类型 kv : map) { // 编译不通过
...
}
原因是课件第 24 页给出的那句话:元素类型无法直接确定! Map 里的一个元素是”键 + 值”两段数据,Java 里没有这样一个现成的类型可以直接写在 for 的括号里。解决办法是先调 entrySet() 把 Map 转成 Set 集合,集合里的元素类型就明确为 Map.Entry<K, V> 了。
需要用到的方法(课件第 25 页):
| Map 提供的方法 | 说明 |
|---|---|
Set<Map.Entry<K, V>> entrySet() | 获取所有”键值对”的集合 |
| Map.Entry 提供的方法 | 说明 |
|---|---|
K getKey() | 获取键 |
V getValue() | 获取值 |
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapDemo2 {
public static void main(String[] args) {
// 目标:掌握 Map 集合的遍历方式二:键值对遍历(难度大点)
Map<String, Integer> map = new HashMap<>();
map.put("蜘蛛精", 1000);
map.put("小龙女", 23);
map.put("木婉清", 31);
map.put("黄蓉", 35);
System.out.println(map);
// map = {蜘蛛精=1000, 小龙女=23, 黄蓉=35, 木婉清=31}
// 1、直接增强 for 遍历 Map 集合是不行的,因为键值对直接来看是不存在数据类型的
// 2、调用 entrySet() 把 Map 集合转换成 Set 集合来遍历
// map = {蜘蛛精=1000, 小龙女=23, 黄蓉=35, 木婉清=31}
// ↓ map.entrySet()
// Set<Map.Entry<String, Integer>> entries = [(蜘蛛精=1000), (小龙女=23), (黄蓉=35), (木婉清=31)]
// ↑ 每个元素就是一个 Entry 对象
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " ===> " + value);
}
}
}
6.3 方式三:Lambda(forEach)
需要用到的方法(课件第 29 页):
| 方法名称 | 说明 |
|---|---|
default void forEach(BiConsumer<? super K, ? super V> action) | 结合 lambda 遍历 Map 集合 |
BiConsumer 是”接收两个参数、没有返回值”的函数式接口,两个参数正好对应键和值,所以 Lambda 里写 (k, v) -> {...} 就行:
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
public class MapDemo3 {
public static void main(String[] args) {
// 目标:掌握 Map 集合的遍历方式三:Lambda
Map<String, Integer> map = new HashMap<>();
map.put("蜘蛛精", 1000);
map.put("小龙女", 23);
map.put("木婉清", 31);
map.put("黄蓉", 35);
System.out.println(map);
// map = {蜘蛛精=1000, 小龙女=23, 黄蓉=35, 木婉清=31}
// 匿名内部类写法
map.forEach(new BiConsumer<String, Integer>() {
@Override
public void accept(String key, Integer value) {
System.out.println(key + "===>" + value);
}
});
// Lambda 写法
map.forEach((key, value) -> {
System.out.println(key + "===>" + value);
});
}
}
三种方式怎么选:只要键用 keySet();键和值都要用用 entrySet()(省掉一次 get() 查表);只是对每个键值对做一件事(打印、累加等)用 forEach() 最省事。
7. 案例:统计投票人数
7.1 需求与分析(课件第 30–31 页)
某个班级 80 名学生,现在需要组织秋游活动,班长提供了四个景点依次是(A、B、C、D),每个学生只能选择一个景点,请统计出最终哪个景点想去的人数最多。
课件的分析步骤把”Map 做计数”的套路写得很清楚:
- 将 80 个学生选择的数据拿到程序中去,形如
[A, A, B, A, B, C, D, ...]; - 准备一个
Map集合用于存储统计结果,Map<String, Integer>,键是景点,值代表投票数量; - 遍历这 80 个选择,每遍历一个景点,就看
Map中是否存在该景点:不存在就存入”景点=1”,存在则把它对应的值 +1。
7.2 代码示例
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class MapTest4 {
public static void main(String[] args) {
// 目标:使用 Map 集合完成投票案例
// 1、把 80 个学生选择的景点造出来
String[] locations = {"玉龙雪山", "伶仃岛", "三亚", "泰国"};
// 2、定义一个 List 集合,随机挑选 80 个学生想去的景点
List<String> data = new ArrayList<>();
Random r = new Random();
for (int i = 0; i < 80; i++) {
data.add(locations[r.nextInt(locations.length)]);
}
System.out.println(data);
// 3、定义一个 Map 集合,键存储景点,值存储想去的人数
Map<String, Integer> map = new HashMap<>();
for (String s : data) {
// 4、判断 map 集合中是否出现了这个景点键,如果没有出现,说明是第一次添加
if (map.containsKey(s)) {
// 之前统计过,其值 +1
map.put(s, map.get(s) + 1);
} else {
// 第一次添加
map.put(s, 1);
}
}
map.forEach((s, n) -> {
System.out.println(s + "选择人数是:" + n);
});
}
}
统计结束后想知道”哪个景点人最多”,需要再比较一次各个 value(Map 本身没有”按值排序”的功能,TreeMap 也只排键)。
8. HashMap 的底层原理
8.1 与 HashSet 的关系(课件第 34 页)
课件第 34 页一句话把两个集合打通了:
HashMap跟HashSet的底层原理是一模一样的,都是基于哈希表实现的。- 实际上:原来学的 Set 系列集合的底层就是基于
Map实现的,只是 Set 集合中的元素只要键数据,不要值数据而已。
课件给出的源码印证是 HashSet 的构造器:
public HashSet() {
map = new HashMap<>();
}
也就是说,HashSet 里其实”包”了一个 HashMap,往 HashSet 里加的元素被当成了 HashMap 的键。哈希表本身是一种增删改查数据,性能都较好的数据结构。
8.2 哈希表的组成(课件第 34–40 页)
| 版本 | 哈希表 = |
|---|---|
| JDK8 之前 | 数组 + 链表 |
| JDK8 开始 | 数组 + 链表 + 红黑树 |
课件用第 35–40 页的连续动画演示了 map.put() 的过程,关键规则有三条:
- 利用键计算哈希值,跟值无关——先算出键的哈希值,再换算成数组下标,把该键值对放进对应的桶里。
- 同一个下标上如果已经有元素,就以链表形式挂在后面(课件里用一串
null表示空桶,桶被占用后挂成链)。 - 链表长度超过 8 且数组长度 >= 64 时,自动转成红黑树——这就是 JDK8 相比 JDK7 的最大改动,避免极端情况下链表过长导致查询退化。
8.3 HashMap 的特点与键唯一性(课件第 41–42 页)
- 特点:无序、不重复、无索引(由键决定特点),但增删改查性能都较好。
HashMap的键依赖hashCode方法和equals方法保证键的唯一。- 如果键存储的是自定义类型的对象,需要重写
hashCode()和equals()方法,这样才能在多个对象内容一样时,让HashMap认为是重复的键。
(补充说明)课件第 42 页的自测问答就是这两条,背诵版为:“由键决定:无序、不重复、无索引,底层是哈希表结构,增删改查性能都较好”;“键的唯一性靠 hashCode 与 equals,自定义对象作键必须重写这两个方法”。
8.4 案例:HashMap 存储自定义对象并遍历(课件第 43 页)
需求:创建一个
HashMap集合,键是学生对象(Student),值是籍贯(String)。存储三个键值对元素,并遍历。
课件给的思路是标准五步:定义学生类 → 创建 HashMap 集合对象 → 创建学生对象 → 把学生添加到集合 → 遍历集合。配套示例工程把”键是自定义对象”这件事演示得更彻底,用的键是 Movie(电影名、评分、主演):
import java.util.HashMap;
import java.util.Map;
public class MapTest1 {
public static void main(String[] args) {
// 目标:掌握 Map 集合(键)去重复的机制
Map<Movie, String> map = new HashMap<>();
map.put(new Movie("摔跤吧,爸爸", 9.5, "阿米尔汗"), "19:00");
map.put(new Movie("三傻宝莱坞", 8.5, "阿米尔汗2"), "20:50");
map.put(new Movie("三傻宝莱坞", 8.5, "阿米尔汗2"), "21:50"); // 与上一行内容相同 → 覆盖
map.put(new Movie("阿甘正传", 7.5, "汤姆汉克斯"), "21:00");
System.out.println(map);
}
}
@Data // @Data 自带无参构造 + get/set + toString + hashCode + equals
@NoArgsConstructor
@AllArgsConstructor
public class Movie implements Comparable<Movie> {
private String name;
private double score;
private String actor;
@Override
public int compareTo(Movie o) {
return Double.compare(this.score, o.score);
}
}
示例里的 Movie 有 Lombok 的 @Data 注解,它自动生成的 hashCode() / equals() 就是”三个属性都相同即视为同一个键”——所以第二个”三傻宝莱坞”把第一个覆盖掉了(只留下 21:50)。如果这里不重写 hashCode/equals,HashMap 就会把两个 new 出来的对象当成两个不同的键,去重逻辑失效。
9. LinkedHashMap 的原理
9.1 原理(课件第 46 页)
课件的原话:底层数据结构依然是基于哈希表实现的,只是每个键值对元素又额外多了一个双链表的机制记录元素顺序(保证有序)。实际上:原来学习的 LinkedHashSet 集合的底层原理就是 LinkedHashMap。
也就是说:LinkedHashMap = HashMap 的哈希表结构 + 一条贯穿所有 Entry 的双链表。“有序”指的就是这条双链表维持的添加顺序,同样由键决定特点(有序、不重复、无索引),值不做要求。
9.2 代码示例
import java.util.LinkedHashMap;
import java.util.Map;
public class MapTest2 {
public static void main(String[] args) {
// 目标:掌握 Map 集合的特点
// LinkedHashMap:按照键有序(保留添加顺序)、不重复、无索引;值不做要求,键和值都可以是 null
Map<String, Integer> map = new LinkedHashMap<>();
map.put("手表", 2);
map.put("iphone", 31);
map.put("huawei", 365);
map.put("iphone", 1);
map.put("娃娃", 31);
map.put("Java入门", 1);
map.put(null, null);
// {手表=2, iphone=1, huawei=365, 娃娃=31, Java入门=1, null=null}
System.out.println(map);
}
}
对照上面 MapDemo1(HashMap)的输出 {Java入门到跑路=2, null=null, iphone15=31, ...} 可以看到:同样的 put 顺序,HashMap 打印出来是”乱”的,LinkedHashMap 打印出来就是添加顺序。另外注意 iphone 的值被后一次 put 改成了 1,但它的位置仍停在第一次加入的位置(修改不算新插入)。
10. TreeMap
10.1 特点与原理(课件第 49 页)
- 特点:不重复、无索引、可排序(按照键的大小默认升序排序,只能对键排序)。
- 原理:
TreeMap跟TreeSet集合的底层原理是一样的,都是基于红黑树实现的排序。
10.2 两种指定排序规则的方式(课件第 49–50 页)
| 方式 | 做法 |
|---|---|
| 方式一 | 让类实现 Comparable 接口,重写比较规则(compareTo) |
| 方式二 | TreeMap 有一个有参数构造器,支持创建 Comparator 比较器对象,用来指定比较规则 |
课件第 50 页的自测答案就是这两种;效果上,方式二传给构造器的比较器优先级更高(同时存在时会覆盖类里的自然排序)。
import java.util.Map;
import java.util.TreeMap;
public class MapTest3 {
public static void main(String[] args) {
// 目标:了解 TreeMap 集合:按照键升序排序,不重复,无索引
// 方式二:TreeMap 集合可以自带比较器对象指定比较规则
Map<Movie, String> map = new TreeMap<>(
(m1, m2) -> Double.compare(m2.getScore(), m1.getScore())); // 按评分降序
// 方式一:Movie 类实现 Comparable 接口指定比较规则(见第 8.4 节的 Movie)
map.put(new Movie("摔跤吧,爸爸", 9.5, "阿米尔汗"), "19:00");
map.put(new Movie("三傻宝莱坞", 8.5, "阿米尔汗2"), "20:50");
map.put(new Movie("三傻宝莱坞", 8.5, "阿米尔汗2"), "21:50");
map.put(new Movie("阿甘正传", 7.5, "汤姆汉克斯"), "21:00");
System.out.println(map);
}
}
10.3 案例:TreeMap 练习(课件第 51 页)
需求:创建一个
TreeMap集合,键是学生对象(Student),值是籍贯(String)。学生属性姓名和年龄,按照年龄进行排序并遍历。
思路同样是五步:定义学生类 → 创建 TreeMap 集合对象 → 创建学生对象 → 把学生添加到集合 → 遍历集合。关键点在于”按年龄排序”必须落成键的比较规则——要么 Student implements Comparable<Student> 并在 compareTo() 里比较年龄,要么用 new TreeMap<>((s1, s2) -> s1.getAge() - s2.getAge()):
// 按课件需求整理的写法(课堂在 IDE 中演示)
Map<Student, String> map = new TreeMap<>((s1, s2) -> s1.getAge() - s2.getAge());
map.put(new Student("张三", 18, 171.5), "北京");
map.put(new Student("李四", 20, 180.5), "上海");
map.put(new Student("王五", 19, 165.5), "广州");
map.forEach((student, place) -> System.out.println(student.getName() + "->" + place));
// 输出顺序:张三(18) → 王五(19) → 李四(20)
(补充说明)TreeMap 判断”键重复”用的是比较结果是否为 0,而不是 equals();如果比较规则只比年龄,那么年龄相同的两个不同学生会被当成同一个键而被丢掉。
11. 补充知识:集合的嵌套
11.1 什么是集合的嵌套(课件第 53 页)
课件给的一句话定义:集合的嵌套指的就是集合中的元素又是一个集合。 在 Map 体系里最常用的两种嵌套是:
Map<String, List<String>>:一个键对应多个值(一对多),例如”省份 → 城市列表”。Map<String, Map<String, String>>:值本身又是一个Map,例如”班级 → (学号 → 姓名)”。
11.2 案例:省和市(课件第 54 页)
需求:要求在程序中记住如下省份和其对应的城市信息,记录成功后,要求可以查询出湖北省的城市信息。
- 江苏省 = 南京市,扬州市,苏州市,无锡市,常州市
- 湖北省 = 武汉市,孝感市,十堰市,宜昌市,鄂州市
- 河北省 = 石家庄市,唐山市,邢台市,保定市,张家口市
课件的分析:定义一个 Map 集合,键表示省份名称,值表示城市名称,注意城市会有多个。 因为”城市会有多个”,所以值的类型要写成 List<String>;查询时用 provinces.get("湖北省") 拿到列表再展示。
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MapTest4 {
public static void main(String[] args) {
// 目标:掌握集合的嵌套(重点)
// 1、定义一个 Map 集合存储全部省份和城市信息
Map<String, List<String>> provinces = new HashMap<>();
// 2、存入省份信息
List<String> cities1 = new ArrayList<>();
Collections.addAll(cities1, "南京市", "扬州市", "苏州市", "无锡市", "常州市");
provinces.put("江苏省", cities1);
List<String> cities2 = new ArrayList<>();
Collections.addAll(cities2, "武汉市", "襄阳市", "孝感市", "十堰市", "宜昌市");
provinces.put("湖北省", cities2);
List<String> cities3 = new ArrayList<>();
Collections.addAll(cities3, "石家庄市", "唐山市", "邢台市", "保定市", "张家口市");
provinces.put("河北省", cities3);
System.out.println(provinces);
// 3、查询湖北省的城市信息
List<String> hbcities = provinces.get("湖北省");
for (String hbcity : hbcities) {
System.out.println(hbcity);
}
}
}
课件第 54 页写的湖北省城市是”武汉市,孝感市,十堰市,宜昌市,鄂州市”,而配套示例工程里写的是”武汉市,襄阳市,孝感市,十堰市,宜昌市”(少了鄂州市)。两者不一致,以自己课堂实际演示的版本为准即可,本条不属于知识点。
Map 嵌套 Map 的写法见本日作业二第 2 题(学校 → 班级 → 学号 → 姓名),遍历套路是先遍历外层 Map 拿到内层 Map,再遍历内层。
12. 关于 Stream 流:本日课件核对说明
课件文件名是《day07、java.util包下的常用API-集合(Collections、Map、Stream流).pptx》,但本文逐页核对了该文件全部 56 页幻灯片,其中没有任何 Stream 相关内容:第 2 页目录只有”Collection 的其他相关知识(可变参数 / Collections / 综合案例)“和”Map 集合”两块,正文也止于第 54 页的”省和市”案例(第 53、55、56 页是演示用图)。
Stream 流的知识块——认识 Stream、Stream 流的常用方法(中间方法 / 终结方法)、收集 Stream 流——出现在 day08 课件《java.io包下的常用API-io流》第 1–12 页,按文章分工归入 day08,本文不重复展开、也不臆造内容。
(补充说明)day08 课件第 2 页对 Stream 的定义是:“也叫 Stream 流,是 JDK8 开始新增的一套 API(java.util.stream.*),可以用于操作集合或者数组的数据,大量结合 Lambda 语法风格编程,代码更简洁、可读性更好。“本章只做核对说明,具体 API 请对照 day08 的文章。
13. 作业与练习
| 作业 | 文档路径 | 题目与训练目标 |
|---|---|---|
| 作业 1 | day07-Collections、Map/作业/作业1/03_题目/作业1.md | ① 4 名学生按成绩从小到大排序(Comparator、增强 for、Collections);② 随机生成一注双色球,同色号码不重复(HashSet、ArrayList、Collections.sort);③ 京东注册用户重名判断(HashSet + 重写 hashCode/equals) |
| 作业 2 | day07-Collections、Map/作业/作业2/04_题目/作业2.md | ① 统计售出商品的种类与数量(HashMap + HashMap 的遍历);② 用 HashMap 嵌套 HashMap 存储”班级 → 学号 → 姓名”;③ 把商品加入购物车,同店同商品数量加一,不同店铺视为新商品 |
几道题的关键点(都直接对应本日知识块):
- 作业一 · 题目 1:
Collections.sort(list, new Comparator<Student>() {...}),用o1.getScore() - o2.getScore()得到按分数升序;这就是第 2.3 节的”排序方式 2”。 - 作业一 · 题目 2:先
HashSet去重(while (hs.size() < 6)反复生成直到凑够 6 个不重复红球),再把HashSet倒进ArrayList才能Collections.sort()排序——正好印证”Collections只能对List排序”。 - 作业一 · 题目 3:把”用户名 + 密码”封装成
User对象放进HashSet,靠add()的返回值判断成功失败(true成功、false已存在),前提是User重写了hashCode()/equals()。 - 作业二 · 题目 1:
map.containsKey(goods)为真则map.put(goods, map.get(goods) + 1),否则map.put(goods, 1);注意end要在计数之前判断跳出,否则会被当成一件商品。 - 作业二 · 题目 2:
HashMap<String, HashMap<String, String>>,外层键是班级(如”Java基础班”),内层键是学号(如"001")——两个班都有学号001,靠外层分组才不冲突。 - 作业二 · 题目 3(购物车):用
Product(store, name)作为键、数量作为值;先重写hashCode()/equals()让”同店同商品”算同一个键,再用containsKey()判断是新增(存 1)还是get()出来加一后重新put()。这正是第 4.2 节”一一对应的数据”和第 8.3 节”自定义键要去重”两处知识点的合体题。
14. 复习要点
- 可变参数的格式是
数据类型... 参数名;它在方法内部就是一个数组,一个形参列表只能有一个且必须放最后。 Collections是操作集合的工具类(静态方法),能批量添加addAll()、打乱shuffle()、排序sort(),但只能对List排序。Collections.sort()的两种规则:类实现Comparable重写compareTo();或传入Comparator对象——前者改类,后者改调用处。Map是双列集合,元素是Entry(键值对);键不重复、值可重复;需要”一一对应”的数据时就用它。Map是双列集合的祖宗,常用方法:put/get/remove/size/clear/isEmpty/containsKey/containsValue/keySet/values;get()返回null不能证明键不存在。Map的三种遍历:keySet()+get()(键找值)、entrySet()+Entry.getKey()/getValue()(键值对)、forEach((k, v) -> ...)(Lambda);Map不能直接增强for遍历。HashMap底层是哈希表(JDK8 前 = 数组 + 链表;JDK8 起 = 数组 + 链表 + 红黑树,链表长度 > 8 且数组长度 >= 64 转红黑树);键的唯一性靠hashCode()+equals(),自定义对象作键必须重写。HashSet底层就是HashMap(只要键不要值);LinkedHashSet底层就是LinkedHashMap。LinkedHashMap在哈希表基础上多了双链表记录添加顺序(有序);TreeMap基于红黑树,按键排序、不重复、无索引,只能对键排序,可用Comparable或Comparator指定规则。- 集合的嵌套指”集合中的元素又是一个集合”:
Map<String, List<String>>表达一对多(省 → 城市),Map<String, Map<String, String>>表达两层分组(班级 → 学号 → 姓名)。