面向对象和集合编程题 ( 二 )

题目 3:图书管理(TreeSet 排序 + Set 去重 + 多条件排序)

核心考点:TreeSet 定制排序、重写 equals/hashCode、Set 唯一校验、对象自然排序

题目要求

  1. 封装Book类:ISBN(唯一)、书名、作者、价格;
  2. ISBN 相同则视为同一本书,Set 自动去重;
  3. 使用TreeSet存储,排序规则:价格升序 → 书名字典序;
  4. 验证重复 ISBN 的图书无法添加;
  5. 输出所有图书。

一、解题步骤解析

步骤 1:分析题目核心需求

我们先把题目拆成 5 个关键目标,逐个击破:

  1. 封装Book类,包含 ISBN(唯一标识)、书名、作者、价格属性。
  2. 实现ISBN 去重:ISBN 相同的图书,TreeSet 会自动视为同一对象,无法重复添加。
  3. 实现多条件排序:先按价格升序排序,价格相同再按书名字典序排序。
  4. 验证去重效果:添加重复 ISBN 的图书,确认无法存入集合。
  5. 遍历 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}

✅ 验证点:

  1. 价格 89.0 的两本书排在前面,108.0 的排在后面(价格升序生效);
  2. 价格相同的两本书,按书名「Spring 实战」和「深入理解 JVM」的字典序排序(Spring 的首字母 S 在 "深" 的拼音首字母 S 后?不,这里注意:中文的compareTo是按 Unicode 编码排序,"深" 的编码比 "S" 大,所以Spring实战排在前面,和示例结果一致);
  3. 重复 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)不允许存储重复元素,判断重复的核心是:
    1. 调用hashCode()获取哈希值,不同则直接视为不同对象;
    2. 哈希值相同时,调用equals()判断内容是否相同,相同则视为重复元素。
  • 本题中我们重写了equals()hashCode(),让它们仅以ISBN为判断依据,实现了「ISBN 相同则视为同一本书」的去重逻辑。

2. TreeSet 的两种排序方式

表格

方式 实现方法 适用场景
自然排序 类实现Comparable接口,重写compareTo() 排序规则固定,希望所有实例都遵循同一排序逻辑
定制排序 创建 TreeSet 时传入Comparator匿名内部类 排序规则不固定,或不想修改原类代码
  • 排序规则:compareTo()compare()方法返回0时,TreeSet 会认为两个对象相等,因此TreeSet 的排序规则也会影响去重 。本题中我们的排序规则是价格 + 书名,同时配合重写的equals()hashCode(),确保去重和排序逻辑一致。

3. 多条件排序的实现技巧

  • 多条件排序的核心是「先比较主要条件,主要条件相同时再比较次要条件」:
    1. 先比较价格,价格不同直接返回比较结果;
    2. 价格相同时,再调用String.compareTo()比较书名,实现字典序排序。
  • Double.compare(a, b)的作用:避免浮点数直接相减导致的精度问题,安全实现数值比较。

4. 常见易错点

  1. 只重写equals()不重写hashCode():会导致哈希值不同,即使equals()返回true,Set 也会认为是不同对象,无法去重。
  2. TreeSet 的排序规则和去重逻辑不一致:比如用 ISBN 排序,但用书名判断相等,会导致去重失效或排序异常。
  3. 浮点数比较直接用this.price - o.price:浮点数精度问题可能导致排序错误,推荐使用

题目 4:订单商品统计(对象聚合 + 多层嵌套集合 + 复杂统计)

核心考点 :对象聚合(订单包含商品列表)、Map<String, List<Order>>、销量 / 营业额统计、Stream 高阶用法

题目要求

  1. Item(商品):名称、单价、数量 → 提供小计金额方法;
  2. Order(订单):订单号、用户 ID、商品列表List<Item> → 提供订单总金额方法;
  3. 用户 ID分组订单(Map:用户→订单列表);
  4. 统计:每个用户总消费、平台总营业额、销量最高的商品

一、题目需求拆解

我们先把题目拆成 4 个核心模块,逐个击破:

  1. 封装实体类Item(商品)和Order(订单),并提供小计 / 总金额计算方法。
  2. 订单分组 :将所有订单按用户 ID 分组,存入Map<String, List<Order>>
  3. 复杂统计
    • 每个用户的总消费金额;
    • 平台所有订单的总营业额;
    • 全平台销量最高的商品及总销量。
  4. 按格式输出结果,和示例打印结果一致。

二、分步解题步骤解析

步骤 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 统计销量最高的商品

核心逻辑:

  1. 把所有订单中的所有商品,收集成一个大的商品流;
  2. 按商品名称分组,统计每个商品的总销量;
  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. 常见易错点

  1. 浮点数精度问题 :金额计算推荐使用BigDecimal,本题为简化用了double,实际开发中需替换。
  2. flatMap使用错误 :忘记用flatMap,直接用map会得到Stream<List<Item>>,无法直接统计所有商品。
  3. 空指针异常 :如果orderList为空,max()方法会返回空Optional,此时调用orElseThrow()会抛出异常,实际开发中需处理空场景。
相关推荐
黄毛火烧雪下1 小时前
Java 核心知识点总结(一)
java·开发语言
其实防守也摸鱼2 小时前
软件安全与漏洞--软件安全编码与防御技术理论题库
开发语言·网络·安全·网络安全·软件安全·软件安全与漏洞
不好听6132 小时前
深入理解链表:线性数据结构的另一面
javascript·数据结构
x138702859572 小时前
c语言中srtlen(指针使用计算字符长度)、传值和传址调用
c语言·开发语言·算法·visual studio
海兰2 小时前
【实用程序】电商销售分析仪表盘 — 从零搭建一个AI参与的全栈数据洞察系统
人工智能·学习·算法
iCxhust2 小时前
C#进程管理程序
开发语言·汇编·stm32·单片机·c#·微机原理
凡人叶枫2 小时前
Effective C++ 条款28:避免使用 handles 指向对象内部
linux·服务器·开发语言·c++·嵌入式开发
技术小结-李爽2 小时前
【工具】Maven的下载、安装、使用
java·maven
极创信息2 小时前
Linux挖矿病毒深度清理实战教程,从进程隐藏、Rootkit驻留到彻底根除
java·大数据·linux·运维·安全·tomcat·健康医疗
努力成为AK大王2 小时前
并发编程的核心挑战、优化方案与核心知识点总结
java·开发语言·数据库