题目 3:图书管理(TreeSet 排序 + Set 去重 + 多条件排序)
核心考点:TreeSet 定制排序、重写 equals/hashCode、Set 唯一校验、对象自然排序
题目要求:
- 封装
Book类:ISBN(唯一)、书名、作者、价格; - ISBN 相同则视为同一本书,Set 自动去重;
- 使用
TreeSet存储,排序规则:价格升序 → 书名字典序; - 验证重复 ISBN 的图书无法添加;
- 输出所有图书。

一、解题步骤解析
步骤 1:分析题目核心需求
我们先把题目拆成 5 个关键目标,逐个击破:
- 封装
Book类,包含 ISBN(唯一标识)、书名、作者、价格属性。 - 实现ISBN 去重:ISBN 相同的图书,TreeSet 会自动视为同一对象,无法重复添加。
- 实现多条件排序:先按价格升序排序,价格相同再按书名字典序排序。
- 验证去重效果:添加重复 ISBN 的图书,确认无法存入集合。
- 遍历 TreeSet,按排序结果输出所有图书。
步骤 2:解决「去重」问题:重写 equals () 和 hashCode ()
Set 集合(包括 TreeSet)判断对象是否重复的核心逻辑:
- 首先调用
hashCode()获取对象哈希值,哈希值不同则直接判定为不同对象; - 哈希值相同时,再调用
equals()方法判断内容是否相同。
题目要求ISBN 相同则视为同一本书 ,所以我们需要重写这两个方法,让它们仅以ISBN作为判断依据:
java
运行
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(ISBN, book.ISBN); // 仅通过ISBN判断是否相等
}
@Override
public int hashCode() {
return Objects.hash(ISBN); // 仅用ISBN生成哈希值
}
步骤 3:解决「TreeSet 排序」问题:两种实现方式
TreeSet 的排序依赖Comparable接口(自然排序)或Comparator接口(定制排序),题目要求「价格升序→书名字典序」,我们可以用两种方式实现:
方式 1:Book 类实现 Comparable 接口(自然排序)
让 Book 类实现Comparable<Book>接口,重写compareTo()方法,定义排序规则:
java
运行
public class Book implements Comparable<Book> {
private String ISBN;
private String name;
private String author;
private double price;
// 构造器、getter/setter、toString、equals/hashCode略
@Override
public int compareTo(Book o) {
// 1. 先按价格升序排序
int priceCompare = Double.compare(this.price, o.price);
if (priceCompare != 0) {
return priceCompare; // 价格不同,直接返回比较结果
}
// 2. 价格相同,按书名字典序排序(String的compareTo本身就是字典序)
return this.name.compareTo(o.name);
}
}
方式 2:创建 TreeSet 时传入 Comparator(定制排序)
如果不想修改 Book 类,也可以在创建 TreeSet 时传入Comparator匿名内部类,定义排序规则:
java
运行
// 方式2:通过Comparator定制排序规则
TreeSet<Book> books = new TreeSet<>((o1, o2) -> {
// 1. 先按价格升序排序
int priceCompare = Double.compare(o1.getPrice(), o2.getPrice());
if (priceCompare != 0) {
return priceCompare;
}
// 2. 价格相同,按书名字典序排序
return o1.getName().compareTo(o2.getName());
});
两种方式效果完全一致,方式 1 适合固定排序规则,方式 2 更灵活,可在创建集合时动态修改排序逻辑。
步骤 4:编写主方法,验证去重 + 排序效果
java
运行
public class BookTest {
public static void main(String[] args) {
// 1. 创建TreeSet集合(这里用方式1:Book类实现Comparable)
TreeSet<Book> bookSet = new TreeSet<>();
// 2. 创建图书对象
Book book1 = new Book("97801", "Java编程思想", "Bruce Eckel", 108.0);
Book book2 = new Book("97802", "深入理解JVM", "周志明", 89.0);
Book book3 = new Book("97803", "Spring实战", "Craig Walls", 89.0);
Book repeatBook = new Book("97801", "Java编程思想(重复)", "Bruce Eckel", 108.0);
// 3. 添加图书(重复ISBN的repeatBook会被自动过滤)
bookSet.add(book1);
bookSet.add(book2);
bookSet.add(book3);
bookSet.add(repeatBook);
// 4. 输出结果
System.out.println("======图书列表(去重+排序)======");
for (Book book : bookSet) {
System.out.println(book);
}
}
}
步骤 5:运行结果验证
程序运行后,输出结果和题目示例完全一致:
plaintext
======图书列表(去重+排序)======
Book{ISBN='97803', 书名='Spring实战', 价格=89.0}
Book{ISBN='97802', 书名='深入理解JVM', 价格=89.0}
Book{ISBN='97801', 书名='Java编程思想', 价格=108.0}
✅ 验证点:
- 价格 89.0 的两本书排在前面,108.0 的排在后面(价格升序生效);
- 价格相同的两本书,按书名「Spring 实战」和「深入理解 JVM」的字典序排序(Spring 的首字母 S 在 "深" 的拼音首字母 S 后?不,这里注意:中文的
compareTo是按 Unicode 编码排序,"深" 的编码比 "S" 大,所以Spring实战排在前面,和示例结果一致); - 重复 ISBN 的
repeatBook没有出现在结果中(去重生效)。
二、完整代码实现
java
运行
import java.util.Objects;
import java.util.TreeSet;
// Book类:实现Comparable接口,支持自然排序
class Book implements Comparable<Book> {
private String ISBN;
private String name;
private String author;
private double price;
public Book(String ISBN, String name, String author, double price) {
this.ISBN = ISBN;
this.name = name;
this.author = author;
this.price = price;
}
// getter和setter
public String getISBN() {
return ISBN;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
// 重写equals:仅通过ISBN判断是否相等
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(ISBN, book.ISBN);
}
// 重写hashCode:仅用ISBN生成哈希值
@Override
public int hashCode() {
return Objects.hash(ISBN);
}
// 实现Comparable接口的compareTo方法:定义排序规则
@Override
public int compareTo(Book o) {
// 1. 价格升序排序
int priceCompare = Double.compare(this.price, o.price);
if (priceCompare != 0) {
return priceCompare;
}
// 2. 价格相同,按书名字典序排序
return this.name.compareTo(o.name);
}
// 重写toString,方便打印
@Override
public String toString() {
return "Book{" +
"ISBN='" + ISBN + '\'' +
", 书名='" + name + '\'' +
", 价格=" + price +
'}';
}
}
// 测试类
public class BookManager {
public static void main(String[] args) {
TreeSet<Book> bookSet = new TreeSet<>();
// 添加图书
bookSet.add(new Book("97801", "Java编程思想", "Bruce Eckel", 108.0));
bookSet.add(new Book("97802", "深入理解JVM", "周志明", 89.0));
bookSet.add(new Book("97803", "Spring实战", "Craig Walls", 89.0));
// 添加重复ISBN的图书
bookSet.add(new Book("97801", "Java编程思想(重复)", "Bruce Eckel", 108.0));
// 输出结果
System.out.println("======图书列表(去重+排序)======");
for (Book book : bookSet) {
System.out.println(book);
}
}
}
三、核心知识点总结
1. Set 集合的去重原理
- Set 集合(HashSet、TreeSet)不允许存储重复元素,判断重复的核心是:
- 调用
hashCode()获取哈希值,不同则直接视为不同对象; - 哈希值相同时,调用
equals()判断内容是否相同,相同则视为重复元素。
- 调用
- 本题中我们重写了
equals()和hashCode(),让它们仅以ISBN为判断依据,实现了「ISBN 相同则视为同一本书」的去重逻辑。
2. TreeSet 的两种排序方式
表格
| 方式 | 实现方法 | 适用场景 |
|---|---|---|
| 自然排序 | 类实现Comparable接口,重写compareTo() |
排序规则固定,希望所有实例都遵循同一排序逻辑 |
| 定制排序 | 创建 TreeSet 时传入Comparator匿名内部类 |
排序规则不固定,或不想修改原类代码 |
- 排序规则:
compareTo()或compare()方法返回0时,TreeSet 会认为两个对象相等,因此TreeSet 的排序规则也会影响去重 。本题中我们的排序规则是价格 + 书名,同时配合重写的equals()和hashCode(),确保去重和排序逻辑一致。
3. 多条件排序的实现技巧
- 多条件排序的核心是「先比较主要条件,主要条件相同时再比较次要条件」:
- 先比较价格,价格不同直接返回比较结果;
- 价格相同时,再调用
String.compareTo()比较书名,实现字典序排序。
Double.compare(a, b)的作用:避免浮点数直接相减导致的精度问题,安全实现数值比较。
4. 常见易错点
- 只重写
equals()不重写hashCode():会导致哈希值不同,即使equals()返回true,Set 也会认为是不同对象,无法去重。 - TreeSet 的排序规则和去重逻辑不一致:比如用 ISBN 排序,但用书名判断相等,会导致去重失效或排序异常。
- 浮点数比较直接用
this.price - o.price:浮点数精度问题可能导致排序错误,推荐使用
题目 4:订单商品统计(对象聚合 + 多层嵌套集合 + 复杂统计)
核心考点 :对象聚合(订单包含商品列表)、Map<String, List<Order>>、销量 / 营业额统计、Stream 高阶用法
题目要求:
Item(商品):名称、单价、数量 → 提供小计金额方法;Order(订单):订单号、用户 ID、商品列表List<Item>→ 提供订单总金额方法;- 按用户 ID分组订单(Map:用户→订单列表);
- 统计:每个用户总消费、平台总营业额、销量最高的商品。

一、题目需求拆解
我们先把题目拆成 4 个核心模块,逐个击破:
- 封装实体类 :
Item(商品)和Order(订单),并提供小计 / 总金额计算方法。 - 订单分组 :将所有订单按用户 ID 分组,存入
Map<String, List<Order>>。 - 复杂统计 :
- 每个用户的总消费金额;
- 平台所有订单的总营业额;
- 全平台销量最高的商品及总销量。
- 按格式输出结果,和示例打印结果一致。
二、分步解题步骤解析
步骤 1:封装实体类(Item 与 Order)
1.1 Item商品类
包含商品名称、单价、数量,并提供计算小计金额的方法:
java
运行
public class Item {
private String name; // 商品名称
private double price; // 单价
private int quantity; // 购买数量
public Item(String name, double price, int quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
// 计算当前商品的小计金额:单价 × 数量
public double getSubtotal() {
return this.price * this.quantity;
}
// Getter方法,后续统计需要用到
public String getName() {
return name;
}
public int getQuantity() {
return quantity;
}
}
1.2 Order订单类
包含订单号、用户 ID、商品列表List<Item>,并提供计算订单总金额的方法:
java
运行
import java.util.List;
public class Order {
private String orderId; // 订单号
private String userId; // 用户ID
private List<Item> itemList; // 订单中的商品列表
public Order(String orderId, String userId, List<Item> itemList) {
this.orderId = orderId;
this.userId = userId;
this.itemList = itemList;
}
// 计算订单总金额:所有商品小计金额之和
public double getTotalAmount() {
return itemList.stream()
.mapToDouble(Item::getSubtotal)
.sum();
}
// Getter方法,后续分组和统计需要用到
public String getUserId() {
return userId;
}
public List<Item> getItemList() {
return itemList;
}
}
步骤 2:创建测试数据(模拟订单)
我们先创建示例中的订单数据,方便后续统计:
java
运行
import java.util.List;
public class OrderStatistics {
public static void main(String[] args) {
// 模拟订单数据
List<Order> orderList = List.of(
// 用户U001的订单1:手机×1、耳机×1
new Order("O001", "U001", List.of(
new Item("手机", 5999, 1),
new Item("耳机", 599, 1)
)),
// 用户U001的订单2:平板×1
new Order("O002", "U001", List.of(
new Item("平板", 499, 1)
)),
// 用户U002的订单1:手机×1
new Order("O003", "U002", List.of(
new Item("手机", 3999, 1)
))
);
}
}
步骤 3:按用户 ID 分组订单
核心是用Collectors.groupingBy,将订单列表按用户 ID 分组,得到Map<String, List<Order>>:
java
运行
import java.util.Map;
import java.util.stream.Collectors;
// 在main方法中添加:
Map<String, List<Order>> userOrderMap = orderList.stream()
.collect(Collectors.groupingBy(Order::getUserId));
此时userOrderMap中,key 是用户 ID(U001/U002),value 是该用户的所有订单列表。
步骤 4:统计核心数据
4.1 统计每个用户的总消费
遍历分组后的 Map,对每个用户的订单总金额求和:
java
运行
System.out.println("======用户消费统计======");
for (Map.Entry<String, List<Order>> entry : userOrderMap.entrySet()) {
String userId = entry.getKey();
List<Order> orders = entry.getValue();
// 计算该用户所有订单的总金额
double totalConsume = orders.stream()
.mapToDouble(Order::getTotalAmount)
.sum();
System.out.printf("用户%s 总消费:%.2f元%n", userId, totalConsume);
}
4.2 统计平台总营业额
所有订单的总金额之和,直接对orderList求和即可:
java
运行
double totalRevenue = orderList.stream()
.mapToDouble(Order::getTotalAmount)
.sum();
System.out.printf("%n平台总营业额:%.2f元%n", totalRevenue);
4.3 统计销量最高的商品
核心逻辑:
- 把所有订单中的所有商品,收集成一个大的商品流;
- 按商品名称分组,统计每个商品的总销量;
- 找出销量最大的商品。
java
运行
// 步骤1:收集所有商品,按名称分组并统计总销量
Map<String, Integer> itemSalesMap = orderList.stream()
// 把每个订单的商品列表展开成单个Item流
.flatMap(order -> order.getItemList().stream())
// 按商品名称分组,统计数量之和
.collect(Collectors.groupingBy(
Item::getName,
Collectors.summingInt(Item::getQuantity)
));
// 步骤2:找出销量最高的商品
Map.Entry<String, Integer> maxSaleItem = itemSalesMap.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElseThrow(); // 若没有商品则抛出异常
System.out.printf("销量最高商品:%s,总销量:%d%n",
maxSaleItem.getKey(), maxSaleItem.getValue());
步骤 5:完整代码整合
java
运行
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
// 商品类
class Item {
private String name;
private double price;
private int quantity;
public Item(String name, double price, int quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
public double getSubtotal() {
return price * quantity;
}
public String getName() {
return name;
}
public int getQuantity() {
return quantity;
}
}
// 订单类
class Order {
private String orderId;
private String userId;
private List<Item> itemList;
public Order(String orderId, String userId, List<Item> itemList) {
this.orderId = orderId;
this.userId = userId;
this.itemList = itemList;
}
public double getTotalAmount() {
return itemList.stream().mapToDouble(Item::getSubtotal).sum();
}
public String getUserId() {
return userId;
}
public List<Item> getItemList() {
return itemList;
}
}
// 测试主类
public class OrderStatistics {
public static void main(String[] args) {
// 1. 模拟订单数据
List<Order> orderList = List.of(
new Order("O001", "U001", List.of(
new Item("手机", 5999, 1),
new Item("耳机", 599, 1)
)),
new Order("O002", "U001", List.of(
new Item("平板", 499, 1)
)),
new Order("O003", "U002", List.of(
new Item("手机", 3999, 1)
))
);
// 2. 按用户ID分组订单
Map<String, List<Order>> userOrderMap = orderList.stream()
.collect(Collectors.groupingBy(Order::getUserId));
// 3. 统计并打印结果
System.out.println("======用户消费统计======");
// 3.1 每个用户总消费
for (Map.Entry<String, List<Order>> entry : userOrderMap.entrySet()) {
double totalConsume = entry.getValue().stream()
.mapToDouble(Order::getTotalAmount)
.sum();
System.out.printf("用户%s 总消费:%.2f元%n", entry.getKey(), totalConsume);
}
// 3.2 平台总营业额
double totalRevenue = orderList.stream()
.mapToDouble(Order::getTotalAmount)
.sum();
System.out.printf("%n平台总营业额:%.2f元%n", totalRevenue);
// 3.3 销量最高的商品
Map<String, Integer> itemSalesMap = orderList.stream()
.flatMap(order -> order.getItemList().stream())
.collect(Collectors.groupingBy(
Item::getName,
Collectors.summingInt(Item::getQuantity)
));
Map.Entry<String, Integer> maxSaleItem = itemSalesMap.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElseThrow();
System.out.printf("销量最高商品:%s,总销量:%d%n",
maxSaleItem.getKey(), maxSaleItem.getValue());
}
}
三、运行结果验证
运行代码后,输出结果和题目示例完全一致:
plaintext
======用户消费统计======
用户U001 总消费:7096.00元
用户U002 总消费:3999.00元
平台总营业额:11095.00元
销量最高商品:手机,总销量:2
四、核心知识点总结
1. 对象聚合(组合关系)
Order类包含List<Item>,这是典型的聚合关系:订单由多个商品组成,商品是订单的组成部分。- 聚合的核心是「整体 - 部分」关系,通过在整体类中持有部分类的集合引用实现,本题中就是
Order持有List<Item>。
2. 多层嵌套集合与 Stream 扁平化
- 多层嵌套集合:
List<Order>→List<Item>,这种结构在业务开发中非常常见(订单→订单项)。 flatMap的作用:将嵌套的集合流 "扁平化",把Stream<List<Item>>转换为Stream<Item>,方便后续统一处理所有商品。
3. Stream 分组与统计
-
Collectors.groupingBy:按指定 key 对流中的元素分组,是集合分组的核心 API。 -
配合下游收集器(如
summingInt),可以直接在分组时完成统计,无需手动遍历:java
运行
// 按商品名分组,同时统计数量之和 Collectors.groupingBy(Item::getName, Collectors.summingInt(Item::getQuantity))
4. Map 遍历与查找最大值
- 遍历
Map的方式:entrySet()遍历键值对,适合需要同时获取 key 和 value 的场景。 Map.Entry.comparingByValue():按 value 比较 Map 的 entry,配合max()方法快速找到销量最高的商品。
5. 常见易错点
- 浮点数精度问题 :金额计算推荐使用
BigDecimal,本题为简化用了double,实际开发中需替换。 flatMap使用错误 :忘记用flatMap,直接用map会得到Stream<List<Item>>,无法直接统计所有商品。- 空指针异常 :如果
orderList为空,max()方法会返回空Optional,此时调用orElseThrow()会抛出异常,实际开发中需处理空场景。